ViewModel compartilhado no Android

Nelson Glauber
6 min readJun 2, 2020
Source: https://bit.ly/3eFLecs

Tenho feito algumas PoC (Proof of concept) no projeto que estou trabalhando atualmente. Coincidentemente essas PoCs possuíam certos fluxos de telas que continham estados que deveriam ser compartilhados entre telas de um mesmo fluxo. Por exemplo, um fluxo de sign in, o usuário digitava o email em uma tela, na outra o nome e sobrenome, em seguida aceitava os termos de uso, etc. Um outro caso foi em um aplicativo de perguntas e respostas (trivia), onde todo os estado do jogo era controlado por mensagens vindas de um web socket, então basicamente eu tinha o estado de loading (com um contador de tempo até a próxima pergunta), a tela da pergunta em si e a tela de resultado da rodada.

Em ambos os casos, um único view model poderia representar o estado do fluxo de telas, pois elas eram intimamente relacionadas. Nesse contexto decidi utilizar a classe ViewModel do Android para que as telas pudessem compartilhar esse estado. Em termos práticos, cada etapa ou tela do fluxo era um Fragment que altera o estado do ViewModel e a Activity apenas escuta as mudanças no ViewModel (via LiveData) para exibir o Fragment apropriado.

Nesse post vou apresentar um exemplo simples de como eu fiz essa implementação.

Configuração

Como mencionei anteriormente, vou utilizar a biblioteca ViewModel do Jetpack. Por isso, é necessário realizar as seguintes mudanças no build.gradle do módulo.

android {
...

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
...


implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.4"
}

Os blocos compileOptions e kotlinOptions determinam que o código seja compilado para o Java 8. Em seguida, a dependência da biblioteca do ViewModel é declarada. Perceba que ela está usando o sufixo -ktx para ter suporte às features exclusivas do Kotlin disponibilizadas pelo Jetpack KTX. Além da biblioteca do ViewModel, também foram adicionadas as bibliotecas KTX para Activity e Fragments.

ViewModel

A classe MyViewModel é bem simples como pode ser observada a seguir.

class MyViewModel: ViewModel() {
private val _currentStep = MutableLiveData<Int>().apply {
value = 0
}
val currentStep: LiveData<Int> = _currentStep

fun acceptStep1() {
_currentStep.value = 1
}

fun acceptStep2() {
_currentStep.value = 2
}
}

Essa classe apenas armazena o passo atual do fluxo de telas, mas pense que aqui ela poderia estar armazenando o email do usuário digitado na primeira tela, depois o nome, em seguida se ele aceitou os termos de uso, etc.

Perceba que foi utilizado um MutableLiveData para mudar os valores internamente e expô-los via LiveData (que é imutável) para que outra classe observe as mudanças nesses dados. Para quem não está muito familiarizado com o MVVM no Android, esse é um padrão bastante comum.

As funções acceptStep1 e acceptStep2 servem basicamente para mudar o estado interno do view model, mas novamente, pense que nesses métodos poderíamos estar fazendo uma requisição web que salvasse esses dados no servidor, por exemplo.

Activities e Fragments

Vou começar com os Fragments…

Step1Fragment

class Step1Fragment : Fragment(R.layout.fragment_step1) {
private val viewModel: MyViewModel by activityViewModels()

override fun onViewCreated(view: View,
savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnStep1.setOnClickListener {
viewModel.acceptStep1()

}
}
}

Step2Fragment

class Step2Fragment : Fragment(R.layout.fragment_step2) {
private val viewModel: MyViewModel by activityViewModels()

override fun onViewCreated(view: View,
savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnStep2.setOnClickListener {
viewModel.acceptStep2()

}
}
}

Eles são praticamente idênticos. O primeiro chama a função acceptStep1 do view model enquanto que o segundo chama acceptStep2.
A parte interessante é a maneira de como esses fragments obtêm o view model utilizando o método activityViewModels da biblioteca KTX de fragments. Dessa forma, ambos os fragments compartilham a mesma instância do view model definido na activity.

Aqui os fragments apenas estão invocando métodos do view model, mas eles também poderiam observar mudanças de um estado específico do view model. Não estou fazendo aqui para deixar o exemplo mais simples.

