Cómo conectarse a una red

Para poder realizar operaciones de red en tu app, el manifiesto debe incluir los siguientes permisos:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Cómo diseñar una comunicación de red segura

Antes de agregar funciones de red a tu app, debes asegurarte de que los datos y la información que contenga permanezcan seguros cuando se transmitan a través de una red. Para ello, sigue estas prácticas recomendadas para la seguridad de redes:

  • Minimiza la cantidad de datos de usuario personales o confidenciales que transmites a través de la red.
  • Envía todo el tráfico de red de tu app a través de SSL.
  • Considera la posibilidad de crear una configuración de seguridad de red que permita a tu app confiar en las CA personalizadas o restringir el conjunto de CA de sistema en las que confía para brindar una comunicación segura.

Si quieres obtener más información sobre la aplicación de principios de redes seguras, consulta las sugerencias de seguridad de redes de Android. También puedes ver el ejemplo de Android NetworkConnect.

Cómo elegir un cliente HTTP

La mayoría de las apps de Android conectadas a la red utilizan HTTP para enviar y recibir datos. La plataforma Android incluye el cliente HttpsURLConnection, que admite TLS, cargas y descargas de transmisión, tiempos de espera configurables, IPv6 y agrupaciones de conexiones.

Cómo introducir las operaciones de red en un subproceso independiente

Para evitar crear una IU que no responda, no realices operaciones de red en el procesamiento de IU. De forma predeterminada, Android 3.0 (nivel de API 11) y versiones posteriores requieren que realices operaciones de red en un procesamiento de IU que no sea el principal. De lo contrario, se arrojará un NetworkOnMainThreadException.

El siguiente fragmento de Activity utiliza un Fragment sin interfaz gráfica para encapsular las operaciones de red asíncronas. Más adelante, verás cómo la implementación de Fragment, NetworkFragment, logra esto. Tu elemento Activity también debería implementar la interfaz DownloadCallback, a fin de permitir que el fragmento vuelva a invocar a Activity en caso de que necesite un estado de conectividad o enviar una actualización a la IU.

Kotlin

class MainActivity : FragmentActivity(), DownloadCallback<String> {

    ...

    // Keep a reference to the NetworkFragment, which owns the AsyncTask object
    // that is used to execute network ops.
    private var networkFragment: NetworkFragment? = null

    // Boolean telling us whether a download is in progress, so we don't trigger overlapping
    // downloads with consecutive button clicks.
    private var downloading = false

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        networkFragment = NetworkFragment.getInstance(supportFragmentManager, "https://www.google.com")
    }

    private fun startDownload() {
        if (!downloading) {
            // Execute the async download.
            networkFragment?.apply {
                startDownload()
                downloading = true
            }
        }
    }
}

Java

public class MainActivity extends FragmentActivity implements DownloadCallback {

    ...

    // Keep a reference to the NetworkFragment, which owns the AsyncTask object
    // that is used to execute network ops.
    private NetworkFragment networkFragment;

    // Boolean telling us whether a download is in progress, so we don't trigger overlapping
    // downloads with consecutive button clicks.
    private boolean downloading = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        networkFragment = NetworkFragment.getInstance(getSupportFragmentManager(), "https://www.google.com");
    }

    private void startDownload() {
        if (!downloading && networkFragment != null) {
            // Execute the async download.
            networkFragment.startDownload();
            downloading = true;
        }
    }
}

Como mínimo, tu interfaz DownloadCallback debe consistir en lo siguiente:

Kotlin

const val ERROR = -1
const val CONNECT_SUCCESS = 0
const val GET_INPUT_STREAM_SUCCESS = 1
const val PROCESS_INPUT_STREAM_IN_PROGRESS = 2
const val PROCESS_INPUT_STREAM_SUCCESS = 3

interface DownloadCallback<T> {

    /**
     * Indicates that the callback handler needs to update its appearance or information based on
     * the result of the task. Expected to be called from the main thread.
     */
    fun updateFromDownload(result: T?)

