State<T> y MutableState<T> de Jetpack Compose son contenedores de valores de uso general respaldados por el sistema de instantáneas de Compose. Si bien se usan con mayor frecuencia para impulsar la recomposición de la IU, el sistema de instantáneas en sí no está vinculado a la renderización de la IU.
Con las bibliotecas de AppState, Transform y StateStore, puedes conservar, derivar, observar y mantener el State de Compose en ViewModels, administradores, repositorios y servicios en segundo plano.
¿Por qué usar el estado de Compose más allá de la IU?
Cuando lees State de Compose dentro de un bloque de observación de estado, como un elemento componible de la IU, transform o derivedStateOf, el entorno de ejecución de Compose proporciona dos beneficios clave en cualquier capa de tu arquitectura:
- Seguimiento automático y dinámico de dependencias: Leer el
.valuede un estado (o usar la delegación de propiedadesby) registra automáticamente ese estado como una dependencia del bloque. Debido a que el seguimiento es dinámico, las dependencias se determinan en el entorno de ejecución según las instrucciones de código que se ejecutan. Si no se toma una rama condicional (por ejemplo, después de una verificación deifo unreturnanticipado), no se leen los estados dentro de esa rama y los cambios en ellos no activan nuevos cálculos innecesarios. - Coherencia y nivel de detalle: Puedes declarar propiedades
MutableStateindividuales y detalladas, y unir escrituras de varias propiedades enSnapshot.withMutableSnapshot. Las actualizaciones intermedias se aíslan en la instantánea mutable y se aplican de forma atómica cuando se completa el bloque, por lo que los observadores que leen dentro de una instantánea nunca ven un estado intermedio incoherente y solo vuelven a calcular cuando cambian las propiedades específicas que leen. A diferencia deStateFlow, no tienes que elegir entre un soloStateFlow<UiState>monolítico (que invalida cada recopilador cuando cambia cualquier campo) y propiedadesStateFlowindependientes (que no se pueden actualizar juntas en una sola transacción atómica).
Elige la API de estado adecuada para tu arquitectura
Elige cómo conservar y mantener el State de Compose según el tiempo que deba durar un fragmento de estado y qué componentes deban acceder a él:
| Categoría de estado | Ciclo de vida y alcance | Ejemplos | APIs de Compose State recomendadas |
|---|---|---|---|
| Estado en vivo | Siempre se almacena en la memoria, se consulta de forma nueva en el inicio y no se conserva tras el cierre del proceso. | Estado de la conexión de red, estado de la conexión Bluetooth, estado temporal de la IU, como la selección de elementos. |
|
| Estado transitorio de la IU | Sobrevive a los cambios de configuración y al cierre del proceso iniciado por el sistema, no se almacena de forma indefinida y tiene alcance para una sola pantalla o actividad. | Elementos de la IU expandidos o contraídos, posición de desplazamiento, búsqueda en pantalla o activación de filtros. |
|
| Estado de sesión transitorio | Se mantiene tras el cierre de procesos en segundo plano, no se almacena de forma indefinida y se comparte entre actividades, servicios o widgets. | Temporizador de enfoque activo, carrito de confirmación de la compra de varios pasos o borrador de formulario en curso. |
|
| Estado permanente | Se conservan en el disco de forma indefinida. | Preferencias y parámetros de configuración del usuario (DataStore), datos de app estructurados y bibliotecas de contenido multimedia (Room). | Flujos de DataStore o Room recopilados dentro de transform con collectAsState |
Cómo usar el estado de Compose en un ViewModel
Dentro de un ViewModel, puedes declarar entradas de pantalla independientes como propiedades MutableState detalladas, conservarlas durante el cierre del proceso con SavedStateHandle.saveable, actualizar varias propiedades de forma atómica con Snapshot.withMutableSnapshot y combinarlas con flujos de repositorio Flow dentro de un transform:
sealed interface TaskListState { data object Loading : TaskListState data class Success( val tasks: List<Task>, val selectedTaskIds: Set<String>, val isOnline: Boolean = true, ) : TaskListState } @HiltViewModel class TaskListViewModel @Inject constructor( private val tasksRepository: TasksRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet<String>()) private set var searchQuery by savedStateHandle.saveable { mutableStateOf("") } private set fun updateSearchQuery(query: String) { Snapshot.withMutableSnapshot { searchQuery = query selectedTaskIds = emptySet() } } val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, ) } }
Patrones clave en este ViewModel:
- Usa
SavedStateHandle.saveablepara el estado guardado reactivo: DelegarsearchQueryaSavedStateHandle.saveableconmutableStateOf(deandroidx.lifecycle:lifecycle-viewmodel-compose) conserva el valor durante el cierre del proceso como unMutableState, por lo quetransformhace un seguimiento de las lecturas automáticamente. Evita usarSavedStateHandle.savedsin formato, que devuelve una propiedad de Kotlin simple en lugar deStatede Compose y no activa la recomposición cuando se cambia. - Escrituras atómicas de varias propiedades con
Snapshot.withMutableSnapshot: ActualizarsearchQueryyselectedTaskIdsdentro deSnapshot.withMutableSnapshotaísla las escrituras y confirma ambos cambios en una sola transacción de instantánea atómica, de modo que los observadores (incluidotransform) vean ambas actualizaciones juntas en un pase de recomposición en lugar de observar el nuevosearchQueryvinculado con elselectedTaskIdsinactivo. - Reutiliza flujos en las recomposiciones con
remember(key): UnirtasksRepository.getTasks(searchQuery)enrememberconsearchQuerycomo clave garantiza que, cuandoselectedTaskIdscambie y recomponga el bloquetransform, Compose reutilice elFlowde la base de datos existente en lugar de reiniciar la consulta.
Comparte el estado del sistema en vivo en toda tu app
En el caso del estado del sistema en vivo que se comparte entre varios ViewModels, actividades o servicios, como el estado de la conexión de red o Bluetooth, no querrás conservar el estado inactivo en el disco durante el cierre del proceso. En su lugar, consulta el estado actual del sistema durante el inicio, mantenlo en una propiedad mutableStateOf privada dentro de un singleton con alcance de aplicación y expón un State<T> de solo lectura:
interface NetworkMonitor { val isOnline: State<Boolean> } @Singleton class LiveNetworkMonitor @Inject constructor( @ApplicationContext context: Context, ) : NetworkMonitor { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) private val _isOnline = mutableStateOf(checkCurrentConnectivity()) override val isOnline: State<Boolean> = _isOnline private val callback = object : ConnectivityManager.NetworkCallback() { override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities, ) { _isOnline.value = networkCapabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } override fun onLost(network: Network) { _isOnline.value = false } } init { connectivityManager.registerDefaultNetworkCallback(callback) } private fun checkCurrentConnectivity(): Boolean { val network = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } }
MutableState admite lecturas y actualizaciones coherentes desde cualquier subproceso, por lo que NetworkCallback puede actualizar _isOnline.value directamente desde su subproceso de devolución de llamada en segundo plano. Si una devolución de llamada en segundo plano actualiza varias propiedades de MutableState juntas, une las escrituras en Snapshot.withMutableSnapshot para que se apliquen de forma atómica.
Cualquier componente de tu app puede leer networkMonitor.isOnline.value, ya sea como una verificación única, dentro de un elemento componible de la IU o dentro de un bloque de observación de estado, como listener, derivedStateOf o transform. Por ejemplo, TaskListViewModel puede leer networkMonitor.isOnline.value dentro de su bloque transform:
val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, isOnline = networkMonitor.isOnline.value, ) }
Debido a que transform hace un seguimiento de networkMonitor.isOnline.value junto con searchQuery, selectedTaskIds y el repositorio Flow, cualquier cambio en la conectividad vuelve a calcular automáticamente TaskListState.Success sin volver a consultar la base de datos.