Patrones de corutina y antipatrones en Kotlin

Patrones de corutina y antipatrones en Kotlin


Decid铆 escribir sobre algunas cosas que, en mi opini贸n, son y no deben evitarse al usar la rutina de Kotlin.


Envuelva llamadas asincr贸nicas en coroutineScope o use SupervisorJob para manejar excepciones


Si puede ocurrir una excepci贸n en el bloque async , no conf铆e en el try/catch .


 val job: Job = Job() val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = scope.async { ... } // (1) fun loadData() = scope.launch { try { doWork().await() // (2) } catch (e: Exception) { ... } } 

En el ejemplo anterior, la funci贸n doWork inicia una nueva corutina (1), que puede generar una excepci贸n no controlada. Si intenta envolver doWork con un try/catch (2), la aplicaci贸n seguir谩 fallando.


Esto se debe a que la falla de cualquier componente secundario del trabajo lleva a la falla inmediata de su padre.


Una forma de evitar el error es usar SupervisorJob (1).


La falla o cancelaci贸n de la ejecuci贸n del componente hijo no conducir谩 a la falla del padre y no afectar谩 a otros componentes.

 val job = SupervisorJob() // (1) val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = scope.async { ... } fun loadData() = scope.launch { try { doWork().await() } catch (e: Exception) { ... } } 

Nota : esto solo funcionar谩 si inicia expl铆citamente su llamada de rutina asincr贸nica con SupervisorJob . Por lo tanto, el c贸digo a continuaci贸n seguir谩 bloqueando su aplicaci贸n, porque la sincronizaci贸n async ejecuta como parte de la rutina principal (1).


 val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun loadData() = scope.launch { try { async { // (1) // may throw Exception }.await() } catch (e: Exception) { ... } } 

Otra forma de evitar un bloqueo, que es m谩s preferible, es envolver async en coroutineScope (1). Ahora, cuando ocurre una excepci贸n dentro de async , cancela todas las dem谩s corutinas creadas en esta 谩rea, sin tocar el 谩rea externa. (2)


 val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = coroutineScope { // (1) async { ... } } fun loadData() = scope.launch { // (2) try { doWork().await() } catch (e: Exception) { ... } } 

Adem谩s, puede manejar excepciones dentro del bloque async .


Use el administrador principal para las corutinas ra铆z


Si necesita hacer un trabajo de fondo y actualizar la interfaz de usuario dentro de su rutina de ra铆z, comience usando el despachador principal.


 val scope = CoroutineScope(Dispatchers.Default) // (1) fun login() = scope.launch { withContext(Dispatcher.Main) { view.showLoading() } // (2) networkClient.login(...) withContext(Dispatcher.Main) { view.hideLoading() } // (2) } 

En el ejemplo anterior, comenzamos la rutina de la ra铆z utilizando el despachador CoroutineScope en CoroutineScope (1). Con este enfoque, cada vez que necesitemos actualizar la interfaz de usuario, tendremos que cambiar el contexto (2).


En la mayor铆a de los casos, es preferible crear CoroutineScope inmediatamente con el despachador principal, lo que conducir谩 a la simplificaci贸n del c贸digo y al cambio de contexto menos expl铆cito.


 val scope = CoroutineScope(Dispatchers.Main) fun login() = scope.launch { view.showLoading() withContext(Dispatcher.IO) { networkClient.login(...) } view.hideLoading() } 

Evite usar as铆ncrono / espera innecesario


Si utiliza la funci贸n async e inmediatamente la llamada en await , debe dejar de hacerlo.


 launch { val data = async(Dispatchers.Default) { /* code */ }.await() } 

Si desea cambiar el contexto de la rutina y suspender inmediatamente la rutina principal, entonces withContext es la forma m谩s preferible de withContext .


 launch { val data = withContext(Dispatchers.Default) { /* code */ } } 

En t茅rminos de rendimiento, este no es un problema tan grande (incluso teniendo en cuenta que async crea una nueva rutina para el trabajo), pero sem谩nticamente async implica que desea ejecutar varias rutinas en segundo plano y solo esperarlas.


Evitar la cancelaci贸n del trabajo


Si necesita cancelar la rutina, no cancele el trabajo.


 class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1() { scope.launch { /* do work */ } } fun doWork2() { scope.launch { /* do work */ } } fun cancelAllWork() { job.cancel() } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() // (1) } 

El problema con el c贸digo anterior es que cuando cancelamos el trabajo, lo ponemos en un estado completado . Las rutinas lanzadas como parte de un trabajo completado no se ejecutar谩n (1).


Si desea deshacer todas las corutinas en un 谩rea espec铆fica, puede usar la funci贸n cancelChildren . Adem谩s, es una buena pr谩ctica proporcionar la capacidad de cancelar trabajos individuales (2).


 class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1(): Job = scope.launch { /* do work */ } // (2) fun doWork2(): Job = scope.launch { /* do work */ } // (2) fun cancelAllWork() { scope.coroutineContext.cancelChildren() // (1) } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() } 

Evite escribir la funci贸n de pausa usando el despachador impl铆cito


No escriba la funci贸n de suspend , cuya ejecuci贸n depender谩 del administrador de rutina particular.


 suspend fun login(): Result { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result } 

En el ejemplo anterior, la funci贸n de inicio de sesi贸n es una funci贸n de suspensi贸n y fallar谩 si la inicia desde una rutina que el despachador principal no utilizar谩.


 launch(Dispatcher.Main) { // (1)    val loginResult = login() ... } launch(Dispatcher.Default) { // (2)   val loginResult = login() ... } 

CalledFromWrongThreadException: solo el hilo fuente que cre贸 la jerarqu铆a de los componentes de Vista puede acceder a ellos.

Cree su funci贸n de suspensi贸n para que pueda ejecutarse desde cualquier administrador de rutina.


 suspend fun login(): Result = withContext(Dispatcher.Main) { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result } 

Ahora podemos llamar a nuestra funci贸n de inicio de sesi贸n desde cualquier despachador.


 launch(Dispatcher.Main) { // (1) no crash val loginResult = login() ... } launch(Dispatcher.Default) { // (2) no crash ether val loginResult = login() ... } 

Evitar el uso de alcance global


Si usa GlobalScope en todas partes en su aplicaci贸n de Android, debe dejar de hacerlo.


 GlobalScope.launch { // code } 

El alcance global se utiliza para lanzar corutinas de nivel superior que se ejecutan durante toda la vida de la aplicaci贸n y no se cancelan con anticipaci贸n.

El c贸digo de la aplicaci贸n generalmente debe usar el CoroutineScope espec铆fico de la aplicaci贸n, por lo que se desaconseja el uso as铆ncrono o el lanzamiento en GlobalScope .

En Android, la rutina puede limitarse f谩cilmente al ciclo de vida de una actividad, fragmento, vista o modelo de vista.


 class MainActivity : AppCompatActivity(), CoroutineScope { private val job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onDestroy() { super.onDestroy() coroutineContext.cancelChildren() } fun loadData() = launch { // code } } 

Source: https://habr.com/ru/post/es432942/


All Articles