¿Cómo usar viewmodel en Compose?

ViewModel en Compose: La Guía Esencial

13/09/2021

Valoración: 4.49 (4015 votos)

En el ecosistema de desarrollo moderno de Android, especialmente con la llegada de Jetpack Compose, la gestión del estado de la interfaz de usuario (UI) se ha convertido en una pieza central. Perder los datos de un usuario simplemente porque ha girado el teléfono es una experiencia inaceptable. Aquí es donde la clase ViewModel, parte fundamental de Android Jetpack, entra en escena como una solución elegante y robusta. Este componente no es solo un contenedor de datos; es el cerebro que orquesta la lógica de negocio a nivel de pantalla y garantiza que el estado de la UI sobreviva a los cambios de configuración, haciendo nuestras aplicaciones más resilientes y profesionales.

¿Cómo usar viewmodel en Compose?
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.

Si alguna vez te has enfrentado al desafío de mantener la información en pantalla tras una rotación o al navegar entre diferentes secciones de tu app, entender y aplicar correctamente el patrón ViewModel cambiará por completo tu forma de desarrollar. Esta guía te llevará desde los conceptos básicos hasta las mejores prácticas, mostrándote cómo integrar ViewModel en tus proyectos de Jetpack Compose de manera efectiva.

Índice de Contenido

¿Qué es y Para Qué Sirve un ViewModel?

En su forma más simple, un ViewModel es una clase diseñada para almacenar y administrar datos relacionados con la interfaz de usuario de una manera consciente del ciclo de vida. Actúa como un puente entre la capa de datos (como los repositorios) y la capa de UI (tus Composables, Activities o Fragments). Su propósito principal es doble y resuelve dos de los problemas más comunes en el desarrollo de Android.

  • Permite conservar el estado de la UI: Su principal ventaja es que sobrevive a los cambios de configuración. Cuando un usuario rota la pantalla, la `Activity` se destruye y se vuelve a crear. Sin un ViewModel, todos los datos de la UI almacenados en esa `Activity` se perderían. Un ViewModel, sin embargo, permanece en memoria, permitiendo que la nueva instancia de la `Activity` se conecte a él y recupere el estado inmediatamente.
  • Proporciona acceso a la lógica de negocio: El ViewModel es el lugar ideal para alojar la lógica que transforma los datos crudos de las capas inferiores en un estado que la UI pueda consumir directamente. Por ejemplo, puede combinar datos de diferentes fuentes, formatearlos o manejar las interacciones del usuario (como clics en botones) y delegar las acciones correspondientes.

Al centralizar la lógica y el estado de la pantalla en un ViewModel, logramos una arquitectura más limpia y mantenible, siguiendo el principio de separación de responsabilidades. La UI se encarga únicamente de mostrar los datos y notificar eventos, mientras que el ViewModel se encarga de prepararlos y procesarlos.

El Dúo Dinámico: Alcance (Scope) y Persistencia

Para entender el poder del ViewModel, es crucial comprender dos conceptos: su alcance y cómo logra la persistencia.

Alcance (Scope) del ViewModel

Un ViewModel no vive para siempre. Su ciclo de vida está ligado a un `ViewModelStoreOwner`. Este es un componente con un ciclo de vida definido, como una `Activity`, un `Fragment` o, en el mundo de la navegación, un `NavBackStackEntry` (que representa un destino en el gráfico de navegación). El ViewModel permanecerá en memoria mientras su `ViewModelStoreOwner` esté activo. Cuando el owner es destruido de forma permanente (por ejemplo, cuando el usuario cierra la `Activity` o navega hacia atrás de forma definitiva desde un destino), el ViewModel también es destruido y su método `onCleared()` es llamado para liberar recursos.

Persistencia ante Cambios de Configuración

La "magia" ocurre durante los cambios de configuración. Aunque la `Activity` se destruye, el sistema operativo entiende que es una destrucción temporal. Por lo tanto, el `ViewModelStoreOwner` y, en consecuencia, el ViewModel asociado, no se destruyen. Cuando la nueva instancia de la `Activity` se crea, solicita un ViewModel del mismo tipo y el sistema le entrega la instancia existente, con todos sus datos intactos. Esto evita tener que realizar costosas llamadas de red o a la base de datos de nuevo.

Para una persistencia aún mayor, que sobrevive incluso a la finalización del proceso de la aplicación por parte del sistema, se utiliza el `SavedStateHandle`. Este mecanismo permite al ViewModel guardar y restaurar su estado, garantizando que la UI se mantenga intacta incluso si el usuario vuelve a la app después de mucho tiempo.

Integración Perfecta con Jetpack Compose

Con Jetpack Compose, el uso de ViewModel se vuelve aún más natural e indispensable. En una arquitectura con Compose, el ViewModel es el principal proveedor del estado de la pantalla para las funciones Composable. Sin embargo, hay una regla de oro que siempre debes recordar.

Un ViewModel NUNCA debe tener como ámbito un Composable.

¿Por qué? Los Composables no son `ViewModelStoreOwner`. Su ciclo de vida es dinámico y pueden ser recompuestos (redibujados) muchas veces por segundo. Si asociaras un ViewModel a un Composable, podrías terminar con comportamientos inesperados, como múltiples instancias del mismo Composable compartiendo el mismo ViewModel. La práctica correcta es alojar tus pantallas de Compose dentro de un `Fragment`, una `Activity`, o utilizar la librería `Compose Navigation`, y asociar el ViewModel a estos contenedores que sí son `ViewModelStoreOwner`.

Puedes obtener una instancia de tu ViewModel dentro de un Composable de forma muy sencilla utilizando la función de extensión `viewModel()` de la librería `androidx.lifecycle:lifecycle-viewmodel-compose`.

Manos a la Obra: Implementando un ViewModel Paso a Paso

