StateStore   Parte do Android Jetpack.

StateStore é um contêiner com segurança de tipos que armazena e expõe o estado compartilhado do aplicativo como State do Compose em todo o app.

Casos de uso

Ao criar um StateStore com escopo de aplicativo, é possível compartilhar o estado entre componentes de aplicativo no mesmo processo:

  • Várias atividades: se o app mostrar várias atividades simultaneamente (como uma atividade de caixa de entrada e uma de editor em um cliente de e-mail) ou tiver uma atividade que ofereça suporte a várias instâncias (como uma atividade de visualizador de documentos), use StateStore para compartilhar informações entre elas.
  • Atividade e serviço: sincronize o estado entre uma atividade e um CarAppService em segundo plano ou uma AppFunction.
  • Atividade e provedor de conteúdo ou widget: compartilhe o estado entre uma atividade e um widget da tela inicial.

Se um singleton só precisar de State<T> na memória sem pesquisa com chave ou restauração de encerramento do processo com suporte do DataStore, exponha as propriedades mutableStateOf diretamente no singleton. Para uma comparação das APIs de estado no seu app, consulte Escolher a API de estado certa para sua arquitetura.

Visão geral da API

O pacote androidx.appstate.statestore fornece as classes StateStore e 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,
)

Defina seu estado

Para garantir a segurança de tipos ao armazenar e recuperar valores, o StateStore usa a classe StateStoreKey para definir as chaves usadas para ler e gravar valores. Mantenha as definições de StateStoreKey private ou internal no gerenciador, repositório ou módulo que possui essa fração 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)

Criar e injetar instâncias de StateStore

StateStore é uma classe comum que você instancia uma vez no escopo do aplicativo ou do módulo (por exemplo, na classe Application ou em um módulo de injeção de dependência) e injeta nos gerenciadores ou repositórios que leem e gravam o estado, em vez de expor StateStore diretamente às classes da interface:

// 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,
)

Ler e gravar estado

No gerenciador ou repositório, leia, grave e atualize valores no StateStore injetado usando as definições particulares de StateStoreKey e exponha propriedades State<T> somente leitura e funções de mutação para os autores da chamada.

Estado de leitura

Use getState(stateKey, defaultValue) para recuperar um objeto State<T>. Se a chave ainda não tiver sido inicializada, StateStore vai preenchê-la com o parâmetro defaultValue, que tem como padrão stateKey.defaultValue. Você também pode transmitir um defaultValue personalizado ao inicializar uma chave não preenchida. Como getState retorna um State<T>, ViewModels, elementos combináveis de IU, widgets e blocos transform podem ler a propriedade do gerenciador de forma reativa:

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}")
    }
}

Estado de gravação

Use setState(stateKey, value) dentro do gerenciador para inicializar ou substituir o valor associado a uma chave:

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)
    }
}

Estado de atualização

Se você precisar atualizar um estado com base no valor atual, use updateState. Isso recupera o valor atual (ou usa o padrão, se não estiver definido) e aplica sua lambda de atualização:

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
            }
        }
    }
}

Integrações

O StateStore se integra ao DataStore do Jetpack para manter e restaurar o estado em encerramentos de processos e reinicializações de apps.

DataStore

O artefato androidx.appstate.statestore:statestore-datastore permite configurar a persistência uma vez na instância compartilhada de StateStore para que as chaves persistentes de qualquer gerenciador sejam salvas e restauradas automaticamente do disco usando o DataStore.

O pacote androidx.appstate.datastore oferece as seguintes 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. Implementar PersistToDataStore: marque qualquer StateStoreKey que precise ser persistido implementando a interface PersistToDataStore:

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

  1. Vincular StateStore ao DataStore: chame syncToDataStore uma vez ao configurar sua instância compartilhada de StateStore (por exemplo, na classe Application ou no módulo de injeção de dependência). Como syncToDataStore é uma função suspend que é suspensa indefinidamente para restaurar valores salvos do disco e continuar salvando mudanças futuras no coroutineScope fornecido, chame-a dentro de launch e transmita o CoroutineScope no nível do aplicativo. Na inicialização, o getState retorna inicialmente defaultValue na memória e atualiza o State quando o DataStore termina de ler do 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)
}

Diretrizes de persistência do DataStore

Como o syncToDataStore serializa e grava no disco sempre que o valor de uma chave PersistToDataStore muda, siga estas diretrizes ao escolher o que e quando persistir:

  • Salve os carimbos de data/hora de início ou término em vez de cada tique do timer: para timers ou progresso de reprodução, mantenha um valor de referência estável (como endTimeMillis ou startedAtMillis) em vez de atualizar uma chave PersistToDataStore a cada segundo ou frame e calcule o tempo decorrido ou restante na interface.
  • Evite entradas rápidas do usuário antes de atualizar uma chave persistente: para rascunhos de texto, consultas de pesquisa ou controles deslizantes, mantenha a entrada imediata em um MutableState na memória (ou um StateStoreKey que não implemente PersistToDataStore) e só chame setState no StateStoreKey persistente depois que a entrada for concluída.
  • Mantenha as chaves persistentes leves: todas as chaves PersistToDataStore em um StateStore são serializadas juntas em um único arquivo em cada atualização. Mantenha o estado compacto da sessão, como IDs ativos, seleções de filtro ou rascunhos de formulários em andamento, e mantenha grandes coleções ou conjuntos de dados consultáveis no Room.