Padrões e antipadrões de Corutin em Kotlin

Padrões e antipadrões de Corutin em Kotlin


Decidi escrever sobre algumas coisas que, na minha opinião, são e não devem ser evitadas ao usar a corotina Kotlin.


Quebra de chamadas assíncronas em coroutineScope ou use SupervisorJob para manipular exceções


Se uma exceção pode ocorrer no bloco async , não confie no 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) { ... } } 

No exemplo acima, a função doWork inicia uma nova corotina (1), que pode gerar uma exceção não tratada. Se você tentar doWork com um try/catch (2), o aplicativo ainda falhará.


Isso ocorre porque a falha de qualquer componente filho do trabalho leva à falha imediata de seus pais.


Uma maneira de evitar o erro é usar SupervisorJob (1).


A falha ou cancelamento da execução do componente filho não levará à falha do pai e não afetará outros 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 : isso só funcionará se você iniciar explicitamente sua chamada de rotina assíncrona com SupervisorJob . Portanto, o código abaixo ainda travará seu aplicativo, porque o async é executado como parte da rotina 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) { ... } } 

Outra maneira de evitar uma falha, que é mais preferível, é coroutineScope async no coroutineScope (1). Agora, quando ocorre uma exceção dentro da async , ela cancela todas as outras corotinas criadas nessa área, sem tocar na á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) { ... } } 

Além disso, você pode manipular exceções dentro do bloco async .


Use o gerenciador principal para rotinas principais


Se você precisar fazer um trabalho em segundo plano e atualizar a interface do usuário dentro da sua rotina principal, inicie-a usando o expedidor 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) } 

No exemplo acima, iniciamos a corotina raiz usando o despachante CoroutineScope no CoroutineScope (1). Com essa abordagem, toda vez que precisarmos atualizar a interface do usuário, teremos que mudar de contexto (2).


Na maioria dos casos, é preferível criar o CoroutineScope imediatamente com o expedidor principal, o que levará à simplificação do código e à troca de contexto menos explícita.


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

Evite usar assíncrono / espera desnecessário


Se você usar a função async e chamar imediatamente em await , deverá parar de fazer isso.


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

Se você deseja alternar o contexto da corotina e suspender imediatamente a corotina pai, com withContext é a maneira mais preferível para isso.


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

Em termos de desempenho, esse não é um problema tão grande (mesmo considerando que o async cria uma nova corotina para o trabalho), mas o async semanticamente implica que você deseja executar várias corotinas em segundo plano e esperar apenas por elas.


Evitar cancelamento de trabalho


Se você precisar cancelar a rotina, não cancele o trabalho.


 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) } 

O problema com o código acima é que, quando cancelamos o trabalho, o colocamos em um estado concluído . As corotinas lançadas como parte de um trabalho concluído não serão executadas (1).


Se você quiser desfazer todas as corotinas em uma área específica, poderá usar a função cancelChildren . Além disso, é uma boa prática fornecer a capacidade de cancelar trabalhos individuais (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 gravar a função de pausa usando o despachante implícito


Não escreva a função de suspend , cuja execução dependerá do gerenciador de corotina específico.


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

No exemplo acima, a função de login é uma função de suspensão e falhará se você a iniciar a partir de uma rotina que o expedidor principal não utilizará.


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

CalledFromWrongThreadException: somente o encadeamento de origem que criou a hierarquia dos componentes do View pode acessá-los.

Crie sua função de suspensão para que possa ser executada a partir de qualquer gerenciador de rotinas.


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

Agora podemos chamar nossa função de login de qualquer despachante.


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

Evite usar o escopo global


Se você usa o GlobalScope em qualquer lugar do seu aplicativo Android, deve parar de fazer isso.


 GlobalScope.launch { // code } 

O escopo global é usado para iniciar corotinas de nível superior que são executadas ao longo da vida útil do aplicativo e não são canceladas antes do tempo.

O código do aplicativo geralmente deve usar o CoroutineScope específico do aplicativo, portanto, o uso assíncrono ou de inicialização no GlobalScope é altamente desencorajado.

No Android, a corotina pode ser facilmente limitada ao ciclo de vida de uma Atividade, Fragmento, Visualização ou ViewModel.


 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/pt432942/


All Articles