Cómo ejecutar tareas de Android en subprocesos en segundo plano

Todas las apps para Android usan un subproceso principal para controlar las operaciones de la IU. Llamar a operaciones de larga duración desde este subproceso principal puede generar bloqueos y faltas de respuesta. Por ejemplo, si tu app realiza una solicitud de red desde el subproceso principal, la IU de tu app se inmoviliza hasta que recibe la respuesta de la red. Puedes crear subprocesos en segundo plano adicionales para controlar operaciones de larga duración mientras el subproceso principal continúa controlando las actualizaciones de la IU.

En esta guía, se muestra a los desarrolladores de Kotlin y del lenguaje de programación Java cómo usar un conjunto de subprocesos para configurar y usar varios subprocesos en una app para Android. Además, se muestra cómo definir el código para ejecutar en un subproceso y cómo establecer la comunicación entre uno de estos subprocesos y el subproceso principal.

Resumen de ejemplos

Según la Guía de arquitectura de apps, en los ejemplos de este tema se realiza una solicitud de red y se muestra el resultado en el subproceso principal de modo que la app pueda mostrar ese resultado en la pantalla.

Específicamente, el ViewModel llama a la capa del repositorio del subproceso principal para activar la solicitud de red. La capa del repositorio se encarga de quitar la ejecución de la solicitud de red del subproceso principal y enviar el resultado al subproceso principal mediante una devolución de llamada.

Para quitar la ejecución de la solicitud de red del subproceso principal, debemos crear otros subprocesos en nuestra app.

Cómo crear varios subprocesos

Un conjunto de subprocesos es una colección administrada de subprocesos que ejecuta tareas en paralelo desde una cola. Se ejecutan las tareas nuevas en los subprocesos existentes a medida que quedan inactivas. Para enviar una tarea a un conjunto de subprocesos, usa la interfaz ExecutorService. Ten en cuenta que ExecutorService no tiene nada que ver con Servicios, el componente de la aplicación de Android.

Crear subprocesos es costoso, por lo que debes crear un conjunto de subprocesos solamente una vez cuando se inicializa tu app. Asegúrate de guardar la instancia de ExecutorService en tu clase Application o en un contenedor de inyección de dependencias. En el siguiente ejemplo, se crea un conjunto de subprocesos que podemos usar para ejecutar tareas en segundo plano.

Kotlin

class MyApplication : Application() {
    val executorService: ExecutorService = Executors.newFixedThreadPool(4)
}

Java

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

Hay otras formas de configurar un conjunto de subprocesos según la carga de trabajo esperada. Consulta Cómo configurar un conjunto de subprocesos para obtener más información.

Cómo ejecutar en un subproceso en segundo plano

Cuando haces una solicitud de red en el subproceso principal, este espera o se bloquea hasta que recibe una respuesta. Como el subproceso está bloqueado, el SO no puede llamar a onDraw() y tu app se detiene, lo que podría generar un diálogo Aplicación no responde (ANR). Para evitar esto, ejecutemos esta operación en un subproceso en segundo plano.

Primero, veamos nuestra clase Repository y cómo realiza la solicitud de red:

Kotlin

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; charset=utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())

            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

Java

// Result.java
public abstract class Result<T> {
    private Result() {}

    public static final class Success<T> extends Result<T> {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Error<T> extends Result<T> {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Result<LoginResponse> makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Success<LoginResponse>(loginResponse);
        } catch (Exception e) {
            return new Result.Error<LoginResponse>(e);
        }
    }
}

La clase makeLoginRequest() es síncrona y bloquea el subproceso de llamada. Para modelar la respuesta de la solicitud de red, tenemos nuestra propia clase Result.

ViewModel activa la solicitud de red cuando el usuario presiona, por ejemplo, un botón:

Kotlin

