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
.
https://gist.github.com/20483f4d8f581da754cbd1ae33ba726b
Ele é uma variável que executa no contexto do ViewModel
, e por fim, ele retorna um CoroutineScope
.
Primeiro, vamos separar por tópicos:
- CoroutineScope
- SupervisorJob
- Dispatcher.Main.Imediate
https://gist.github.com/9864701890a9bdd8cbb337f0b419a01a
É uma interface que possui uma variável do tipo 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.
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.
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
- Job 1 → 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.
Agora que já entendemos o que é e como funciona um Job
, agora vamos de fato para o 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. 🙌🏽
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
.
E para finalizar o nosso entendimento, vamos ver sobre o Dispatcher do viewModelScope.
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
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.
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)
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
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.
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, :
- onDraw
- Listeners
- 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.
- Sua coroutine 🏃🏽♂️
- Listeners
- onDraw
Então o immediate
do Dispatchers.Main
serve para acelerar o processo da criação/execução da sua coroutine.
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 umaActivity
ouFragment
- 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.
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
https://gist.github.com/48c95ca359a562a816affc296584bb71
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
.
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
.
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
- 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.