
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)
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()
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 {
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)
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)
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) { }.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) { } }
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 { } } fun doWork2() { scope.launch { } } fun cancelAllWork() { job.cancel() } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1()
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 { }
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) {
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) {
Evite usar o escopo global
Se você usa o GlobalScope
em qualquer lugar do seu aplicativo Android, deve parar de fazer isso.
GlobalScope.launch {
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 {