Skip to content

Instantly share code, notes, and snippets.

@jhonatansabadi
Created March 17, 2022 15:58
Show Gist options
  • Save jhonatansabadi/4cd40b7434e2484909131f643e0df8a5 to your computer and use it in GitHub Desktop.
Save jhonatansabadi/4cd40b7434e2484909131f643e0df8a5 to your computer and use it in GitHub Desktop.

Guia completo sobre viewModelScope e conceitos de Coroutines

Bom, se você já utiliza Coroutines e sua arquitetura é a MVVM ou então Clean Architecture, ou até uma mistura dos dois, você provavelmente já ouviu falar de viewModelScope.

De forma bem resumida, o viewModeScope nada mais é do que um coroutineScope, ou seja, um bloco onde eu posso executar minhas coroutines sem travar a thread em que estou executando.

Ele respeita o ciclo de vida do seu ViewModel, o que facilita o cancelamento de coroutines que não são mais necessárias, evitando assim o temido memory leak 👻  .

Tá... então se eu tenho uma suspend fun e quero executar do meu ViewModel, é só só colocar dentro do viewModelScope que vai estar tudo certo e meu app não vai travar e minha MainThread vai ficar livre??

Não exatamente... o viewModelScope vai sim liberar sua MainThread, mas ele ainda vai executar na MainThread...

Pra entender tudo isso, vamos esmiuçar o viewModelScope.

Composição do viewModelScope

https://gist.github.com/20483f4d8f581da754cbd1ae33ba726b

Ele é uma variável que executa no contexto do ViewModel, e por fim, ele retorna um CoroutineScope.

Vamos entender um pouco melhor seu funcionamento

Primeiro, vamos separar por tópicos:

  • CoroutineScope
  • SupervisorJob
  • Dispatcher.Main.Imediate

CoroutineScope

https://gist.github.com/9864701890a9bdd8cbb337f0b419a01a

É uma interface que possui uma variável do tipo CoroutineContext.

CoroutineContext

Basicamente uma interface com um set (list) de CoroutineContext, que podem ser:

  • Dispatcher
  • Job
  • CoroutineName
  • Outros

É bem comum ver algo do tipo:

https://gist.github.com/13bae571692bd9a404ec443f25138a9c

O coroutine context tá criando uma lista com esses valores, algo do tipo:

https://gist.github.com/21baad07ea7fe79a06c2b53baff715a9

E pra que exatamente isso serve?

Bom, o contexto é importante para coroutine saber onde vai executar e de que forma vai ser executada, o contexto “configura” a coroutine para um Thread separada para IO com o Dispatcher.IO e cria um Job() falaremos mais abaixo.

Job()

Primeiro vale entender o que é um Job.

Job é algo cancelável que possui um ciclo de vida e que tem por objetivo completar o clico.

Trazendo pro nosso contexto, Job é a coroutine em si, quando você lança uma coroutine, está lançando um Job, e quando ele finaliza, ele entra em estado de completo e te devolve a informação desejada, caso seja cancelado, de forma intencional ou não, ele entra em estado de cancelado e por ai vai.

CoroutineScope em si é um Job, e toda coroutine lançada dentro dele, vai fazer do seu ciclo de vida.

Captura de Tela 2022-03-17 às 10.17.05.png

Trazendo para o código, teríamos algo assim:

https://gist.github.com/756bd3771728734611fc0db0ad747f3c

  • Scope Job → viewModelScope
    • Job 1 → launch
      • Job 1 → launch
      • Job 2 → launch
      • Job 3 → launch

Todos os launch { } estão abaixo do viewModelScope, então eles respeitam o ciclo de vida.

Resumindo, quando você sai da sua Activity ou Fragment, o ViewModel vai encerrar, e vai cancelar todos os viewModelScopes, com isso, todos os Jobs lançados dentro desse scope serão cancelados junto.

O principal ponto do Job dentro de um scope, é que eles trabalham em conjunto, por exemplo.

Você precisa fazer 3 chamadas a uma API, e depois junta as 3 informações e devolve pra UI.

https://gist.github.com/160c48c5cd87d5c03290dc2b51450bc7