class LoginViewModel(
    private val loginRepository: LoginRepository
) {
    fun makeLoginRequest(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

Java

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

Con el código anterior, LoginViewModel bloquea el subproceso principal cuando realiza la solicitud de red. Podemos usar el conjunto de subprocesos del que creamos una instancia para mover la ejecución a un subproceso en segundo plano. Primero, según los principios de inyección de dependencias, LoginRepository toma una instancia de Executor, en lugar de ExecutorService, porque ejecuta código y no administra subprocesos:

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) { ... }

Java

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

El método execute() del ejecutor toma una interfaz Runnable. Runnable es una interfaz de método único (SAM) con un método run() que se ejecuta en un subproceso cuando se lo invoca.

Ahora, creemos otra función llamada makeLoginRequest() que pase la ejecución al subproceso en segundo plano y, por el momento, ignore la respuesta:

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) {

    fun makeLoginRequest(jsonBody: String) {
        executor.execute {
            val ignoredResponse = makeSynchronousLoginRequest(url, jsonBody)
        }
    }

    private fun makeSynchronousLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        ... // HttpURLConnection logic
    }
}

Java

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

Dentro del método execute(), creamos una interfaz Runnable nueva con el bloque de código que queremos ejecutar en el subproceso en segundo plano; en nuestro caso, el método de solicitud de red síncrona. De forma interna, el ExecutorService administra la interfaz Runnable y la ejecuta en un subproceso disponible.

Consideraciones

Cualquier subproceso de tu app puede ejecutarse en paralelo con otros subprocesos, incluido el subproceso principal, por lo que debes asegurarte de que tu código sea seguro para los subprocesos. Ten en cuenta que, en nuestro ejemplo, evitamos escribir en variables compartidas entre subprocesos y, en cambio, pasamos datos inmutables. Esta es una buena práctica, ya que cada subproceso funciona con su propia instancia de datos y evitamos la complejidad de la sincronización.

Si necesitas compartir el estado entre subprocesos, debes tener cuidado y administrar el acceso desde los subprocesos mediante mecanismos de sincronización, como bloqueos, lo que está fuera del alcance de esta guía. Siempre que sea posible, debes evitar compartir el estado mutable entre subprocesos.

Cómo establecer la comunicación con el subproceso principal

En el paso anterior, ignoramos la respuesta a la solicitud de red. Para mostrar el resultado en la pantalla, el LoginViewModel debe conocerlo. Para ello, podemos usar devoluciones de llamada.

La función makeLoginRequest() debería tomar una devolución de llamada como parámetro para mostrar un valor de forma asíncrona. Se llama a la devolución de llamada con el resultado cada vez que se completa la solicitud de red o se produce una falla. En Kotlin, podemos usar una función de orden superior. Sin embargo, en Java, tenemos que crear una nueva interfaz de devolución de llamada para obtener la misma función:

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) {

    fun makeLoginRequest(
        jsonBody: String,
        callback: (Result<LoginResponse>) -> Unit
    ) {
        executor.execute {
            try {
                val response = makeSynchronousLoginRequest(jsonBody)
                callback(response)
            } catch (e: Exception) {
                val errorResult = Result.Error(e)
                callback(errorResult)
            }
        }
    }
    ...
}

Java

