Visão geral do ViewModel Parte do Android Jetpack.

A classe ViewModel é um detentor de estado da tela ou da lógica de negócios. Ele expõe o estado à interface e encapsula a lógica de negócios correspondente. A principal vantagem do ViewModel é que ele armazena o estado em cache e mantém essas informações mesmo após mudanças de configuração. Isso significa que a interface não precisa buscar dados novamente ao navegar entre diferentes atividades ou implementar mudanças de configuração, como ao girar a tela.

Para mais informações sobre detentores de estado, consulte as orientações sobre esse assunto. Da mesma forma, para mais informações sobre a camada da interface de modo geral, consulte as respectivas orientações.

Benefícios do ViewModel

Uma alternativa para o uso de ViewModel é implementar uma classe simples contendo os dados mostrados na interface. Isso pode se tornar um problema ao navegar entre diferentes atividades ou destinos de navegação. Se esses dados não forem armazenados usando o mecanismo para salvar o estado de instância, eles serão destruídos durante a navegação. O ViewModel fornece uma API adequada para a persistência de dados que resolve esse problema.

Os principais benefícios da classe ViewModel são essencialmente dois:

  • Permite manter o estado da interface.
  • Oferece acesso à lógica de negócios.

Persistência

O ViewModel permite a persistência dos dados após a mudança do estado e após acionar diferentes operações. Com esse armazenamento em cache, não é necessário buscar dados novamente após mudanças de configuração comuns, como a rotação da tela.

Escopo

Ao instanciar um ViewModel, o sistema transmite um objeto que implementa a interface ViewModelStoreOwner. Esse objeto pode ser um destino ou um gráfico de navegação, uma atividade, um fragmento ou qualquer outro tipo que implemente a interface. Em seguida, o Lifecycle do ViewModelStoreOwner é definido como o escopo do ViewModel. Ele continua na memória até que o ViewModelStoreOwner apareça de forma definitiva.

Um intervalo de classes é uma subclasse direta ou indireta da interface ViewModelStoreOwner. As subclasses diretas são ComponentActivity, Fragment e NavBackStackEntry. Para uma lista completa de subclasses indiretas, consulte a documentação de referência do ViewModelStoreOwner.

Quando a atividade ou o fragmento que foi definido como escopo do ViewModel é destruído, o trabalho assíncrono continua a ser executado no ViewModel com escopo. Essa é a chave para a persistência de dados.

Para mais informações, consulte a seção abaixo sobre o ciclo de vida do ViewModel.

SavedStateHandle

O SaveStateHandle permite manter os dados não só depois de mudanças de configuração, mas também após a recriação de processos. Ou seja, com esse elemento, você pode manter o estado da interface mesmo quando o usuário fecha e abre o app novamente.

Acesso à lógica de negócios

Mesmo que a grande maioria das lógicas de negócios estejam presentes na camada de dados, a camada de interface também pode conter uma lógica de negócios. Isso pode ocorrer, por exemplo, ao combinar dados de vários repositórios para criar o estado da interface da tela ou quando um tipo específico de dados não precisa de uma camada de dados.

O ViewModel é o lugar certo para processar a lógica de negócios da camada de interface. Ele também é responsável por processar eventos e os delegar a outras camadas da hierarquia quando a lógica de negócios precisa ser aplicada para modificar dados do app.

Jetpack Compose

No Jetpack Compose, o ViewModel é a principal forma de expor o estado da interface da tela aos elementos combináveis. Em um app híbrido, as atividades e os fragmentos hospedam as funções combináveis. Essa abordagem é diferente das anteriores, em que a criação de partes reutilizáveis da interface com atividades e fragmentos não era tão simples e intuitiva, fazendo com que esses elementos fossem muito mais ativos como controladores de interface.

O aspecto mais importante a ser considerado ao usar o ViewModel com o Compose é que não é possível definir um elemento combinável como o escopo de um ViewModel. Isso ocorre porque um elemento combinável não é um ViewModelStoreOwner. Nesse caso, duas instâncias do mesmo elemento na composição, ou dois elementos diferentes que acessam o mesmo tipo de ViewModel no mesmo ViewModelStoreOwner, receberiam a mesma instância do ViewModel, o que geralmente não é o comportamento esperado.

Para aproveitar os benefícios do ViewModel no Compose, hospede cada tela em um fragmento ou atividade diferente ou use a navegação do Compose e implemente os ViewModels em funções combináveis que estejam o mais próximo possível do destino da navegação. Isso é necessário porque é possível definir atividades, fragmentos, gráficos e destinos de navegação como escopo de um ViewModel.

Para mais informações, consulte o guia de elevação de estado para o Jetpack Compose.

Implementar um ViewModel

Confira abaixo um exemplo de implementação de um ViewModel para uma tela que permite que o usuário jogue o dado.

Kotlin

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Java

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