    /**
     * Get the device's active network status in the form of a NetworkInfo object.
     */
    fun getActiveNetworkInfo(): NetworkInfo

    /**
     * Indicate to callback handler any progress update.
     * @param progressCode must be one of the constants defined in DownloadCallback.Progress.
     * @param percentComplete must be 0-100.
     */
    fun onProgressUpdate(progressCode: Int, percentComplete: Int)

    /**
     * Indicates that the download operation has finished. This method is called even if the
     * download hasn't completed successfully.
     */
    fun finishDownloading()
}

Java

public interface DownloadCallback<T> {
    interface Progress {
        int ERROR = -1;
        int CONNECT_SUCCESS = 0;
        int GET_INPUT_STREAM_SUCCESS = 1;
        int PROCESS_INPUT_STREAM_IN_PROGRESS = 2;
        int PROCESS_INPUT_STREAM_SUCCESS = 3;
    }

    /**
     * Indicates that the callback handler needs to update its appearance or information based on
     * the result of the task. Expected to be called from the main thread.
     */
    void updateFromDownload(T result);

    /**
     * Get the device's active network status in the form of a NetworkInfo object.
     */
    NetworkInfo getActiveNetworkInfo();

    /**
     * Indicate to callback handler any progress update.
     * @param progressCode must be one of the constants defined in DownloadCallback.Progress.
     * @param percentComplete must be 0-100.
     */
    void onProgressUpdate(int progressCode, int percentComplete);

    /**
     * Indicates that the download operation has finished. This method is called even if the
     * download hasn't completed successfully.
     */
    void finishDownloading();
}

Ahora, agrega las siguientes implementaciones de los métodos de la interfaz DownloadCallback a tu Activity:

Kotlin

override fun updateFromDownload(result: String?) {
    // Update your UI here based on result of download.
}

override fun getActiveNetworkInfo(): NetworkInfo {
    val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    return connectivityManager.activeNetworkInfo
}

override fun onProgressUpdate(progressCode: Int, percentComplete: Int) {
    when (progressCode) {
    // You can add UI behavior for progress updates here.
        ERROR -> {
        }
        CONNECT_SUCCESS -> {
        }
        GET_INPUT_STREAM_SUCCESS -> {
        }
        PROCESS_INPUT_STREAM_IN_PROGRESS -> {
        }
        PROCESS_INPUT_STREAM_SUCCESS -> {
        }
    }
}

override fun finishDownloading() {
    downloading = false
    networkFragment?.cancelDownload()
}

Java

@Override
public void updateFromDownload(String result) {
    // Update your UI here based on result of download.
}

