Compose-Status außerhalb der UI verwenden

State<T> und MutableState<T> von Jetpack Compose sind Werthalter für allgemeine Zwecke, die vom Compose-Snapshot-System unterstützt werden. Obwohl sie am häufigsten verwendet werden, um die Neuzusammensetzung der Benutzeroberfläche zu steuern, ist das Snapshot-System selbst nicht an das UI-Rendering gebunden.

Mit den AppState-Bibliotheken Transform und StateStore können Sie Compose-State in ViewModels, Managern, Repositories und Hintergrunddiensten speichern, ableiten, beobachten und persistent speichern.

Warum Compose State über die UI hinaus verwenden?

Wenn Sie Compose State in einem zustandsbeobachtenden Block lesen, z. B. in einer komponierbaren UI-Funktion, transform oder derivedStateOf, bietet die Compose-Laufzeit zwei wichtige Vorteile für alle Ebenen Ihrer Architektur:

  • Automatische und dynamische Abhängigkeitsverfolgung: Wenn der .value eines Status gelesen wird (oder die by-Attributdelegierung verwendet wird), wird dieser Status automatisch als Abhängigkeit des Blocks registriert. Da das Tracking dynamisch ist, werden Abhängigkeiten zur Laufzeit durch die ausgeführten Codepfade bestimmt. Wenn ein bedingter Zweig nicht ausgeführt wird (z. B. nach einer if-Prüfung oder einem vorzeitigen return), werden die Zustände innerhalb dieses Zweigs nicht gelesen und Änderungen daran lösen keine unnötigen Neuberechnungen aus.
  • Konsistenz und Granularität: Sie können separate, detaillierte MutableState-Attribute deklarieren und Schreibvorgänge mit mehreren Attributen in Snapshot.withMutableSnapshot verpacken. Zwischenupdates werden auf den änderbaren Snapshot beschränkt und atomar angewendet, wenn der Block abgeschlossen ist. Beobachter, die innerhalb eines Snapshots lesen, sehen also nie einen inkonsistenten Zwischenzustand und führen nur dann eine Neuberechnung durch, wenn sich die von ihnen gelesenen spezifischen Attribute ändern. Im Gegensatz zu StateFlow müssen Sie sich nicht zwischen einem einzelnen monolithischen StateFlow<UiState> (der jeden Collector ungültig macht, wenn sich ein Feld ändert) und separaten StateFlow-Eigenschaften (die nicht zusammen in einer einzelnen atomaren Transaktion aktualisiert werden können) entscheiden.

Die richtige State API für Ihre Architektur auswählen

Wählen Sie aus, wie Sie Compose-State speichern und beibehalten möchten, je nachdem, wie lange ein Zustand bestehen soll und welche Komponenten darauf zugreifen müssen:

Statuskategorie Lebensdauer und Umfang Beispiele Empfohlene Compose-State-APIs
Livestatus Wird immer im Arbeitsspeicher gespeichert, beim Start neu abgefragt und nicht über die Prozessbeendigung hinaus beibehalten. Status der Netzwerkverbindung, Status der Bluetooth-Verbindung, temporärer UI-Status wie die Auswahl von Elementen.
Vorübergehender UI-Status Übersteht Konfigurationsänderungen und vom System initiierte Prozessbeendigungen, wird nicht unbegrenzt gespeichert; auf einen einzelnen Bildschirm oder eine einzelne Aktivität beschränkt. Maximierte oder minimierte UI-Elemente, Scrollposition, Suchanfrage auf dem Bildschirm oder Filter-Umschaltflächen.
Vorübergehender Sitzungsstatus Überlebt die Beendigung von Hintergrundprozessen, wird nicht unbegrenzt gespeichert; wird von Aktivitäten, Diensten oder Widgets gemeinsam genutzt. Aktiver Fokus-Timer, mehrstufiger Einkaufswagen oder Formularentwurf, der gerade bearbeitet wird.
  • StateStore, gekapselt in einem Manager oder Repository
  • Schlüssel, die PersistToDataStore implementieren und mit syncToDataStore synchronisiert werden
