0

Avançando nas Coroutines e Dispatchers no Android com Kotlin

#Kotlin #Android
Francisco Rasia
Francisco Rasia

Um uma publicação recente nós vimos os conceitos mais essenciais sobre coroutines no Android. Aprendemos como, por meio de alguns conceitos muito simples como suspend functions e os escopos de execução podemos lidar com tarefas assíncronas sem nos preocuparmos com a complexidade de gerenciar múltiplas threads e callbacks e demos os primeiros passos para enfrentar o terrível Monstro ANR 👿.

Agora nós vamos nos embrenhar nas matas densas da Floresta das Threads Perdidas e encarar os seus perigos. Então, caro leitor ou leitora, vista seu colete de Mithril, tenha em mão suas poções, lance o feitiço de proteção arcana +10 e me siga nessa nova jornada.


🔒 Criando funções main-safe a partir de código blocking

Nem sempre é possível converter uma database para o uso de corrotinas diretamente; a biblioteca OkHttp3, por exemplo, emprega funções que bloqueiam a thread durante a execução.

Entretanto, é possível tornar as chamadas à API main-safe, ou seja, seguras para chamada a partir da thread principal, por meio da alternância entre Dispatchers, que é a entidade que determina em que contexto uma corrotina será executada. Pense num Dispatcher como um sargento do exército élfico: é ele que vai direcionar cada tarefa para a fila correta.

No Android são comumente utilizados três Dispatchers:

  • Dispatchers.Main: lida com as tarefas da UI e da thread principal;
  • Dispatchers.IO: responsável por lidar com os acessos ao armazenamento interno e à rede;
  • Dispatchers.Default: gerencia as operações com uso intensivo de CPU;

Podemos alternar entre Dispatchers por meio da função withContext(). Ao invocar esse método passando uma função lambda, se alterna para o contexto indicado (Main, IO ou Default) para executar o bloco de código, retornando o resultado daquele lambda.

Tome sua Poção de Aprendizado +5 e vamos para a prática!


🏹 Prática

Nesse artigo vamos trabalhar com o Lib_Ghi, um protótipo de cliente de acesso à Studio Ghibli API - mas as técnicas que vou mostrar servem para outras soluções que utilizam OkHttp3 para consumir APIs web. O código-fonte está disponível no meu repositório do Github:


🌎 Studio Ghibli API v1.0.1: https://ghibliapi.herokuapp.com/#

💻 Repositório no github: https://github.com/chicorasia/bootcamp-libghi


A classe responsável por fazer a comunicação com a API é a FilmRepository, que mantém dois métodos: um deles recupera os dados da API e ou faz a conversão do Json no objeto do modelo (Film):


class FilmRepository {
​
  fun loadData() : List<Film>{
    val client: OkHttpClient = OkHttpClient()
    val request: Request = Request.Builder()
     .url("https://ghibliapi.herokuapp.com/films")
     .build()
​
    val response = client.newCall(request).execute()
    val result = parseJsonToResult(response.body?.string())
​
    return result.films
​
 }
​
  private fun parseJsonToResult(json: String?): FilmResult {
​
    val turnsType = object : TypeToken<List<Film>>(){}.type
    return FilmResult(Gson().fromJson(json, turnsType))
​
 }
​
}


A classe FilmListViewModel é responsável por manter os dados em uma variável do tipo MutableLiveData<List<Model>>. É isso mesmo, perspicaz desenvolvedor ou desenvolvedora: o app emprega o framework MVVM. O método FilmListViewModel.init() é invocado a partir da MainActivity, numa thread à parte e, ao ser executado, preenche o atributo _filmList com a lista recebida da API. Como tudo tem que ocorrer de maneira assíncrona, é necessário usar o método postValue().


class FilmListViewModel: ViewModel() {
​
  private val _filmList = MutableLiveData<List<Film>>()
  val filmList: LiveData<List<Film>>
    get() = _filmList
​
​
  fun init() {
    getAllFilms()
 }
​
  private fun getAllFilms() {
​
    _filmList.postValue(FilmRepository().loadData())
​
 }
​
}
​
/* Na MainActivity */
​
override fun onCreate(savedInstanceState: Bundle?) {
   
   /... implementação...
   
    Thread{
      mViewModel.init()
   }.start()
    /...
   }


💊 Nessa Pílula de Código eu explico melhor como o app está organizado e por que essas operações precisam ser assíncronas: https://www.youtube.com/watch?v=6eYSh8Rtz5Y


🐲 Primeiro passo: incorporar funções suspend no ViewModel

Antes de mais nada, precisamos adicionar algumas dependências no build.gradle:


implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"


Vamos transformar a função getAllFilms() em uma função de suspensão usando o modificador suspend e colocando as operações em um bloco try-catch a ser executado no escopo do ViewModel usando viewModelScope.launch { }. Já vamos aproveitar e criar uma classe FilmLoadErrordentro desse mesmo arquivo. Também precisamos indicar que o corpo da função init() precisa ser executado dentro de um ViewModelScope:


class FilmListViewModel: ViewModel() {
​
 /.../
​
  fun init() {
    viewModelScope.launch { getAllFilms() }
 }
​
  private suspend fun getAllFilms() {
    viewModelScope.launch {
      try{
        _filmList.value = FilmRepository().dataLoad()
     } catch (error: FilmLoadError){
        Log.e("lib_ghi", "Ocorreu um erro do tipo FilmLoadError")
     }
   }
 }
}
​
class FilmLoadError(override val message: String, cause: Throwable) : Throwable(cause)


Se tentarmos executar o app nesse momento ele vai quebrar com uma NetworkOnMainThreadException💣 ; ao olhar o log, vemos que a exception ocorreu justamente na chamada do método FilmRepository.loadData(). Mas... como isso aconteceu se nós indicamos que a função getAllFilms()é do tipo suspend e roda no escopo do ViewModel?


