Comunidade

Eu Não Memorizei o ViewModel do Android, Derivei-o de 8 Problemas (Preparação para Entrevistas)

O autor descreve uma abordagem inversa para entender o ViewModel do Android, resolvendo 8 problemas específicos em vez de copiar código pronto. Ele explica como o StateFlow funciona como um quadro branco que só pode ser alterado por uma única entidade, mantendo a consistência da informação. O texto detalha soluções para questões como sobrevivência do ViewModel após rotação da tela e lidar com falhas de rede, enfatizando que cada parte do código tem um propósito claro.

Compartilhar
I Didn't Memorize the Android ViewModel, I Derived It From 8 Problems( Interview Prep) - DEV Community

Todo tutorial de ViewModel que li me entregava o arquivo final: @HiltViewModel, um MutableStateFlow privado, um StateFlow público, viewModelScope.launch, uma classe selada. Compilou. Copiei. Entendi nada.

O problema de começar com código finalizado é que parece arbitrário. Por que duas propriedades ao invés de uma? Por que init? Por que começar em Loading?

Então fiz no sentido contrário. Joguei o código fora e perguntei: quais problemas este item está realmente resolvendo? Existem 8. Empilhe as 8 respostas e o arquivo escreve-se sozinho — nada nele é uma escolha de estilo.

Aqui está a versão que eu gostaria de ter tido.

A Versão para Crianças de 6 Anos

Imagine um quadro branco na parede de uma sala. Qualquer pessoa na sala pode ler o quadro, mas apenas você segura a caneta. Quando você muda o que está escrito, todos olhando para ele veem imediatamente a nova coisa — ninguém precisa perguntar "já mudou?".

O quadro branco é StateFlow. A regra de que apenas você segura a caneta é por que há duas propriedades ao invés de uma. E o quadro pendura na parede (o ViewModel), não em uma pessoa — então quando alguém sai da sala, a escrita sobrevive.

Agora a versão real.

Primeiro: o que uma tela realmente pode estar fazendo?

Antes do ViewModel, um observação: uma tela de lista está sempre em exatamente uma das três situações. Está carregando, foi bem-sucedida ou falhou. É isso.

sealed interface PokemonListUiState {
    object Loading : PokemonListUiState
    data class Success(val pokemon: List<PokemonResult>) : PokemonListUiState
    data class Error(val message: String) : PokemonListUiState
}
Entrar no modo tela cheia Sair do modo tela cheia

sealed significa conjunto fechado — "estas são as únicas 3, nada mais pode se juntar." O compilador sabe a lista completa, então quando a tela faz when (state), ele força você a lidar com todas três. Você não esquece o caso de erro.

Por que não um enum? Porque cada caso carrega carga diferente: Success guarda uma lista, Error guarda uma mensagem, Loading guarda nada. Enums não conseguem fazer isso limpo. E observe que Loading é um object, não um data class — ele não carrega dados, então uma instância compartilhada única é suficiente.

Agora, os 8 problemas.

Problema 1: O telêmetro é frágil

Rode o telefone e o Android destruirá e reconstruirá sua tela. Se a tela mantinha os dados, eles desapareceram — você refazeria a solicitação da rede em cada rotação. Você precisa de algo que durar mais do que a tela.

Solução: um ViewModel. Isso é literalmente seu trabalho.

class PokemonListViewModel : ViewModel()
Enter fullscreen mode Exit fullscreen mode

Problema 2: O ViewModel precisa de um repositório — mas não deveria construir um

Construir um significa construir Retrofit, o que significa OkHttp, o que significa uma pool de conexões... tudo para pedir uma lista de Pokémon. Nós apenas queremos fazer a pergunta, não montar isso.

Solução: deixe o Hilt entregar.

@HiltViewModel
class PokemonListViewModel @Inject constructor(
    private val repository: PokemonRepository
) : ViewModel()
Enter fullscreen mode Exit fullscreen mode

Note que ele pede PokemonRepository — a interface, não a implementação. O ViewModel não deve saber ou se importar de onde vieram os dados.

Problema 3: Os dados chegam MAIS TARDE — como a tela sabe?