Nesse cenário, não faz sentido retornar somente o name, precisamos também do lastName, então caso a função getLastNameFromApi() falhe, eu também quero cancelar o getNameFromApi().

Com o Job() isso já é feito, ele propaga o cancelamento para os outros Jobs que estão dentro do mesmo contexto.

Captura de Tela 2022-03-17 às 10.29.58.png

Agora que já entendemos o que é e como funciona um Job, agora vamos de fato para o SupervisorJob

SupervisorJob()

Ele é um Job, então todo o conceito de se aplica a ele, exceto um, o cancelamento.

É aí que mora toda a diferença, vamos voltar para o exemplo da chamada da API.

https://gist.github.com/293a3a6169d67efec8eb0ea2699ee621

Nesse cenário, quando o getLastNameFromApi falhar, o getNameFromApi não vai ser cancelado, e sim mantido. 🙌🏽

Captura de Tela 2022-03-17 às 10.40.59.png

E o viewModelScope utiliza esse mecanismo, quando um Job falha, ele não vai falhar os outros, e porque disso?

o viewModeScope é único para o seu ViewModel, e ele envolve todos os seus Jobs (coroutines) dentro do seu escopo, então no seu viewModelScope você precisar fazer várias chamadas de diferentes lugares, que as vezes nem se conversam, então você precisa tratar de forma individual, exemplo:

https://gist.github.com/11b6210d98020357e06c1e9aa1f25a3e

Faria sentido o setUser ser cancelado e propagar o cancelamento para o setUserConfigurations? Não, né?!

Esse é o motivo do viewModelScope ser um SupervisorJob e não um simples Job.

👉🏽 Vale reforçar que, o `ViewModel` sendo cancelado pela `Activity` ou `Fragment`, ambos `setUser` e `setUserConfiguration` serão cancelados para não criar memory leak.

E para finalizar o nosso entendimento, vamos ver sobre o Dispatcher do viewModelScope.

Dispatcher.Main.immediate

Primeiro de tudo, o que é um Dispatcher?

Nada mais é do que kotlin object que define em qual tipo de Thread a coroutine vai ser executada, que tem as seguintes variáveis:

  • Main
  • Default
  • IO
  • Unconfined

Dispatchers.Main

Executa coroutines dentro da MainThread, ou seja, essa operação vai concorrer com suas operações de UI, como: Listeners, UI draws e tudo que envolve UI.

Então o que devo executar?

Animações, sets de UI e buscas simples que retornam dados para UI.

Dispatchers.IO

Executa coroutines dentro de um conjunto de Threads separadas para IO (input/output).

Esse dispatcher tem basicamente dois usos:

  • Chamadas de API (Retrofit)
  • Chamadas de Banco de dados (Room)

Dispatchers.Default

Executa coroutines dentro de um conjunto de Threads, assim como o IO, porém o foco dele é em operações pesadas do próprio app.

Quando utilizar?

  • Operações em listas (sort, filter, map)
  • Transformação de imagem (resize, png to bitmap)
  • E outros

Dispatchers.Unconfined

Por fim, temos o Unconfined, que como o nome já diz, ele não define nenhum tipo de Thread para ser executado, ele é executado com o Dispatcher que o chamar, e posso finalizar em outra

Seu uso não é recomendado em código real.

Agora que entendemos os tipos principais de Dispatchers, faltou entender o que ser o Immediate.

Dispatchers.Main.immediate

Sabemos que o Dispatchers.Main executa uma coroutine dentro da MainThread, certo?

Mas a questão é: Quando ele executa a coroutine?

A resposta é 🥁 : Depende

Depende é a melhor resposta. Depende do que está sendo executado na MainThread, por exemplo:

A MainThread está desenhando os elementos de UI com o onDraw, está adicionando os Listeners, e no meio disso tudo você chama uma coroutine, e o que acontece? Ela joga para o final da lista de execução, :

  1. onDraw
  2. Listeners
  3. Sua coroutine

Então o immediate faz com que sua coroutine seja executada na hora que você a chamar, “passando” ela na frente de outras operações.

  1. Sua coroutine 🏃🏽‍♂️
  2. Listeners
  3. onDraw