interface RepositoryCallback<T> {
    void onComplete(Result<T> result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

Ahora, el ViewModel debe implementar la devolución de llamada. Puede usar una lógica diferente según el resultado:

Kotlin

class LoginViewModel(
    private val loginRepository: LoginRepository
) {
    fun makeLoginRequest(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody) { result ->
            when(result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

Java

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
            @Override
            public void onComplete(Result<LoginResponse> result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

En este ejemplo, se ejecuta la devolución de llamada en el subproceso de llamada, que es un subproceso en segundo plano. Por lo tanto, no puedes modificar la capa de IU ni comunicarte directamente con ella hasta que vuelvas al subproceso principal.

Cómo usar controladores

Puedes usar un Handler para poner en cola una acción que se ejecutará en un subproceso diferente. Para especificar el subproceso en el que se ejecutará la acción, crea el Handler usando un Looper para el subproceso. Un Looper es un objeto que ejecuta el bucle de mensajes para un subproceso asociado. Una vez que hayas creado un Handler, puedes usar el método post(Runnable) para ejecutar un bloque de código en el subproceso correspondiente.

Looper incluye una función auxiliar, getMainLooper(), que recupera el objeto Looper del subproceso principal. Puedes ejecutar código en el subproceso principal con este Looper para crear un Handler. Como es algo que puedes hacer con bastante frecuencia, también puedes guardar una instancia del Handler en el mismo lugar en que guardaste el ExecutorService:

Kotlin

class MyApplication : Application() {
    val executorService: ExecutorService = Executors.newFixedThreadPool(4)
    val mainThreadHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
}

Java

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

Se recomienda insertar el controlador en el Repository, ya que brinda más flexibilidad. Por ejemplo, en el futuro, es posible que quieras pasar un Handler diferente para programar tareas en un subproceso separado. Si siempre te comunicas con el mismo subproceso, puedes pasar el Handler al constructor del Repository, como se muestra en el siguiente ejemplo.

Kotlin

class LoginRepository(
    ...
    private val resultHandler: Handler
) {

    fun makeLoginRequest(
        jsonBody: String,
        callback: (Result<LoginResponse>) -> Unit
    ) {
          executor.execute {
              try {
                  val response = makeSynchronousLoginRequest(jsonBody)
                  resultHandler.post { callback(response) }
              } catch (e: Exception) {
                  val errorResult = Result.Error(e)
                  resultHandler.post { callback(errorResult) }
              }
          }
    }
    ...
}

Java

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

Como alternativa, si buscas mayor flexibilidad, puedes pasar un controlador a cada función:

Kotlin

class LoginRepository(...) {
    ...
    fun makeLoginRequest(
        jsonBody: String,
        resultHandler: Handler,
        callback: (Result<LoginResponse>) -> Unit
    ) {
        executor.execute {
            try {
                val response = makeSynchronousLoginRequest(jsonBody)
                resultHandler.post { callback(response) }
            } catch (e: Exception) {
                val errorResult = Result.Error(e)
                resultHandler.post { callback(errorResult) }
            }
        }
    }
}

Java

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

En este ejemplo, la devolución de llamada que se pasó a la llamada makeLoginRequest del repositorio se ejecuta en el subproceso principal. De esta forma, puedes modificar directamente la IU desde la devolución de llamada o usar LiveData.setValue() para comunicarte con la IU.

Cómo configurar un conjunto de subprocesos

Puedes crear un conjunto de subprocesos usando una de las funciones de ayuda Executor con configuraciones predefinidas, como se muestra en el código de ejemplo anterior. Como alternativa, si deseas personalizar los detalles del conjunto de subprocesos, puedes crear una instancia usando directamente ThreadPoolExecutor. Puedes configurar los siguientes detalles:

  • Tamaño inicial y máximo del conjunto.
  • Tiempo de mantenimiento de conexión y unidad de tiempo. El tiempo de mantenimiento de actividad es la duración máxima que un subproceso puede permanecer inactivo antes de cerrarse.
  • Una cola de entrada que conserve tareas del objeto Runnable. Esta cola debe implementar la interfaz BlockingQueue. Para cumplir con los requisitos de tu app, puedes elegir entre las implementaciones de cola disponibles. Si quieres obtener más información sobre estas, consulta la descripción general de la clase de ThreadPoolExecutor.

Este es un ejemplo en el que se especifica el tamaño del conjunto de subprocesos en función de la cantidad total de núcleos de procesador, un tiempo de mantenimiento de actividad de un segundo y una cola de entrada.

Kotlin

class MyApplication : Application() {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors()

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private val workQueue: BlockingQueue<Runnable> =
            LinkedBlockingQueue<Runnable>()

    // Sets the amount of time an idle thread waits before terminating
    private const val KEEP_ALIVE_TIME = 1L
    // Sets the Time Unit to seconds
    private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS
    // Creates a thread pool manager
    private val threadPoolExecutor: ThreadPoolExecutor = ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    )
}

Java

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}

Bibliotecas de simultaneidad

Es importante comprender los conceptos básicos de los subprocesos y sus mecanismos subyacentes. Sin embargo, hay muchas bibliotecas populares que ofrecen abstracciones de nivel superior, en comparación con estos conceptos, y servicios listos para usar a fin de pasar datos entre subprocesos. Entre estas bibliotecas, se incluyen Guava y RxJava para los usuarios del lenguaje de programación Java, además de las corrutinas, que recomendamos para los usuarios de Kotlin.

En la práctica, debes elegir la que funcione mejor para tu app y tu equipo de desarrollo, aunque las reglas de los subprocesos son las mismas.

Consulta también

Para obtener más información acerca de los procesos y subprocesos en Android, consulta Descripción general de procesos y subprocesos.