A tela já se desenhou enquanto a rede ainda estava trabalhando. Duas opções: a tela continua verificando (

MutableStateFlow<PokemonListUiState>(...)
Entrar no modo tela cheia Sair do modo tela cheia

Defina .value e todos que estão observando são notificados imediatamente. Push, não poll. Isso é o quadro branco.

Problema 4: Mas se a tela pode observar, ela também pode alterá-lo?

Isto seria um desastre. Qualquer tela poderia escrever lixo: viewModel.uiState.value = Success(fakeList). O estado mudaria de qualquer lugar do aplicativo e quando um bug aparecesse você teria que caçar cada arquivo que pudesse tê-lo escrito.

Solução: expor a leitura, esconder a escrita.

private val _uiState = MutableStateFlow<PokemonListUiState>(PokemonListUiState.Loading)
val uiState: StateFlow<PokemonListUiState> = _uiState.asStateFlow()
Entrar no modo tela cheia Sair do modo tela cheia

MutableStateFlow tem um .value configurável. asStateFlow() retorna uma visão de leitura única dos mesmos dados — sem setter exposto. Agora há exatamente um lugar no código onde o estado pode mudar: dentro deste ViewModel. Essa única fonte da verdade é a real recompensa — é um recurso de depuração.

Isto é a resposta para 'por que duas propriedades em vez de uma.'

Problema 5: Quando o fetch deve começar?

No momento em que o usuário abre a tela. Não ao pressionar um botão, não mais tarde.

Solução: execute-o no instante em que o ViewModel é criado.

init { ... }
Entrar no modo de tela cheia Sair do modo de tela cheia

Problema 6: A função do repositório é suspend — você não pode apenas chamá-la

suspend funções só são executadas dentro de uma coroutine. Mas em qual coroutine? Escolha um escopo longo (como GlobalScope) e a coroutine continua rodando mesmo após o usuário ter saído da tela — mantendo referências, atualizando estado para algo que não existe mais. Isso é um vazamento de memória.

Solução: use um escopo cuja vida está ligada à do ViewModel.

viewModelScope.launch { ... }
Entrar no modo de tela cheia Sair do modo de tela cheia

Dois termos para separar: escopo = onde a coroutine vive (viewModelScope, que o Android cancela automaticamente quando o ViewModel morre). builder = o que inicia (launch). Você não cria o escopo; a biblioteca lhe dá um pré-conectado ao ciclo de vida correto.

Problema 7: A rede pode falhar

Sem wifi. Servidor fora do ar. Tempo limite.

Solução: tente, e se explodir, capture a exceção e mostre o erro em vez de fazer crash.

Problema 8: O que o usuário olha antes da resposta chegar?

Tela em branco? Lista vazia?

Solução: comece com o estado Loading, antes de qualquer coisa acontecer.

MutableStateFlow<PokemonListUiState>(PokemonListUiState.Loading)
Entrar no modo tela cheia Sair do modo tela cheia

O usuário vê um spinner imediatamente, e é a verdade: "Estou trabalhando nisso."

Empilhe o 8 e o arquivo se escreve sozinho

@HiltViewModel
class PokemonListViewModel @Inject constructor(
    private val repository: PokemonRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<PokemonListUiState>(PokemonListUiState.Loading)
    val uiState: StateFlow<PokemonListUiState> = _uiState.asStateFlow()

    init {
        viewModelScope.launch {
            _uiState.value = try {
                val response = repository.getPokemonList(limit = 20, offset = 0)
                PokemonListUiState.Success(response.results)
            } catch (e: Exception) {
                PokemonListUiState.Error(e.message ?: "Algo deu errado")
            }
        }
    }
}
Entre no modo tela cheia Sair do modo tela cheia

Leia como uma frase: sobreviva à tela → peça pelo repositório → mantenha o estado de forma observável → esconda a escrita → comece em Carregando → faça a busca ao nascer → em um escopo que morre com você → lidar com falhas. Nenhuma linha é arbitrária.

 Problemas que encontrei

  • "MutableStateFlow faz com que sobreviva à rotação." Não — eu combinei dois trabalhos separados. O ViewModel sobrevive (Problema 1). StateFlow notifica (Problema 3). Eles apenas acontecem de viver no mesmo arquivo.
  • Loading vs Success(emptyList()). Eu pensei que Loading existia para evitar um crash. Não existe — nada irá cair. É sobre não mentir ao usuário. Loading significa "Estou trabalhando." Success(emptyList()) significa "Terminei e encontrei zero Pokémon." Essas são afirmações completamente diferentes, e uma delas é falsa antes que a rede responda.
  • e.message é nuloável. Error(e.message) não compila contra um String. Você precisa de um fallback: e.message ?: "Algo deu errado".
  • Expor o fluxo mutável "apenas por agora." No momento em que é público, o estado pode mudar de qualquer lugar e sua única fonte da verdade está perdida. Nunca permanece "apenas por agora".

 Conclusões prontas para entrevistas

P: Por que duas propriedades — uma privada _uiState e uma pública uiState?
Para que a tela possa ler o estado, mas nunca escrever nele. asStateFlow() expõe uma visão de leitura única do mesmo dado, mantendo o ViewModel como o único lugar onde o estado pode mudar. Com um MutableStateFlow público, qualquer arquivo poderia mutá-lo e os bugs tornam-se inlocalizáveis.

P: Por que usar viewModelScope.launch em vez de GlobalScope.launch?
viewModelScope é automaticamente cancelado quando o ViewModel é destruído. GlobalScope sobrevive à tela — a coroutine continua rodando e mantendo referências para uma tela que o usuário já deixou. Isso é um vazamento de memória.

P: Por que começar em Loading em vez de Success(emptyList())?
Porque você ainda não conseguiu — você nem mesmo perguntou. Success(emptyList()) informa ao usuário "pronto, encontrei nada", o que é uma mentira. Loading diz "Estou trabalhando", o que é verdade e lhes dá um spinner em vez de uma tela vazia falsa.

P: Por que usar uma interface sealed para UiState ao invés de um enum ou booleanos?
Sealed = conjunto fechado, então when é exaustivo e você não pode esquecer um estado. Diferente de um enum, cada caso carrega seus próprios dados (Success → uma lista, Error → uma mensagem). Diferente de isLoading/isError booleanos, você não pode representar combinações impossíveis como loading e error ao mesmo tempo.

 TL;DR

A ViewModel não é uma pilha de cerimônias — são 8 respostas empilhadas. Sobreviver à rotação (ViewModel), receber dependências entregues a você (Hilt), fazer o estado observável (StateFlow), manter o marcador para si mesmo (privado mutável + público somente leitura), começar honesto (Loading), buscar ao nascer (init), permanecer em um escopo que morre com você (viewModelScope) e esperar falhas (try/catch).

Memorize o porquê de cada uma. Procure a sintaxe — ninguém se lembra como escrever asStateFlow().

Fonte original

Conteúdo traduzido e adaptado pela redação do Notícias Mobile. Confira também a matéria na fonte original.

Leia a matéria completa