StateStore   Parte de Android Jetpack.

StateStore es un contenedor con seguridad de tipos que almacena y expone el estado compartido de la aplicación como State de Compose en toda la app.

Casos de uso

Si creas un StateStore con alcance de aplicación, puedes compartir el estado entre los componentes de la aplicación dentro del mismo proceso:

  • Varias actividades: Si tu app muestra varias actividades de forma simultánea (como una actividad de Recibidos y una actividad de editor en un cliente de correo electrónico) o tiene una actividad que admite instancias múltiples (como una actividad de visor de documentos), puedes usar StateStore para compartir información entre ellas.
  • Actividad y servicio: Sincroniza el estado entre una actividad y un CarAppService o una AppFunction en segundo plano.
  • Actividad y proveedor de contenido o widget: Comparte el estado entre una actividad y un widget de la pantalla principal.

Si un singleton solo necesita State<T> en la memoria sin búsqueda con clave ni restablecimiento de cierre del proceso respaldado por DataStore, puedes exponer las propiedades de mutableStateOf directamente en el singleton. Para obtener una comparación de las APIs de estado en tu app, consulta Elige la API de estado adecuada para tu arquitectura.

Descripción general de la API

El paquete androidx.appstate.statestore proporciona las clases StateStore y StateStoreKey:

package androidx.appstate.statestore

import androidx.compose.runtime.State
import kotlinx.serialization.Serializable

public class StateStore {
    public val keys: Set<StateStoreKey<*>>
    public fun <T> getState(
        stateKey: StateStoreKey<T>,
        defaultValue: T = stateKey.defaultValue,
    ): State<T>
    public fun <T> setState(stateKey: StateStoreKey<T>, value: T)
    public inline fun <T> updateState(
        stateKey: StateStoreKey<T>,
        defaultValue: T = stateKey.defaultValue,
        update: (T) -> T,
    )
}

@Serializable
public open class StateStoreKey<T>(
    public val defaultValue: T,
)

Define tu estado

Para garantizar la seguridad de tipos cuando se almacenan y recuperan valores, StateStore usa la clase StateStoreKey para definir las claves que se usan para leer y escribir valores. Mantén las definiciones de StateStoreKey private o internal en el administrador, el repositorio o el módulo que posee esa porción de estado:

@Serializable
sealed interface FocusSession {
    @Serializable
    data object Idle : FocusSession

    @Serializable
    data class Running(
        val taskId: String,
        val endTimeMillis: Long,
    ) : FocusSession

    @Serializable
    data class Paused(
        val taskId: String,
        val remainingMillis: Long,
    ) : FocusSession
}

@Serializable
private data object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)

Crea e inserta instancias de StateStore

StateStore es una clase normal de la que creas una instancia una vez en el alcance de la aplicación o del módulo (por ejemplo, en tu clase Application o en un módulo de inserción de dependencias) y que insertas en los administradores o repositorios que leen y escriben el estado, en lugar de exponer StateStore directamente a las clases de la IU:

// Instantiate at the application or DI module level
val stateStore = StateStore()

// Inject into managers or repositories that encapsulate their own keys
class FocusSessionManager(
    private val stateStore: StateStore,
)

Estado de lectura y escritura

Dentro de tu administrador o repositorio, lee, escribe y actualiza valores en el StateStore insertado con tus definiciones privadas de StateStoreKey, y expón propiedades de State<T> de solo lectura y funciones de mutación a los emisores.

Estado de lectura

Usa getState(stateKey, defaultValue) para recuperar un objeto State<T>. Si la clave aún no se inicializó, StateStore la propaga con el parámetro defaultValue, que se establece de forma predeterminada en stateKey.defaultValue (o puedes pasar un defaultValue personalizado cuando inicialices una clave sin propagar). Debido a que getState devuelve un State<T>, los ViewModels, los elementos componibles de la IU, los widgets y los bloques transform pueden leer la propiedad del administrador de forma reactiva:

class FocusSessionManager(
    private val stateStore: StateStore,
) {
    val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
}

class FocusTimerViewModel(
    focusSessionManager: FocusSessionManager,
) : ViewModel() {
    val session: State<FocusSession> = focusSessionManager.session
}

@Composable
fun FocusBanner(viewModel: FocusTimerViewModel = viewModel()) {
    // Reads state reactively; recomposes when FocusSessionKey updates
    when (val session = viewModel.session.value) {
        FocusSession.Idle -> Text(text = "No active focus session")
        is FocusSession.Running -> Text(text = "Focusing on ${session.taskId}")
        is FocusSession.Paused -> Text(text = "Paused: ${session.taskId}")
    }
}

Escribir estado

Usa setState(stateKey, value) dentro de tu administrador para inicializar o reemplazar el valor asociado con una clave:

