AsyncTask deprecated, e agora?

AsyncTask deprecated, e agora?
Alex Felipe
Alex Felipe

Compartilhe

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.

Imersão dev Back-end: mergulhe em programação hoje, com a Alura e o Google Gemini. Domine o desenvolvimento back-end e crie o seu primeiro projeto com Node.js na prática. O evento é 100% gratuito e com certificado de participação. O período de inscrição vai de 18 de novembro de 2024 a 22 de novembro de 2024. Inscreva-se já!

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 um CoroutineContext que define onde a Coroutine será executada, no caso do Dispatchers.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 uma CoroutineScope, 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 thread IO para uma Main 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.

Alex Felipe
Alex Felipe

Alex é instrutor e desenvolvedor e possui experiência em Java, Kotlin, Android. Atualmente cria conteúdo no canal https://www.youtube.com/@AlexFelipeDev.

Veja outros artigos sobre Mobile