Depois, você pode acessar o ViewModel de uma atividade desta maneira:

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack Compose

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

Usar corrotinas com o ViewModel

O ViewModel inclui suporte a corrotinas do Kotlin. Com elas, é possível manter o trabalho assíncrono, da mesma maneira que mantém o estado da interface.

Para mais informações, consulte Usar corrotinas do Kotlin com Componentes da arquitetura do Android.

O ciclo de vida de um ViewModel

O ciclo de vida de um ViewModel está diretamente vinculado ao escopo dele. Um ViewModel permanece na memória até que o ViewModelStoreOwner definido como escopo dele desapareça. Isso pode ocorrer nestes contextos:

  • No caso de uma atividade, quando ela é finalizada.
  • No caso de um fragmento, quando ele é removido.
  • No caso de uma entrada de navegação, quando ela é removida da backstack.

Isso faz com que os ViewModels sejam uma ótima solução para armazenar dados que resistam a mudanças de configuração.

A Figura 1 ilustra os vários estados do ciclo de vida de uma atividade à medida que ela é submetida a uma rotação e, em seguida, é concluída. Ela também mostra a vida útil do ViewModel ao lado do ciclo de vida da atividade associada. Este diagrama específico ilustra os estados de uma atividade. Os mesmos estados básicos se aplicam ao ciclo de vida de um fragmento.

Ilustra o ciclo de vida de um ViewModel como um estado de mudanças de atividade.

Geralmente, um ViewModel é solicitado na primeira vez que o sistema chama o método onCreate() de um objeto de atividade. O sistema pode chamar onCreate() várias vezes durante a existência de uma atividade, como quando a tela de um dispositivo é rotacionada. O ViewModel existe de quando você primeiro solicite um ViewModel até que a atividade seja finalizada e destruída.

Como limpar dependências do ViewModel

O ViewModel chama o método onCleared quando o ViewModelStoreOwner o destrói ao longo do ciclo de vida. Isso permite que você limpe qualquer trabalho ou dependência que siga o ciclo de vida do ViewModel.

O exemplo abaixo mostra uma alternativa ao viewModelScope. O viewModelScope é um CoroutineScope (link em inglês) integrado que segue automaticamente o ciclo de vida do ViewModel. O ViewModel usa o escopo para acionar operações relacionadas a lógicas de negócios. Se você quiser usar um escopo personalizado em vez de viewModelScope para testar mais facilmente, o ViewModel poderá receber um CoroutineScope como dependência no construtor. Quando o ViewModelStoreOwner limpa o ViewModel ao final do ciclo de vida, o ViewModel também cancela o CoroutineScope.

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

A partir da versão 2.5 do ciclo de vida, é possível transmitir um ou mais objetos Closeable para o construtor do ViewModel que é fechado automaticamente quando a instância do ViewModel é limpa.

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

Práticas recomendadas

Confira abaixo diferentes práticas recomendadas que precisam ser consideradas ao implementar o ViewModel:

  • Devido ao escopo, use os ViewModels como os detalhes da implementação de um detentor de estado de tela. Não use esses elementos como detentores de estados de componentes da interface reutilizáveis, como grupos de ícones ou formulários. Caso contrário, você receberia o mesmo ViewModel em diferentes usos do mesmo componente de IU no mesmo ViewModelStoreOwner, a menos que você use uma chave de modelo de visualização explícita por ícone.
  • Os ViewModels não podem ser informados sobre os detalhes de implementação da interface. Use os nomes mais genéricos possíveis nos métodos que a API ViewModel expõe e nos campos de estado da interface. Assim, o ViewModel pode acomodar qualquer tipo de interface: smartphones, dispositivos dobráveis, tablets ou até mesmo um Chromebook.
  • Como eles podem potencialmente viver mais do que o ViewModelStoreOwner, os ViewModels não pode conter nenhuma referência a APIs relacionadas ao ciclo de vida, como Context ou Resources para evitar vazamentos de memória.
  • Não transmita ViewModels para outras classes, funções ou outros componentes de interface. Como eles são gerenciados pela plataforma, é necessário mantê-los o mais próximo possível dela. Perto da atividade, fragmento ou função combinável da tela. Isso impede que componentes de nível inferior acessem mais dados e lógicas do que o necessário.

Mais informações

À medida que seus dados se tornam mais complexos, você pode optar por usar uma classe separada apenas para os carregar. O objetivo do ViewModel é encapsular os dados de um controlador de interface para permitir que eles sejam mantidos após mudanças de configuração. Para saber mais sobre como carregar, manter e gerenciar dados após mudanças de configuração, consulte Salvar estados da interface.

O Guia para arquitetura de apps Android sugere criar uma classe de repositório para processar essas funções.

Outros recursos

Para mais informações sobre a classe ViewModel, consulte os recursos abaixo.

Documentação

Amostras