AsyncTask deprecated, e agora?
A AsyncTask ficou deprecated a partir da API 30, o Android 11. Como alternativa, a documentação sugere o uso do pacote java.util.concurrent ou Coroutines do Kotlin... Considerando esse detalhe, neste artigo, vamos aprender a converter uma implementação com AsyncTask
utilizando Coroutines do Kotlin.
Projeto de demonstração com AsyncTask
Como demonstração, vamos utilizar o projeto Todo, um App que lista e cria tarefas. Em sua implementação, foi utilizado o Room para armazenar as informações. Por padrão, as operações do Room precisam ser feitas em uma thread diferente da UI, portanto, a listagem e inserção de tarefas foram feitas com AsyncTask
.
Em outras palavras, o nosso objetivo é avaliar as duas amostras e converter para o uso de Coroutines.
Adicionando Coroutines no projeto Android
Para instalar a Coroutine no projeto Android, precisamos adicionar a dependência:
dependencies {
// dependências
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}
Então é só sincronizar que temos acesso às Coroutines. Com tudo configurado, podemos começar a conversão do nosso primeiro exemplo, a inserção de notas.
Convertendo a task de salvar nota
No código de inserção temos a seguinte AsyncTask
:
class SalvaNotaTask(
private val dao: NotaDao,
private val nota: Nota,
val quandoNotaSalva: () -> Unit,
) : AsyncTask<Unit, Unit, Unit>() {
override fun doInBackground(vararg p0: Unit?) {
dao.salva(nota)
}
override fun onPostExecute(result: Unit?) {
quandoNotaSalva()
}
}
Então, temos a seguinte chamada dentro do FormularioNotaFragment
que permite salvar a nota:
class FormularioNotaFragment : Fragment() {
// restante do código
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val botao = binding.formularioNotaBotaoSalvarFragment
botao.setOnClickListener {
salvaNota()
}
}
private fun salvaNota() {
val campoTitulo = binding.formularioNotaTituloFragment
val titulo = campoTitulo.text.toString()
val campoDescricao = binding.formularioNotaDescricaoFragment
val descricao = campoDescricao.text.toString()
val nota = Nota(
titulo = titulo,
descricao = descricao
)
SalvaNotaTask(dao, nota) {
activity?.onBackPressed()
}.execute()
}
}
Basicamente, ao clicar no botão salvar, chamamos o salvaNota()
que cria uma Nota
e executa a SalvaNotaTask()
enviando o NotaDao
e a Nota
criada. Ao finalizar, a expressão lambda é executada e chama o activity?.onBackPressed()
que volta para a lista de notas.
Com Coroutines, executamos a mesma tarefa com o seguinte código:
private fun salvaNota() {
// restante do código
CoroutineScope(Dispatchers.IO).launch {
dao.salva(nota)
withContext(Dispatchers.Main) {
activity?.onBackPressed()
}
}
}
Observe que não precisamos criar uma classe nova! Além disso, não há a necessidade de implementar métodos de callbacks para separar a execução assíncrona da thread principal.
Porém, quando escrevemos um código mais enxuto que faz a mesma coisa de um código maior, é natural surgir dúvidas, como por exemplo, o que é o CoroutineScope
, Dispatchers.IO
e etc...
De uma maneira resumida, ambos significam o seguinte:
CoroutinesScope
: determina o escopo de execução da coroutine. Ao criar uma instância, somos obrigados a determinar umCoroutineContext
que define onde a Coroutine será executada, no caso doDispatchers.IO
, indicamos que é uma execução que precisa ser feita em paralela à thread principal, ou seja, destinada a execuções como leitura e escrita, requisições e etc.launch
: função de extensão de umaCoroutineScope
, a partir dela executamos a coroutine sem bloquear a thread principal.withContext
: suspend function que permite trocar o escopo de uma Coroutine. Seguindo o nosso exemplo, trocamos de uma threadIO
para umaMain
que representa a execução da UI Thread.Dispartchers
: um grupo de implementações de CoroutinesDispatchers. A partir dele temos acesso a implementações comuns, como a execução em threads destinadas a IO ou a main thread.
Suspend functions, são funções que podem ser chamadas apenas por Coroutines ou por outras funções de suspensão. Caso tenha interesse em saber mais detalhes sobre Coroutines, fique à vontade em consultar a documentação.
Para deixar mais clara toda teoria, podemos visualizar a execução do nosso código com os seguintes comentários:
CoroutineScope(Dispatchers.IO).launch {
// executa paralelo à UI thread
dao.salva(nota)
withContext(Dispatchers.Main) {
// executa na UI thread
activity?.onBackPressed()
}
}
Apenas com esse exemplo, podemos aplicar a mesma técnica para buscar as notas!
Convertendo a task de busca de notas
Na busca com AsyncTask
temos o seguinte código:
class BuscaNotasTask(
private val dao: NotaDao,
private val quandoNotasEncontradas: (notas: List<Nota>) -> Unit
) : AsyncTask<Unit, Unit, List<Nota>>() {
override fun doInBackground(vararg p0: Unit?): List<Nota> {
return dao.buscaTodas()
}
override fun onPostExecute(resultado: List<Nota>?) {
resultado?.let { notas ->
quandoNotasEncontradas(notas)
}
}
}
Então na ListaNotasActivity
chamamos da seguinte maneira:
class ListaNotasActivity : AppCompatActivity() {
// restante do código
override fun onResume() {
super.onResume()
BuscaNotasTask(dao) { notas ->
adapter.atualiza(notas)
}.execute()
}
}
Com Coroutines, seguindo a mesma técnica, temos o seguinte código:
override fun onResume() {
super.onResume()
CoroutineScope(IO).launch {
val notas = dao.buscaTodas()
withContext(Main){
adapter.atualiza(notas)
}
}
}
Observe que o resultado é bastante similar ao nosso primeiro exemplo, a diferença é que na busca de notas esperamos o retorno da função buscaTodas()
.
Um detalhe que podemos ressaltar, é que Coroutines permite a escrita de código assíncrono de maneira sequencial, ou seja, sem a necessidade de callbacks!
Escopo de lifecycle owner
Além de criar o nosso próprio escopo, no ambiente Android, temos acesso aos escopos de lifecycle owner, como Activities ou Fragments. Isso significa que as Coroutines executadas com esses escopos são sincronizadas com o ciclo de vida do componente.
Em outras palavras, a execução da Coroutine nesse escopo é cancelada automaticamente se o ciclo de vida chegar no estado de destruição!
Para uma demonstração do comportamento, podemos executar o código com alguns logs:
override fun onResume() {
super.onResume()
lifecycleScope.launch(IO) {
Log.i(TAG, "onResume: buscando notas")
val notas = dao.buscaTodas()
withContext(Main){
adapter.atualiza(notas)
}
Log.i(TAG, "onResume: buscando finalizada")
}
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy: activity destruída")
}
Observe que podemos definir também o CoroutinesContext na função
launch
.
Ao abrir e fechar o App, não somos capazes de notar a sincronia mencionada, pois é uma execução rápida! Para que isso seja possível durante o teste, podemos aplicar um atraso de execução com a suspend function delay()
:
lifecycleScope.launch(IO) {
Log.i(TAG, "onResume: buscando notas")
delay(5000)
val notas = dao.buscaTodas()
withContext(Main){
adapter.atualiza(notas)
}
Log.i(TAG, "onResume: buscando finalizada")
}
Fazendo a mesma simulação de abrir e fechar o App, temos o seguinte resultado:
I/ListaNotasActivity: onResume: buscando notas
I/ListaNotasActivity: onDestroy: activity destruída
Veja que a busca não é finalizada! Dessa forma, concluímos a existência da sincroniza do escopo da Coroutine de lifecycle owner com o ciclo de vida do componente.
Conclusão
Mesmo que a AsyncTask
esteja obsoleta, a Coroutine é uma ótima alternativa. Além do que vimos, as Coroutines estão cada vez mais comuns dentro do ambiente Android, seja como alternativa para AsyncTask
, como também, no uso de muitas bibliotecas famosas do Android que estão aderindo ao uso, como por exemplo, as bibliotecas do Jetpack entre outras.