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 interfazBlockingQueue
. 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 deThreadPoolExecutor
.
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.