Dauerhafter Zustand Werden unbegrenzt auf dem Laufwerk gespeichert. Nutzereinstellungen und -konfigurationen (DataStore), strukturierte App-Daten und Mediatheken (Room). DataStore- oder Room-Flows, die in transform mit collectAsState erfasst werden

Compose-Status in einem ViewModel verwenden

In einem ViewModel können Sie unabhängige Bildschirmeingaben als detaillierte MutableState-Properties deklarieren, sie mit SavedStateHandle.saveable über die Prozessbeendigung hinaus beibehalten, mehrere Properties mit Snapshot.withMutableSnapshot atomar aktualisieren und sie mit Repository-Flow-Streams in einem transform kombinieren:

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

Wichtige Muster in diesem ViewModel:

  • SavedStateHandle.saveable für reaktiven gespeicherten Zustand verwenden: Wenn Sie searchQuery mit mutableStateOf (aus androidx.lifecycle:lifecycle-viewmodel-compose) an SavedStateHandle.saveable delegieren, wird der Wert über die Prozessbeendigung hinaus als MutableState beibehalten, sodass transform Lesevorgänge automatisch verfolgt. Vermeiden Sie die Verwendung von SavedStateHandle.saved ohne weitere Angaben, da dadurch eine einfache Kotlin-Property anstelle von Compose-State zurückgegeben wird und bei Änderungen keine Neuzusammensetzung ausgelöst wird.
  • Atomare Schreibvorgänge für mehrere Properties mit Snapshot.withMutableSnapshot: Wenn Sie searchQuery und selectedTaskIds innerhalb von Snapshot.withMutableSnapshot aktualisieren, werden die Schreibvorgänge isoliert und beide Änderungen in einer einzigen atomaren Snapshot-Transaktion übernommen. Beobachter (einschließlich transform) sehen beide Aktualisierungen zusammen in einem einzigen Neuzusammensetzungsdurchlauf, anstatt das neue searchQuery mit dem veralteten selectedTaskIds zu sehen.
  • Flüsse über Neukompositionen hinweg mit remember(key) wiederverwenden: Wenn Sie tasksRepository.getTasks(searchQuery) in remember mit searchQuery als Schlüssel verpacken, wird bei einer Änderung von selectedTaskIds und einer Neukomposition des transform-Blocks die vorhandene Datenbank-Flow wiederverwendet, anstatt die Abfrage neu zu starten.

Live-Systemstatus in der gesamten App teilen

Für den Live-Systemstatus, der von mehreren ViewModels, Aktivitäten oder Diensten gemeinsam genutzt wird, z. B. den Status der Netzwerk- oder Bluetooth-Verbindung, sollten Sie keine veralteten Statusdaten über die Prozessbeendigung hinaus auf dem Laufwerk speichern. Fragen Sie stattdessen beim Start den aktuellen Systemstatus ab, speichern Sie ihn in einer privaten mutableStateOf-Property in einem Singleton mit Anwendungsbereich und stellen Sie einen schreibgeschützten State<T> bereit:

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 unterstützt konsistente Lesevorgänge und Aktualisierungen von jedem Thread aus, sodass NetworkCallback _isOnline.value direkt über den Hintergrund-Callback-Thread aktualisieren kann. Wenn ein Hintergrund-Callback mehrere MutableState-Eigenschaften gleichzeitig aktualisiert, umschließen Sie die Schreibvorgänge mit Snapshot.withMutableSnapshot, damit sie atomar angewendet werden.

Jede Komponente in Ihrer App kann networkMonitor.isOnline.value lesen – entweder als einmalige Prüfung, innerhalb einer UI-Komposition oder innerhalb eines Blocks, der den Status beobachtet, z. B. listener, derivedStateOf oder transform. Beispiel: TaskListViewModel kann networkMonitor.isOnline.value im Block transform lesen:

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

Da transform networkMonitor.isOnline.value zusammen mit searchQuery, selectedTaskIds und dem Repository Flow verfolgt, wird bei jeder Änderung der Konnektivität TaskListState.Success automatisch neu berechnet, ohne dass die Datenbank erneut abgefragt werden muss.