💡 Vale ressaltar que essa mudança não vai impactar a criação em si do seu layout e nem provocar travamentos.

Então o immediate do Dispatchers.Main serve para acelerar o processo da criação/execução da sua coroutine.

Voltando ao viewModelScope

Depois de detalhamento de todos os elementos que envolvem o viewModelScope,

vale relembrar os principais elementos de coroutines utilizados:

  • CoroutineScope
  • CoroutineContext
  • SupervisorJob
  • Dispatchers

E os o principais pontos de entendimento do viewModelScope em si:

  • É um scope que roda somente dentro do ViewModel
  • É cancelado quando o ViewModel também é cancelado por uma Activity ou Fragment
  • Ele não propaga cancelamento, ou seja, se um Job dentro dele falhar, os outros permanecem
  • E ele roda da MainThread

Formas de utilizar o viewModelScope

Agora fica mais fácil a utilização, então vamos lá.

A forma mais simples de utilizar é:

https://gist.github.com/b99dced1365ca5144864154550eef7b3

Neste exemplo simples, ele está fazendo o seguinte:

  • Utilizando um scope previamente criado pelo meu ViewModel
  • Criando um Job com o launch
  • Passando como parâmetro para minha coroutine dois contextos
    • SupervisorJob
    • Dispatchers.Main.immediate

Então neste simples bloco, o viewModelScope se encarrega de fazer tudo pra gente.

Tá...Então se fizer uma chamada de API dessa forma, ela será feita na MainThread? 🤔

SIM! Será feito na MainThread.

E como evitar isso?

Tem basicamente duas formas de se evitar.

  • Trocando o Dispatcher
  • Movendo a responsabilidade da troca de Thread para camada de dados

Trocando o Dispatcher do launch por Dispatcher.IO

https://gist.github.com/48c95ca359a562a816affc296584bb71

⚠️  Porém, devemos tomar o seguinte cuidado ao fazer isso.

Toda vez que eu troco o Dispatcher, o meu código é executado em outra Thread, certo?!

Então, imagina que você precisa atualizar um LiveData que está dentro do seu ViewModel com o valor da sua chamada.

https://gist.github.com/965550b47827cdcc36dd1c733c54ca66

Neste cenário, o Android Studio mostrará uma mensagem bem amigável falando que você não pode alterar elementos da sua MainThread a partir de outra Thread.

Captura de Tela 2022-03-17 às 11.57.15.png

E como resolver isso de forma prática?

Bem simples, trocando o value por postValue

https://gist.github.com/54bbbc48c840302a347695393b916e89

Dessa forma, o valor não será atribuído no momento exato, será criado uma Task para a MainThread atribuir o valor.

Tá, e isso faz diferença?

Sim, faz diferença.

https://gist.github.com/fcfccffa5e8149e5beb46fef371a27a8

Neste caso, o primeiro valor atribuído será o result 2, e em seguida o result 1, pois o value é executado de forma imediata e o postValue é executado através de uma Task na MainThread.

Movendo a responsabilidade da troca de Thread para camada de dados

Se você está utilizado MVVM, é bem provável que você tenha um Repositoy, e no caso de Clean Architecture, UseCase.

No exemplo, vamos utilizar um UseCase.

https://gist.github.com/3677b974b7bb0ae048e62f2872c62726

Detalhando o funcionamento do exemplo:

  • O viewModelScope vai criar um Job com o launch na MainThread

  • Dentro do launch, é chamado o UseCase

    • Dentro do UseCase chamamos o withContext passando o Dispatcher.IO
    💡 O `withContext` faz a troca de contexto, ou seja, vem até ele com `Dispatchers.Main`, ele faz a troca para o `Dispatchers.IO,` executa a coroutine na `Thread` de `IO`, e no seu retorno, ele volta para a `MainThread`.
    • Retornamos o User para o ViewModel
  • Atribuimos o valor do UseCase no LiveData

Com essa abordagem, podemos deixar nossa MainThread livre de operações de IO e não precisamos nos preocupar com a troca de contexto que ocorre no UseCase.

Coroutines que precisam “viver” além do ciclo de vida do ViewModel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment