Descripción general de ViewModel   Parte de Android Jetpack.

La clase ViewModel es una lógica empresarial o un contenedor de estado a nivel de pantalla. Expone el estado a la IU y encapsula la lógica empresarial relacionada. Su principal ventaja es que almacena en caché el estado y lo conserva durante los cambios de configuración. Esto significa que la IU no tiene que recuperar datos cuando navegas entre actividades o si sigues cambios de configuración, como cuando rotas la pantalla.

Para obtener más información sobre los contenedores de estado, consulta la guía de contenedores de estado. Del mismo modo, para obtener más información sobre la capa de la IU en general, consulta la guía de la capa de la IU.

Beneficios de ViewModel

La alternativa a un ViewModel es una clase simple que contiene los datos que muestras en tu IU. Esto puede convertirse en un problema cuando navegas entre actividades o destinos de Navigation. Si lo haces, esos datos se destruirán si no los almacenas con el mecanismo de guardado de estado de instancias. ViewModel proporciona una API conveniente para la persistencia de datos que resuelve este problema.

Los beneficios clave de la clase ViewModel son básicamente dos:

  • Te permite conservar el estado de la IU.
  • Proporciona acceso a la lógica empresarial.

Persistencia

ViewModel permite la persistencia tanto en el estado que contiene un ViewModel como en las operaciones que este activa. Este almacenamiento en caché significa que no necesitas recuperar datos mediante cambios de configuración comunes, como una rotación de pantalla.

Alcance

Cuando creas una instancia de ViewModel, le pasas un objeto que implementa la interfaz ViewModelStoreOwner. Puede ser un destino de Navigation, un gráfico de navegación, una actividad, un fragmento o cualquier otro tipo que implemente la interfaz. El alcance de tu ViewModel se define en el Ciclo de vida del ViewModelStoreOwner. Continúa en la memoria hasta que su ViewModelStoreOwner desaparece de forma permanente.

Un rango de clases son subclases directas o indirectas de la interfaz ViewModelStoreOwner. Las subclases directas son ComponentActivity, Fragment y NavBackStackEntry. Para obtener una lista completa de las subclases indirectas, consulta la referencia de ViewModelStoreOwner.

Cuando se destruye el fragmento o la actividad para los que se definió el alcance del ViewModel, el trabajo asíncrono continúa en el ViewModel específico. Esta es la clave de la persistencia.

Si deseas obtener más información, consulta la sección sobre el ciclo de vida de ViewModel.

SavedStateHandle

SavedStateHandle te permite conservar datos no solo a través de cambios de configuración, sino también durante la recreación de procesos. Es decir, te permite mantener el estado de la IU intacto, incluso cuando el usuario cierra la app y la abre más adelante.

Acceso a la lógica empresarial

Aunque la gran mayoría de la lógica empresarial está presente en la capa de datos, también puede estarlo en la capa de la IU. Esto puede suceder cuando se combinan datos de varios repositorios para crear el estado de la IU de la pantalla o cuando un tipo particular de datos no requiere una capa de datos.

ViewModel es el lugar adecuado para administrar la lógica empresarial en la capa de la IU. ViewModel también se encarga de controlar los eventos y delegarlos a otras capas de la jerarquía cuando se debe aplicar la lógica empresarial para modificar los datos de la aplicación.

Jetpack Compose

Cuando se usa Jetpack Compose, ViewModel es el medio principal para exponer el estado de la IU de la pantalla a tus elementos componibles. En una app híbrida, las actividades y los fragmentos simplemente alojan las funciones de componibilidad. Esto representa un cambio respecto de enfoques anteriores, en el que no era tan simple e intuitivo crear piezas de IU reutilizables con actividades y fragmentos, lo que los hacía mucho más activos como controladores de IU.

Lo más importante que debes tener en cuenta cuando usas ViewModel con Compose es que no puedes definir el alcance de un ViewModel para un elemento componible. Esto se debe a que un elemento de este tipo no es un ViewModelStoreOwner. Dos instancias del mismo elemento componible en la composición, o dos elementos componibles diferentes que acceden al mismo tipo de ViewModel con el mismo ViewModelStoreOwner recibirían la misma instancia del ViewModel, que a menudo no es el comportamiento esperado.

Para obtener los beneficios de ViewModel en Compose, aloja cada pantalla en un fragmento o una actividad, o usa Compose Navigation y ViewModels en funciones de componibilidad lo más cerca posible del destino de Navigation. Esto se debe a que puedes definir el alcance de un ViewModel a los destinos de Navigation, los gráficos de navegación, las actividades y los fragmentos.

Para obtener más información, consulta la guía sobre la elevación de estado para Jetpack Compose.

Cómo implementar un ViewModel

El siguiente es un ejemplo de implementación de un ViewModel para una pantalla que le permite al usuario lanzar un 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
            )
        );
    }
}

Luego, puedes acceder a ViewModel desde una actividad de la siguiente manera:

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
}

Cómo usar corrutinas con ViewModel

ViewModel incluye compatibilidad con corrutinas de Kotlin. Puede conservar el trabajo asíncrono de la misma manera que conserva el estado de la IU.

Para obtener más información, consulta Cómo usar corrutinas de Kotlin con componentes de la arquitectura de Android.

El ciclo de vida de un ViewModel

El ciclo de vida de un ViewModel está vinculado directamente a su alcance. Un ViewModel permanece en la memoria hasta que desaparece el ViewModelStoreOwner que determina su alcance. Esto puede ocurrir en los siguientes contextos:

  • En el caso de una actividad, cuando termina.
  • En el caso de un fragmento, cuando se desvincula.
  • En el caso de una entrada de Navigation, cuando se quita de la pila de actividades.

Esto hace que los ViewModels sean una gran solución para almacenar datos que sobreviven a cambios de configuración.

En la Figura 1, se muestran los distintos estados del ciclo de vida de una actividad a medida que atraviesa una rotación y hasta que termina. La ilustración también muestra el ciclo de vida del ViewModel junto al de la actividad asociada. Este diagrama en particular muestra los estados de una actividad. Los mismos estados básicos se aplican al ciclo de vida de un fragmento.

Muestra el ciclo de vida de un ViewModel a medida que una actividad cambia de estado.

Por lo general, solicitas un ViewModel la primera vez que el sistema llama al método onCreate() del objeto de una actividad. El sistema puede llamar a onCreate() varias veces durante la vida de una actividad, como cuando rota la pantalla de un dispositivo. El ViewModel existe desde la primera vez que solicitas un ViewModel hasta que finaliza la actividad y se destruye.

Cómo borrar dependencias de ViewModel

ViewModel llama al método onCleared cuando ViewModelStoreOwner lo destruye durante su ciclo de vida. Esto te permite limpiar cualquier trabajo o dependencia que siga el ciclo de vida de ViewModel.

En el siguiente ejemplo, se muestra una alternativa para viewModelScope. viewModelScope es un CoroutineScope integrado que sigue automáticamente el ciclo de vida de ViewModel. ViewModel lo usa para activar operaciones relacionadas con la empresa. Si deseas usar un alcance personalizado en lugar de viewModelScope para realizar pruebas con más facilidad, ViewModel puede recibir un objeto CoroutineScope como dependencia en su constructor. Cuando el ViewModelStoreOwner borra el ViewModel al final de su ciclo de vida, el ViewModel también cancela el CoroutineScope.

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

    // Other ViewModel logic ...

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

A partir de la versión 2.5 del ciclo de vida y versiones posteriores, puedes pasar uno o más objetos Closeable al constructor de ViewModel, que se cierra automáticamente cuando se borra la instancia de ViewModel.

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ácticas recomendadas

Las siguientes son varias prácticas recomendadas clave que debes seguir cuando implementes ViewModel:

  • Debido a su alcance, usa ViewModels como detalles de implementación de un contenedor de estado a nivel de pantalla. No los uses como contenedores de estado de componentes de IU reutilizables, como grupos de chips o formularios. De lo contrario, obtendrán la misma ViewModel en diferentes usos del mismo componente de IU con la misma ViewModelStoreOwner, a menos que uses una clave de modelo de vista explícita por chip.
  • Los ViewModels no deberían conocer los detalles de implementación de la IU. Mantén los nombres de los métodos que expone la API de ViewModel y los de los campos de estado de la IU lo más genéricos posible. De esta manera, ViewModel podrá admitir cualquier tipo de IU: un teléfono celular, un plegable, una tablet o incluso una Chromebook.
  • Como pueden tener una vida más larga que el ViewModelStoreOwner, los ViewModels no deberían contener ninguna referencia de APIs relacionadas con el ciclo de vida, como Context o Resources para evitar fugas de memoria.
  • No pases ViewModels a otras clases, funciones ni otros componentes de la IU. Debido a que la plataforma los administra, debes mantenerlos lo más cerca de ella como sea posible. Cerca de la función de componibilidad a nivel de la actividad, el fragmento o la pantalla. Esto evita que los componentes de nivel inferior accedan a más datos y lógica de los que necesitan.

Más información

A medida que los datos se hacen más complejos, quizás decidas tener una clase por separado solo para cargar los datos. El objetivo de ViewModel es encapsular los datos para un controlador de IU a fin de que estos sobrevivan a los cambios de configuración. Si quieres obtener más información para cargar, conservar y administrar datos durante cambios de configuración, consulta Estados de IU guardados.

La Guía sobre la arquitectura de apps para Android sugiere crear una clase de repositorio para controlar estas funciones.

Recursos adicionales

Para obtener más información sobre la clase ViewModel, consulta los siguientes recursos.

Documentación

Ejemplos