class FocusSessionManager(
    private val stateStore: StateStore,
) {
    val session: State<FocusSession> = stateStore.getState(FocusSessionKey)

    fun startFocus(taskId: String, endTimeMillis: Long) {
        stateStore.setState(
            FocusSessionKey,
            FocusSession.Running(
                taskId = taskId,
                endTimeMillis = endTimeMillis,
            ),
        )
    }

    fun endFocus() {
        stateStore.setState(FocusSessionKey, FocusSession.Idle)
    }
}

Actualizar estado

Si necesitas actualizar un estado en función de su valor actual, usa updateState. Esto recupera el valor actual (o usa el predeterminado si no se estableció) y aplica tu lambda de actualización:

class FocusSessionManager(
    private val stateStore: StateStore,
) {
    val session: State<FocusSession> = stateStore.getState(FocusSessionKey)

    fun pauseFocus(nowMillis: Long) {
        stateStore.updateState(FocusSessionKey) { current ->
            if (current is FocusSession.Running) {
                FocusSession.Paused(
                    taskId = current.taskId,
                    remainingMillis = (current.endTimeMillis - nowMillis)
                        .coerceAtLeast(0L),
                )
            } else {
                current
            }
        }
    }

    fun resumeFocus(nowMillis: Long) {
        stateStore.updateState(FocusSessionKey) { current ->
            if (current is FocusSession.Paused) {
                FocusSession.Running(
                    taskId = current.taskId,
                    endTimeMillis = nowMillis + current.remainingMillis,
                )
            } else {
                current
            }
        }
    }
}

Integraciones

StateStore se integra en DataStore de Jetpack para conservar y restablecer el estado en los cierres de procesos y los reinicios de apps.

DataStore

El artefacto androidx.appstate.statestore:statestore-datastore te permite configurar la persistencia una vez en tu instancia compartida de StateStore para que las claves persistentes de cualquier administrador se guarden y restablezcan automáticamente desde el disco con DataStore.

El paquete androidx.appstate.datastore proporciona las siguientes APIs:

package androidx.appstate.datastore

import androidx.appstate.statestore.StateStore
import androidx.datastore.core.DataStore
import kotlinx.coroutines.CoroutineScope

public interface PersistToDataStore

public suspend fun StateStore.syncToDataStore(
    path: String,
    coroutineScope: CoroutineScope,
)

public suspend fun StateStore.syncToDataStore(
    dataStore: DataStore<StateStorePreferences>,
)
  1. Implementa PersistToDataStore: Marca cualquier StateStoreKey que deba conservarse implementando la interfaz PersistToDataStore:

@Serializable
private data object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
    PersistToDataStore

  1. Vincula StateStore a DataStore: Llama a syncToDataStore una vez cuando configures tu instancia compartida de StateStore (por ejemplo, en tu clase Application o módulo de inserción de dependencias). Como syncToDataStore es una función suspend que se suspende de forma indefinida para restablecer los valores guardados del disco y seguir guardando los cambios futuros en el coroutineScope proporcionado, llámala dentro de launch y pasa tu CoroutineScope a nivel de la aplicación. En el inicio, getState muestra inicialmente defaultValue en la memoria y actualiza State una vez que DataStore termina de leer desde el disco:

class ProductivityApplication : Application() {
    val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    val stateStore = StateStore().also { store ->
        // Restore saved values and automatically persist future changes
        // for all PersistToDataStore keys across managers
        applicationScope.launch {
            store.syncToDataStore(
                path = dataStoreFile("statestore.json").absolutePath,
                coroutineScope = applicationScope,
            )
        }
    }

    val focusSessionManager = FocusSessionManager(stateStore)
}

Lineamientos de persistencia de DataStore

Debido a que syncToDataStore serializa y escribe en el disco cada vez que cambia el valor de una clave PersistToDataStore, sigue estos lineamientos cuando elijas qué conservar y cuándo hacerlo:

  • Guarda las marcas de tiempo de inicio o finalización en lugar de cada tic del temporizador: Para los temporizadores o el progreso de la reproducción, conserva un valor de referencia estable (como endTimeMillis o startedAtMillis) en lugar de actualizar una clave PersistToDataStore cada segundo o fotograma, y calcula la visualización transcurrida o restante en vivo en la IU.
  • Estabiliza la entrada rápida del usuario antes de actualizar una clave persistente: Para los borradores de texto, las búsquedas o los controles deslizantes, mantén la entrada inmediata en un MutableState en la memoria (o un StateStoreKey que no implemente PersistToDataStore) y solo llama a setState en el StateStoreKey persistente después de que se establezca la entrada.
  • Mantén las claves persistentes ligeras: Todas las claves PersistToDataStore de un StateStore se serializan juntas en un solo archivo en cada actualización. Conserva el estado de sesión compacto, como los IDs activos, las selecciones de filtros o los borradores de formularios en curso, y mantén las colecciones grandes o los conjuntos de datos consultables en Room.