Guia completo sobre viewModelScope e conceitos de Coroutines

Jhonatan Sabadi
Android Dev BR
Published in
8 min readMar 17, 2022

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 viewModelScope 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ó 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...

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

Composição do viewModelScope

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

Vamos separar por tópicos:

  • CoroutineScope
  • SupervisorJob
  • Dispatcher.Main.Imediate

CoroutineScope

public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}

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

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

  • Dispatcher
  • Job
  • CoroutineName
  • Outros

É bem comum ver algo do tipo:

O coroutine context está criando uma lista com esses valores:

E para 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 uma determinada Thread com um determinado Dispatcher e cria um Job() (falaremos mais abaixo).

Primeiro vamos entender o que é um Job.

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

Trazendo para o 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 também é um Job, e toda coroutine lançada dentro dele, vai respeitar o seu ciclo de vida.

Tudo se resume em Job

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

  1. - Scope Job → viewModelScope

1.1 - Job 1 → launch

1.1.1 - Job 1 → launch

1.1.2 - Job 2 → launch

  1. 1.3 - Job 3 → launch

Implicitamente, o launch { } está pegando o Job pai e atribuindo a ele mesmo, dessa forma ele consegue manter a referência.

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

Quando você sai da sua Activity ou Fragment, o ViewModel vai cancelar o viewModelScope, 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 chamadas a uma API, e depois unifica as informações e devolve pra UI.

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 escopo.

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

SupervisorJob

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

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

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

E o viewModelScope utiliza esse mecanismo, quando um Job falha, ele não vai propagar a falhar para 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 sempre são relacionados, então você precisa tratar de forma individual, exemplo:

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 setUserConfigurations serão cancelados para não criar memory leak.

Para completar a estrutura, faltou falar apenas sobre o Dispatchers

Primeiro de tudo, o que é um Dispatcher?

Nada mais é do que um 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 duas utilizações:

  • 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, 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 podendo ser finalizar em outro Dispatcher.

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 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,

Vamos relembrar os principais elementos de coroutines utilizados:

  • CoroutineScope
  • CoroutineContext
  • SupervisorJob
  • Dispatchers

E os o principais pontos de entendimento do viewModelScope:

  • É um escopo que pode ser utilizado somente dentro do ViewModel
  • É cancelado quando o ViewModel é 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 é:

Neste exemplo simples, ele está fazendo o seguinte:

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

2. Dispatchers.Main.immediate

Então neste simples bloco, o viewModelScope se encarrega de fazer toda configuração necessária para criação de coroutine .

Sabendo que o viewModelScope utiliza Dispatcher.Main.immediate , quando realizarmos uma chamada de API, ele utilizará a MainThread ?🤔

SIM! Será feito na MainThread. E como evitar isso?

Tem basicamente duas formas de evitar:

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

Trocando o Dispatcher do launch por Dispatcher.IO

⚠️ Devemos tomar 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.

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.

E como resolver isso de forma prática?

Bem simples, trocando o value por postValue

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

E isso faz diferença?

Sim, faz diferença.

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.

Detalhando o funcionamento do exemplo:

  • O viewModelScope vai criar um Job com o launch na MainThread
  • Dentro do launch, é chamado o UseCase

💡 O withContext faz a troca de contexto, ou seja, vem até ele com Dispatcher.Main.immediate, ele faz a troca para o Dispatcher.IO , executa a coroutine na Thread de IO, e no seu retorno, ele volta para a MainThread.

  • Por fim, atribuímos 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

Imagina que no seu app, você tem uma tela onde você faz um update no seu banco de dados, e volta para a tela principal, seu código seria:

Até ai tudo certo.

Só que tem um porém…

Este update, é um update bem rápido, mas pode ser que por algum motivo ele demore alguns milisegundos a mais.

E se isso acontecer, e o usuário apertar o botão de voltar e sua operação ainda não estiver sido concluída, ela simplesmente não vai ser, pois o ViewModel vai ser cancelado com a finalização da activity/fragment e vai cancelar o viewModelScope, que vai cancelar seu Job, no caso, o seu update.

Esse é um caso clássico, e a solução é bem simples.

Toda vez que damos um launch, ele cria um Job, certo?! E para criar o fluxo de Parent Job, Child Job, o Job utiliza o coroutineContext.job para manter a referência.

E para “quebrar” essa cadeia, vamos fazer da seguinte forma:

Quando atribuímos um SupervisorJob ou um Job para o launch, quebramos a referência do pai, por simplesmente não passarmos o coroutineContext.job em seu construtor.

Dessa forma, quando o viewModelScope for cancelado, ele não tem referência para cancelar o Job filho criado e com isso sua coroutine não é encerrada.

Tome cuidado ao utilizar…

Como ela não é cancelada, você pode acabar causando memory leak, então tenha cuidado ao utilizar, utilize apenas em casos que são realmente necessários.

--

--