@Override
public NetworkInfo getActiveNetworkInfo() {
    ConnectivityManager connectivityManager =
            (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
    return networkInfo;
}

@Override
public void onProgressUpdate(int progressCode, int percentComplete) {
    switch(progressCode) {
        // You can add UI behavior for progress updates here.
        case Progress.ERROR:
            ...
            break;
        case Progress.CONNECT_SUCCESS:
            ...
            break;
        case Progress.GET_INPUT_STREAM_SUCCESS:
            ...
            break;
        case Progress.PROCESS_INPUT_STREAM_IN_PROGRESS:
            ...
            break;
        case Progress.PROCESS_INPUT_STREAM_SUCCESS:
            ...
            break;
    }
}

@Override
public void finishDownloading() {
    downloading = false;
    if (networkFragment != null) {
        networkFragment.cancelDownload();
    }
}

Cómo implementar un fragmento sin interfaz gráfica para encapsular operaciones de red

Dado que NetworkFragment se ejecuta en el procesamiento de IU de forma predeterminada, utiliza AsyncTask para ejecutar las operaciones de red en un subproceso en segundo plano. Se considera que este Fragment no tiene interfaz gráfica porque no hace referencia a ningún elemento de la IU. En su lugar, solo se utiliza para encapsular la lógica y gestionar los eventos del ciclo de vida, dejando que el elemento Activity del host actualice la IU.

Cuando utilices una subclase de AsyncTask para ejecutar operaciones de red, debes tener cuidado de no crear una fuga de memoria en el caso de que el elemento Activity al que hace referencia AsyncTask se destruya antes de que AsyncTask termine su trabajo en segundo plano. Para garantizar que esto no suceda, el siguiente fragmento borra cualquier referencia a Activity en el método onDetach() del elemento Fragment.

Kotlin

private const val TAG = "NetworkFragment"
private const val URL_KEY = "UrlKey"

class NetworkFragment : Fragment() {
    private var mCallback: DownloadCallback<String>? = null
    private var downloadTask: DownloadTask? = null
    private var urlString: String? = null

    companion object {
        /**
         * Static initializer for NetworkFragment that sets the URL of the host it will be
         * downloading from.
         */
        fun getInstance(fragmentManager: FragmentManager, url: String): NetworkFragment {
            val networkFragment = NetworkFragment()
            val args = Bundle()
            args.putString(URL_KEY, url)
            networkFragment.arguments = args
            fragmentManager.beginTransaction().add(networkFragment, TAG).commit()
            return networkFragment
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        urlString = arguments?.getString(URL_KEY)
        ...
    }

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        // Host Activity will handle callbacks from task.
        mCallback = context as? DownloadCallback<String>
    }

    override fun onDetach() {
        super.onDetach()
        // Clear reference to host Activity to avoid memory leak.
        mCallback = null
    }

    override fun onDestroy() {
        // Cancel task when Fragment is destroyed.
        cancelDownload()
        super.onDestroy()
    }

    /**
     * Start non-blocking execution of DownloadTask.
     */
    fun startDownload() {
        cancelDownload()
        mCallback?.also { callback ->
            downloadTask = DownloadTask(callback).apply {
                execute(urlString)
            }
        }
    }

    /**
     * Cancel (and interrupt if necessary) any ongoing DownloadTask execution.
     */
    fun cancelDownload() {
        downloadTask?.cancel(true)
    }

    ...
}

Java

/**
 * Implementation of headless Fragment that runs an AsyncTask to fetch data from the network.
 */
public class NetworkFragment extends Fragment {
    public static final String TAG = "NetworkFragment";

    private static final String URL_KEY = "UrlKey";

    private DownloadCallback<String> mCallback;
    private DownloadTask downloadTask;
    private String urlString;

    /**
     * Static initializer for NetworkFragment that sets the URL of the host it will be downloading
     * from.
     */
    public static NetworkFragment getInstance(FragmentManager fragmentManager, String url) {
        NetworkFragment networkFragment = new NetworkFragment();
        Bundle args = new Bundle();
        args.putString(URL_KEY, url);
        networkFragment.setArguments(args);
        fragmentManager.beginTransaction().add(networkFragment, TAG).commit();
        return networkFragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        urlString = getArguments().getString(URL_KEY);
        ...
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        // Host Activity will handle callbacks from task.
        mCallback = (DownloadCallback<String>) context;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        // Clear reference to host Activity to avoid memory leak.
        mCallback = null;
    }

    @Override
    public void onDestroy() {
        // Cancel task when Fragment is destroyed.
        cancelDownload();
        super.onDestroy();
    }

    /**
     * Start non-blocking execution of DownloadTask.
     */
    public void startDownload() {
        cancelDownload();
        downloadTask = new DownloadTask(mCallback);
        downloadTask.execute(urlString);
    }

    /**
     * Cancel (and interrupt if necessary) any ongoing DownloadTask execution.
     */
    public void cancelDownload() {
        if (downloadTask != null) {
            downloadTask.cancel(true);
        }
    }

    ...
}

Ahora, deberías implementar una subclase de AsyncTask como una clase interna privada dentro de tu Fragment:

Kotlin

/**
 * Implementation of AsyncTask designed to fetch data from the network.
 */
private class DownloadTask(callback: DownloadCallback<String>)
    : AsyncTask<String, Int, DownloadTask.Result>() {

    private var mCallback: DownloadCallback<String>? = null

    init {
        setCallback(callback)
    }

    internal fun setCallback(callback: DownloadCallback<String>) {
        mCallback = callback
    }

    /**
     * Wrapper class that serves as a union of a result value and an exception. When the download
     * task has completed, either the result value or exception can be a non-null value.
     * This allows you to pass exceptions to the UI thread that were thrown during doInBackground().
     */
    internal class Result {
        var mResultValue: String? = null
        var mException: Exception? = null

        constructor(resultValue: String) {
            mResultValue = resultValue
        }

        constructor(exception: Exception) {
            mException = exception
        }
    }

    /**
     * Cancel background network operation if we do not have network connectivity.
     */
    override fun onPreExecute() {
        if (mCallback != null) {
            val networkInfo = mCallback?.getActiveNetworkInfo()
            if (networkInfo?.isConnected == false
                    || networkInfo?.type != ConnectivityManager.TYPE_WIFI
                    && networkInfo?.type != ConnectivityManager.TYPE_MOBILE) {
                // If no connectivity, cancel task and update Callback with null data.
                mCallback?.updateFromDownload(null)
                cancel(true)
            }
        }
    }

    /**
     * Defines work to perform on the background thread.
     */
    override fun doInBackground(vararg urls: String): DownloadTask.Result? {
        var result: Result? = null
        if (!isCancelled && urls.isNotEmpty()) {
            val urlString = urls[0]
            result = try {
                val url = URL(urlString)
                val resultString = downloadUrl(url)
                if (resultString != null) {
                    Result(resultString)
                } else {
                    throw IOException("No response received.")
                }
            } catch (e: Exception) {
                Result(e)
            }

        }
        return result
    }

    /**
     * Updates the DownloadCallback with the result.
     */
    override fun onPostExecute(result: Result?) {
        mCallback?.apply {
            result?.mException?.also { exception ->
                updateFromDownload(exception.message)
                return
            }
            result?.mResultValue?.also { resultValue ->
                updateFromDownload(resultValue)
                return
            }
            finishDownloading()
        }
    }

    /**
     * Override to add special behavior for cancelled AsyncTask.
     */
    override fun onCancelled(result: Result) {}
}

Java

/**
 * Implementation of AsyncTask designed to fetch data from the network.
 */
private class DownloadTask extends AsyncTask<String, Integer, DownloadTask.Result> {

    private DownloadCallback<String> mCallback;

    DownloadTask(DownloadCallback<String> callback) {
        setCallback(callback);
    }

    void setCallback(DownloadCallback<String> callback) {
        mCallback = callback;
    }

     /**
     * Wrapper class that serves as a union of a result value and an exception. When the download
     * task has completed, either the result value or exception can be a non-null value.
     * This allows you to pass exceptions to the UI thread that were thrown during doInBackground().
     */
    static class Result {
        public String mResultValue;
        public Exception mException;
        public Result(String resultValue) {
            mResultValue = resultValue;
        }
        public Result(Exception exception) {
            mException = exception;
        }
    }

    /**
     * Cancel background network operation if we do not have network connectivity.
     */
    @Override
    protected void onPreExecute() {
        if (mCallback != null) {
            NetworkInfo networkInfo = mCallback.getActiveNetworkInfo();
            if (networkInfo == null || !networkInfo.isConnected() ||
                    (networkInfo.getType() != ConnectivityManager.TYPE_WIFI
                            && networkInfo.getType() != ConnectivityManager.TYPE_MOBILE)) {
                // If no connectivity, cancel task and update Callback with null data.
                mCallback.updateFromDownload(null);
                cancel(true);
            }
        }
    }

    /**
     * Defines work to perform on the background thread.
     */
    @Override
    protected DownloadTask.Result doInBackground(String... urls) {
        Result result = null;
        if (!isCancelled() && urls != null && urls.length > 0) {
            String urlString = urls[0];
            try {
                URL url = new URL(urlString);
                String resultString = downloadUrl(url);
                if (resultString != null) {
                    result = new Result(resultString);
                } else {
                    throw new IOException("No response received.");
                }
            } catch(Exception e) {
                result = new Result(e);
            }
        }
        return result;
    }

    /**
     * Updates the DownloadCallback with the result.
     */
    @Override
    protected void onPostExecute(Result result) {
        if (result != null && mCallback != null) {
            if (result.mException != null) {
                mCallback.updateFromDownload(result.mException.getMessage());
            } else if (result.mResultValue != null) {
                mCallback.updateFromDownload(result.mResultValue);
            }
            mCallback.finishDownloading();
        }
    }

    /**
     * Override to add special behavior for cancelled AsyncTask.
     */
    @Override
    protected void onCancelled(Result result) {
    }
}

Cómo usar HttpsUrlConnection para obtener datos

En el fragmento anterior, el método doInBackground() se ejecuta en un subproceso en segundo plano e invoca al método de ayuda downloadUrl(). downloadUrl() debería tomar la URL en cuestión y usarla para realizar una solicitud HTTP GET. Una vez establecida la conexión, debes utilizar el método getInputStream() para recuperar los datos como InputStream. El siguiente fragmento utiliza la API de HttpsURLConnection para lograr esto:

Kotlin

/**
 * Given a URL, sets up a connection and gets the HTTP response body from the server.
 * If the network request is successful, it returns the response body in String form. Otherwise,
 * it will throw an IOException.
 */
@Throws(IOException::class)
private fun downloadUrl(url: URL): String? {
    var connection: HttpsURLConnection? = null
    return try {
        connection = (url.openConnection() as? HttpsURLConnection)
        connection?.run {
            // Timeout for reading InputStream arbitrarily set to 3000ms.
            readTimeout = 3000
            // Timeout for connection.connect() arbitrarily set to 3000ms.
            connectTimeout = 3000
            // For this use case, set HTTP method to GET.
            requestMethod = "GET"
            // Already true by default but setting just in case; needs to be true since this request
            // is carrying an input (response) body.
            doInput = true
            // Open communications link (network traffic occurs here).
            connect()
            publishProgress(CONNECT_SUCCESS)
            if (responseCode != HttpsURLConnection.HTTP_OK) {
                throw IOException("HTTP error code: $responseCode")
            }
            // Retrieve the response body as an InputStream.
            publishProgress(GET_INPUT_STREAM_SUCCESS, 0)
            inputStream?.let { stream ->
                // Converts Stream to String with max length of 500.
                readStream(stream, 500)
            }
        }
    } finally {
        // Close Stream and disconnect HTTPS connection.
        connection?.inputStream?.close()
        connection?.disconnect()
    }
}

Java

/**
 * Given a URL, sets up a connection and gets the HTTP response body from the server.
 * If the network request is successful, it returns the response body in String form. Otherwise,
 * it will throw an IOException.
 */
private String downloadUrl(URL url) throws IOException {
    InputStream stream = null;
    HttpsURLConnection connection = null;
    String result = null;
    try {
        connection = (HttpsURLConnection) url.openConnection();
        // Timeout for reading InputStream arbitrarily set to 3000ms.
        connection.setReadTimeout(3000);
        // Timeout for connection.connect() arbitrarily set to 3000ms.
        connection.setConnectTimeout(3000);
        // For this use case, set HTTP method to GET.
        connection.setRequestMethod("GET");
        // Already true by default but setting just in case; needs to be true since this request
        // is carrying an input (response) body.
        connection.setDoInput(true);
        // Open communications link (network traffic occurs here).
        connection.connect();
        publishProgress(DownloadCallback.Progress.CONNECT_SUCCESS);
        int responseCode = connection.getResponseCode();
        if (responseCode != HttpsURLConnection.HTTP_OK) {
            throw new IOException("HTTP error code: " + responseCode);
        }
        // Retrieve the response body as an InputStream.
        stream = connection.getInputStream();
        publishProgress(DownloadCallback.Progress.GET_INPUT_STREAM_SUCCESS, 0);
        if (stream != null) {
            // Converts Stream to String with max length of 500.
            result = readStream(stream, 500);
        }
    } finally {
        // Close Stream and disconnect HTTPS connection.
        if (stream != null) {
            stream.close();
        }
        if (connection != null) {
            connection.disconnect();
        }
    }
    return result;
}

Ten en cuenta que el método getResponseCode() muestra el código de estado de la conexión. Esta es una forma útil de obtener información adicional sobre la conexión. Un código de estado de 200 indica que se realizó correctamente.

Para obtener más información sobre HttpsURLConnection, consulta el ejemplo de Android NetworkConnect.

Cómo convertir InputStream en una string

InputStream es una fuente legible de bytes. Una vez que se obtiene un elemento InputStream, es común decodificarlo o convertirlo en un tipo de datos de destino. Por ejemplo, si quieres descargar datos de imágenes, puedes decodificarlos y mostrarlos de esta manera:

Kotlin

val inputStream: InputStream? = null
...
val bitmap: Bitmap = BitmapFactory.decodeStream(inputStream)
findViewById<ImageView>(R.id.image_view)?.apply {
    setImageBitmap(bitmap)
}

Java

InputStream inputStream = null;
...
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
ImageView imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageBitmap(bitmap);

En el ejemplo anterior, InputStream representa el texto del cuerpo de la respuesta. Así es como convertiría el InputStream en una string para que Activity pueda mostrarla en la IU:

Kotlin

/**
 * Converts the contents of an InputStream to a String.
 */
@Throws(IOException::class, UnsupportedEncodingException::class)
fun readStream(stream: InputStream, maxReadSize: Int): String? {
    val reader: Reader? = InputStreamReader(stream, "UTF-8")
    val rawBuffer = CharArray(maxReadSize)
    val buffer = StringBuffer()
    var readSize: Int = reader?.read(rawBuffer) ?: -1
    var maxReadBytes = maxReadSize
    while (readSize != -1 && maxReadBytes > 0) {
        if (readSize > maxReadBytes) {
            readSize = maxReadBytes
        }
        buffer.append(rawBuffer, 0, readSize)
        maxReadBytes -= readSize
        readSize = reader?.read(rawBuffer) ?: -1
    }
    return buffer.toString()
}

Java

/**
 * Converts the contents of an InputStream to a String.
 */
public String readStream(InputStream stream, int maxReadSize)
        throws IOException, UnsupportedEncodingException {
    Reader reader = null;
    reader = new InputStreamReader(stream, "UTF-8");
    char[] rawBuffer = new char[maxReadSize];
    int readSize;
    StringBuffer buffer = new StringBuffer();
    while (((readSize = reader.read(rawBuffer)) != -1) && maxReadSize > 0) {
        if (readSize > maxReadSize) {
            readSize = maxReadSize;
        }
        buffer.append(rawBuffer, 0, readSize);
        maxReadSize -= readSize;
    }
    return buffer.toString();
}

Hasta ahora, la secuencia de eventos en el código es la siguiente:

  1. Activity inicia un NetworkFragment y transfiere una URL especificada.
  2. Cuando una acción del usuario activa el método downloadData() de Activity, NetworkFragment ejecuta DownloadTask.
  3. El método AsyncTask de onPreExecute() se ejecuta primero (en el procesamiento de IU) y cancela la tarea si el dispositivo no está conectado a Internet.
  4. El método AsyncTask de doInBackground() se ejecuta en un subproceso en segundo plano e invoca al método downloadUrl().
  5. El método downloadUrl() toma una string de URL como parámetro y utiliza un objeto HttpsURLConnection para obtener el contenido web como InputStream.
  6. InputStream se transfiere al método readStream(), que convierte la transmisión en una string.
  7. Finalmente, una vez completado el trabajo en segundo plano, el método AsyncTask de onPostExecute() se ejecuta en el procesamiento de IU y utiliza DownloadCallback para enviar el resultado de vuelta a la IU como una String.

Cómo sobrevivir a los cambios de configuración

Hasta ahora, has implementado con éxito un elemento Activity que realiza una operación de red. Pero, si el usuario decide cambiar la configuración del dispositivo (es decir, girar la pantalla 90°) mientras doInBackground() se está ejecutando en el subproceso en segundo plano, el elemento Activity se destruye y se recrea a sí mismo, logrando que vuelva a ejecutarse onCreate() y haciendo referencia a un nuevo NetworkFragment (consulta la Guía de cambios durante el tiempo de ejecución). Por lo tanto, el AsyncTask del NetworkFragment original tendrá un DownloadCallback que hará referencia al elemento Activity original que ya no puede actualizar la IU. De este modo, se habrá desperdiciado el trabajo en red realizado en un subproceso en segundo plano.

Para persistir a través de estos cambios de configuración, necesitas conservar el elemento Fragment original y asegurarte de que el objeto Activity reconstruido haga referencia a él. Para ello, debes hacer las siguientes modificaciones a tu código:

Primero, NetworkFragment debería llamar a setRetainInstance(true) en su método onCreate():

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Retain this Fragment across configuration changes in the host Activity.
    retainInstance = true
}