Como a activity obtém a instância do view model? Veja no código a seguir:

class MainActivity : AppCompatActivity(R.layout.activity_main) {
private val viewModel: MyViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.currentStep.observe(this, Observer { step ->
when (step) {
0 -> supportFragmentManager.commit {
replace(R.id.vwRoot, Step1Fragment())
}
1 -> supportFragmentManager.commit {
replace(R.id.vwRoot, Step2Fragment())
}
2 -> {
Toast.makeText(this, "Done!",
Toast.LENGTH_SHORT).show()
finish()
}
}
})

}
}

Para obter a instância do view model, a activity utiliza a função viewModels disponível na biblioteca KTX da Activity. Perceba que a activity está apenas orquestrando a exibição dos fragments ao observar a mudança do estado atual do view model.

A figura a seguir mostra a aplicação em execução. Intencionalmente usei o emulador do Android 6 para demonstrar que esse recurso funciona também em versões mais antigas do Android.

Aplicação de exemplo em execução

Passando parâmetros para o ViewModel

Nesse exemplo, o ViewModel não recebe nenhum parâmetro, mas é muito comum que isso aconteça, pois normalmente o view model é o intermediário entre a UI e as partes internas do seu app (ou pelo menos deveria ser).

Então digamos que a classe MyViewModel necessite receber um parâmetro em seu construtor como a seguir:

class MyViewModel(val someString: String): ViewModel() { ... }

Aqui estou usando uma String, mas pense que poderia ser o repositório da sua aplicação ou um use case se você estiver usando clean architecture. Nesse caso, seria preciso definir uma subclasse de ViewModelProvider.Factory para passar esse parâmetro.

class MyViewModelFactory(
private val someParam: String
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MyViewModel(someParam) as T
}
}

Essa classe ficará responsável por criar a instância do view model. O parâmetro someParam recebido no construtor será usado para instanciar o MyViewModel na função create.
Por fim, na activity, para obter a instância do view model, é preciso fazer a seguinte mudança:

class MainActivity : AppCompatActivity(R.layout.activity_main) {
private val viewModel: MyViewModel by viewModels {
MyViewModelFactory("Sua String")
}
...

Passando parâmetros para o ViewModel via Koin

Se estiver utilizando alguma biblioteca de injeção de dependências como o Koin. Isso pode ser feito sem a necessidade de criar a subclasse de ViewModelProvider.Factory.

Primeiramente é preciso adicionar a dependência do Koin:

dependencies {
def koin_version = '2.1.5'
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-android-viewmodel:$koin_version"
}

Em seguida, criar o arquivo (que aqui nomeei como appModule.kt) onde será definido o módulo que proverá as dependências da aplicação, e que nesse exemplo apenas instanciará o view model.

import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module

val appModule = module {
viewModel { MyViewModel("Sua string") }
}

Depois, o Koin deve ser inicializado. Isso normalmente isso é feito no método onCreate da aplicação.

class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@MyApp)
modules(appModule)
}
}
}

Não esqueça (como eu sempre esqueço) de registrar a classe no AndroidManifest.xml.

<application android:name=".MyApp" ...

E por fim, fazer a mudança na activity.

class MainActivity : AppCompatActivity(R.layout.activity_main) {
private val viewModel: MyViewModel by viewModel()

Perceba que aqui foi utilizada a função viewModel do Koin (no singular, ao contrário da viewModels da lib KTX).

Conclusão

Dependendo do seu caso de uso, a utilização de um view model compartilhado pode ser uma boa alternativa. Como foi possível observar nesse post, sua utilização é bastante simples graças às bibliotecas KTX e/ou bibliotecas de injeção de dependências como o Koin.

Mas lembre-se: se seu ViewModel começar a ficar muito grande, então é porque chegou a hora de refatorá-lo para dividir o trabalho. Nesse caso, cada tela teria seu View Model e esses view models deveriam receber os parâmetros necessários para dar continuidade no fluxo.

--

--

Nelson Glauber

Android Developer. Google Developer Expert for Android/Kotlin. Author of “Dominando o Android”.