Usa el estado de Compose fuera de la IU

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 .value de un estado (o usar la delegación de propiedades by) 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 de if o un return anticipado), 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 MutableState individuales y detalladas, y unir escrituras de varias propiedades en Snapshot.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 de StateFlow, no tienes que elegir entre un solo StateFlow<UiState> monolítico (que invalida cada recopilador cuando cambia cualquier campo) y propiedades StateFlow independientes (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.saveable para el estado guardado reactivo: Delegar searchQuery a SavedStateHandle.saveable con mutableStateOf (de androidx.lifecycle:lifecycle-viewmodel-compose) conserva el valor durante el cierre del proceso como un MutableState, por lo que transform hace un seguimiento de las lecturas automáticamente. Evita usar SavedStateHandle.saved sin formato, que devuelve una propiedad de Kotlin simple en lugar de State de Compose y no activa la recomposición cuando se cambia.
  • Escrituras atómicas de varias propiedades con Snapshot.withMutableSnapshot: Actualizar searchQuery y selectedTaskIds dentro de Snapshot.withMutableSnapshot aísla las escrituras y confirma ambos cambios en una sola transacción de instantánea atómica, de modo que los observadores (incluido transform) vean ambas actualizaciones juntas en un pase de recomposición en lugar de observar el nuevo searchQuery vinculado con el selectedTaskIds inactivo.
  • Reutiliza flujos en las recomposiciones con remember(key): Unir tasksRepository.getTasks(searchQuery) en remember con searchQuery como clave garantiza que, cuando selectedTaskIds cambie y recomponga el bloque transform, Compose reutilice el Flow de 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.