Veamos un ejemplo práctico para una pantalla que lanza dos dados. Queremos mantener el valor de los dados y el número de lanzamientos incluso si el usuario rota el dispositivo.

Paso 1: Definir el Estado de la UI (UI State)

Es una buena práctica crear una clase de datos (`data class` en Kotlin) que represente todo el estado necesario para dibujar la pantalla. Esto favorece un flujo de datos unidireccional.

// Representa el estado completo de la pantalla de lanzamiento de dados data class DiceUiState( val firstDieValue: Int? = null, val secondDieValue: Int? = null, val numberOfRolls: Int = 0, )

Paso 2: Crear el ViewModel

El ViewModel contendrá el estado y la lógica para modificarlo. Usaremos `StateFlow` para exponer el estado a la UI de forma reactiva.

import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlin.random.Random class DiceRollViewModel: ViewModel() { // Flujo privado y mutable para que solo el ViewModel pueda modificarlo private val _uiState = MutableStateFlow(DiceUiState()) // Flujo público e inmutable para que la UI lo observe val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow() // Lógica de negocio para lanzar los dados y actualizar el estado 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, ) } } }

Paso 3: Consumir el ViewModel en el Composable

Finalmente, en nuestra pantalla de Compose, obtenemos la instancia del ViewModel y observamos su estado. La librería `lifecycle-runtime-compose` nos provee `collectAsStateWithLifecycle` para una recolección segura del flujo.

import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun DiceRollScreen( // Obtiene la instancia del ViewModel. Compose se encarga de asociarlo al scope correcto. viewModel: DiceRollViewModel = viewModel() ) { // Recolecta el estado del StateFlow. Cuando el estado cambia, el Composable se recompone. val uiState by viewModel.uiState.collectAsStateWithLifecycle() // Aquí iría el código de tu UI que usa uiState para mostrar los valores // y llama a viewModel.rollDice() en un evento de clic. // Ejemplo: // Column { // Text("Lanzamientos: ${uiState.numberOfRolls}") // Text("Dado 1: ${uiState.firstDieValue ?: '?'}") // Text("Dado 2: ${uiState.secondDieValue ?: '?'}") // Button(onClick = { viewModel.rollDice() }) { // Text("Lanzar Dados") // } // } }

Tabla Comparativa: ViewModel vs. Otras Alternativas

Es útil comparar el ViewModel con otras formas de gestionar el estado para entender cuándo usar cada una.

CaracterísticaViewModelClase SimplerememberSaveable
Sobrevive a rotaciónNo
Sobrevive a muerte de procesoSolo con SavedStateHandleNo
Manejo de lógica de negocioIdeal para elloPosible, pero sin gestión de ciclo de vidaNo es su propósito
Separación de responsabilidadesExcelentePobre (mezcla con la UI)No aplica (estado de UI)
Ámbito de usoNivel de pantallaCualquieraDentro de un Composable

Mejores Prácticas y Consejos Clave

  • No referenciar vistas ni el Contexto: Un ViewModel nunca debe tener una referencia a una `Activity`, `Fragment` o `Context`. Esto crearía una fuga de memoria, ya que el ViewModel puede vivir más que la vista. Si necesitas un `Context`, utiliza la clase `AndroidViewModel` que lo provee de forma segura.
  • Usa Corrutinas con `viewModelScope`: Para operaciones asíncronas como llamadas a una API, usa el `viewModelScope`. Es un `CoroutineScope` que se cancela automáticamente cuando el ViewModel es destruido, evitando trabajos en segundo plano innecesarios y fugas de recursos.
  • Mantén los ViewModels enfocados: Un ViewModel debe ser responsable del estado de una sola pantalla o de una parte lógica de la UI. Evita crear ViewModels monolíticos que manejen toda la aplicación.
  • No pases ViewModels como parámetros: Evita pasar la instancia del ViewModel a Composables anidados. En su lugar, pasa solo los datos que necesitan y las funciones lambda para notificar eventos hacia arriba. Esto mejora la reutilización y el testing de tus Composables.

Preguntas Frecuentes (FAQ)

¿Puedo usar más de un ViewModel por pantalla?

Sí, es técnicamente posible. Sin embargo, suele ser una señal de que la responsabilidad de la pantalla podría dividirse mejor. La práctica común es tener un solo ViewModel que exponga un único objeto de estado (`UiState`) para toda la pantalla. Si la pantalla es muy compleja, podrías considerar dividirla en múltiples fragmentos o destinos de navegación, cada uno con su propio ViewModel.

¿Cómo paso parámetros a un ViewModel?

Si tu ViewModel necesita parámetros en su constructor (como un ID de usuario), no puedes instanciarlo directamente. Necesitarás crear una `ViewModelProvider.Factory`. Sin embargo, la forma moderna y recomendada de manejar esto es mediante la inyección de dependencias con librerías como Hilt o Koin, que simplifican enormemente este proceso.

¿ViewModel reemplaza a la capa de datos (Repositorios)?

No, en absoluto. El ViewModel no debe obtener datos directamente de la red o la base de datos. Esa es la responsabilidad de la capa de datos (clases Repositorio). El ViewModel actúa como un cliente de la capa de datos; le solicita la información y luego la adapta para que la UI la pueda consumir fácilmente.

En resumen, el ViewModel es una herramienta indispensable en el arsenal de cualquier desarrollador Android. Su capacidad para gestionar el estado de la UI de forma consciente del ciclo de vida, junto con su perfecta integración con Jetpack Compose y las corrutinas, lo convierte en la piedra angular para construir aplicaciones modernas, eficientes y, sobre todo, robustas.

Si quieres conocer otros artículos parecidos a ViewModel en Compose: La Guía Esencial puedes visitar la categoría Juegos.

Subir