🧙‍♂️​ É hora de chamar o Dispatcher

As requisições do OkHttp bloqueiam a thread, de modo que simplesmente adicionar o modificador suspendà função loadData() não resolve o problema. Está na hora de traçar o círculo de runas na areia com seu cajado mágico e se preparar para conjurar o Dispatcher.

Vamos modificar a função FilmeRepository.loadData() para executar a requisição à API dentro de um lambda no contexto do Dispatchers.IO, usando a função withContext()que vimos no início do artigo. Também vamos fazer com que o retorno da requisição seja devolvido diretamente, pegando a lista de filmes por meio do atributo films da response e fazendo um safecast para o nosso tipo esperado, um ArrayList<Film>. Repare que o retorno da função precisa ser nulável!


class FilmRepository {
​
  suspend fun loadData(): List<Film>? { //retorno tem que ser nulável
    val client: OkHttpClient = OkHttpClient()
    val request: Request = Request.Builder()
     .url("https://ghibliapi.herokuapp.com/films")
     .build()
​
    return withContext(Dispatchers.IO) { //delega para o Dispatcher desejado
      try {
        val response = client.newCall(request).execute()
        parseJsonToResult(response.body?.string()).films //pegar a lista!
     } catch (cause: Throwable) {
        throw FilmLoadError("Ocorreu um erro", cause)
     }
   } as? ArrayList<Film> // safecast para o tipo esperado
​
​
 }


Vamos entender um pouco melhor esse encantamento?

A tarefa é delegada para o Dispatcher de IO - aquele responsável pelas chamadas de rede e de disco. Com isso, a tarefa é executada em uma thread à parte. No bloco try-catch tentamos fazer a requisição; se ocorrer uma exceção (por exemplo, rede inacessível), jogamos um erro que vai ser tratado pelo bloco de catch do método FilmListViewModel.getAllFilms(). No final, tentamos fazer o cast do resultado para um ArrayList<Film>. Se o resultado for inválido, vai retornar null.

Antes de rodar o app vamos encapsular o comportamento do método FilmListViewModel.getAllFilms() em uma função do tipo suspend lambda. Esse passo não é estritamente necessário mas vai ajudar no futuro, quando quisermos, por exemplo, controlar a exibição do progressBar a partir do ViewModel:


private fun launchDataLoad(block: suspend () -> Unit) {
  viewModelScope.launch {
    //execute alguma coisa antes...
    block()
    //execute alguma coisa depois...
 }
}


E, para finalizar, vamos modificar o método init(), que vai chamar o launchDataLoad() passando a função getAllFilms() como parâmetro. Repare que agora não precisamos mais indicar o escopo da corrotina.


fun init() {
  launchDataLoad { getAllFilms() }
}


E, finalmente, no método onCreate() da nossa MainActivity, podemos invocar o mViewModel.init() diretamente da thread principal, sabendo que a API de coroutines irá cuidar da execução nos escopos corretos:


override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  binding = ActivityMainBinding.inflate(layoutInflater)
  setContentView(binding.root)
​
  val mViewModel = ViewModelProvider.NewInstanceFactory()
   .create(FilmListViewModel::class.java)
​
  showProgressBar()
  mViewModel.init() //executa na thread principal
  mViewModel.filmList.observe(this, {
    val adapter = mViewModel.filmList.value?.let { FilmAdapter(it) }
    filmRecyclerView.layoutManager = LinearLayoutManager(this@MainActivity)
    filmRecyclerView.adapter = adapter
    hideProgressBar()
 })
​
}


Experimente rodar o app; se tudo correu bem, ele vai carregar a lista de filmes corretamente e, ao tentar rodar o app com o dispositivo ou emulador no modo avião, vai ser registrado o lançamento do erro no logcat!


🦄 Mas era só isso?

Calma, jovem aprendiz. Isso é apenas uma implementação inicial. Podemos deixá-la mais robusta e funcional melhorando o tratamento de erros, controlando a exibição da ProgressBar a partir do ViewModel e lançando mensagens de erro por meio de Toast ou de SnackBar, para dar um melhor feedback ao usuário. Mas esse será o tema de outra jornada, pois já é tarde na Floresta das Threads Perdidas e, aqui, a noite é longa e cheia de horrores.


📯Conclusão

Nesse artigo nós avançamos na exploração e compreensão das corrotinas no Android com Kotlin, aprendendo como podemos nos valer dos Dispatchers para executar tarefas de bloqueio a partir de chamadas na Thread principal de maneira segura.

Esse padrão de projeto pode ser empregado para integrar APIs com comportamento de bloqueio ou para executar tarefas com uso intensivo de CPU. No exemplo, estamos consumindo uma API por meio do OkHttp3. As soluções da arquitetura Android - Retrofit e Room - já possuem funções suspend, o que torna mais simples a implementação.

Parabéns, colegas de aventura! Agora podemos recolher nossos tesouros, contar os XPs recebidos e cavalgar para a taverna mais próxima para celebrar mais uma vitória e cantar histórias de heroísmo e aventura!


🔎Para saber mais

🏭 Codelabs: https://developer.android.com/codelabs/kotlin-coroutines#0

📖Documentação: https://developer.android.com/topic/libraries/architecture/coroutines

💻 Repositório no github: https://github.com/chicorasia/bootcamp-libghi

​ 💊 Pílula de Kotlin: usando Coroutines e Dispatchers no Android: https://youtu.be/7-sNqUabVBo


📷

Artur Rutkowski on Unsplash

0
2

Comentários (0)

Arquiteto, urbanista, desenvolvedor Java & Android e criador em chefe na chicorialabs.com.br

Brasil