Java

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    // Retain this Fragment across configuration changes in the host Activity.
    setRetainInstance(true);
}

Luego, modifica cómo inicializar NetworkFragment en tu método estático getInstance():

Kotlin

companion object {
    fun getInstance(fragmentManager: FragmentManager, url: String): NetworkFragment {
        // Recover NetworkFragment in case we are re-creating the Activity due to a config change.
        // This is necessary because NetworkFragment might have a task that began running before
        // the config change occurred and has not finished yet.
        // The NetworkFragment is recoverable because it calls setRetainInstance(true).
        var networkFragment = fragmentManager.findFragmentByTag(TAG) as? NetworkFragment
        if (networkFragment == null) {
            networkFragment = NetworkFragment()
            networkFragment.arguments = Bundle().apply {
                putString(URL_KEY, url)
            }
            fragmentManager.beginTransaction()
                    .add(networkFragment, TAG)
                    .commit()
        }
        return networkFragment
    }
}

Java

public static NetworkFragment getInstance(FragmentManager fragmentManager, String url) {
    // Recover NetworkFragment in case we are re-creating the Activity due to a config change.
    // This is necessary because NetworkFragment might have a task that began running before
    // the config change occurred and has not finished yet.
    // The NetworkFragment is recoverable because it calls setRetainInstance(true).
    NetworkFragment networkFragment = (NetworkFragment) fragmentManager
            .findFragmentByTag(NetworkFragment.TAG);
    if (networkFragment == null) {
        networkFragment = new NetworkFragment();
        Bundle args = new Bundle();
        args.putString(URL_KEY, url);
        networkFragment.setArguments(args);
        fragmentManager.beginTransaction().add(networkFragment, TAG).commit();
    }
    return networkFragment;
}

Tu app ahora puede extraer datos de Internet de forma exitosa.

Ten en cuenta que existen otras herramientas de administración de subprocesos en segundo plano que pueden ayudarte a lograr este mismo objetivo. A medida que tu app se vuelva más compleja, es posible que descubras que estas otras herramientas se adaptan mejor a ella. En lugar de AsyncTask, las opciones que vale la pena investigar son IntentService y AsyncTaskLoader.

Para obtener más información sobre este tema, consulta las siguientes guías relacionadas: