Transformieren Teil von Android Jetpack
Transform ist eine Bibliothek, die auf der Compose-Laufzeit basiert. Sie führt einen @Composable-Block ohne UI aus, um Eingaben in Compose-State umzuwandeln.
Wenn sich ein State-Wert im Block ändert, wird der Block automatisch neu zusammengesetzt und der zurückgegebene State-Wert aktualisiert. So können Sie das Modell für die Abhängigkeitsverfolgung und Neuzusammensetzung von Compose in ViewModels, Repositories oder Hintergrunddiensten verwenden.
Anwendungsfälle
- Reaktive Datentransformation: Rohdatenstreams (z. B. Netzwerkantworten oder Datenbankabfragen) in einen für die Benutzeroberfläche geeigneten Zustand umwandeln.
- Mehrere Eingaben kombinieren: Sie können mehrere unabhängige Statusquellen (z. B. Nutzereingaben, Einstellungen und Netzwerkstatus) ohne komplexe
Flow-Operatoren zu einem einzigen zusammenhängenden Status zusammenführen. - Debouncing und Throttling: Implementieren Sie zeitbasiertes Verhalten (z. B. Debouncing von Suchanfragen) mit standardmäßigen Compose-Nebeneffekten wie
LaunchedEffect. - Bedingte Vorgänge: Führen Sie je nach aktuellen Bedingungen unterschiedliche Logik zur Statuserfassung mit Standardausdrücken vom Typ
if-elseoderwhenaus.
API-Übersicht
Das androidx.appstate.transform-Paket bietet folgende Funktionen:
package androidx.appstate.transform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable (R) -> R,
): State<R>
@Composable
public fun <R> transform(
initialValue: R,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable (R) -> R,
): State<R>
public suspend fun listener(
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable () -> Unit,
)
Transformation erstellen
Sie können Transformationen entweder eigenständig (mit einer bereitgestellten CoroutineScope) oder innerhalb einer vorhandenen Komposition erstellen. Die Schlüsselparameter für jede Transformation sind initialValue und onUpdate. Der Lambda-Ausdruck onUpdate empfängt den vorherigen Ausgabewert R als Parameter (bei der ersten Komposition mit initialValue initialisiert). Sie können diesen Parameter zurückgeben, um den zuletzt ausgegebenen Status beizubehalten, anstatt ihn auf einen Ladestatus wie { previous -> flow.collectAsState(initial = null).value ?: previous } zurückzusetzen.
Mit transform können Sie beispielsweise Änderungen an einem Suchanfragestring beobachten und teure Berechnungen wie Netzwerkaufrufe entprellen:
var query: String by mutableStateOf("") val searchResults: List<String> by transform<List<String>>( initialValue = emptyList(), scope = coroutineScope, ) { var results by remember { mutableStateOf(emptyList<String>()) } LaunchedEffect(query) { if (query.isBlank()) { results = emptyList() return@LaunchedEffect } delay(300.milliseconds) results = api.search(query) } results }
In diesem Snippet:
querywird durch einMutableStateunterstützt, das von einer Suchleisten-UI-Komposition gelesen und aktualisiert werden kann.transformverwendet eineinitialValueeiner leeren Liste, eineCoroutineScopeundonUpdateals nachgestelltes Lambda.- Im
onUpdate-Block behältrememberdenresults-Zustand während der Neuzusammensetzungen bei, währendLaunchedEffectausgeführt wird. LaunchedEffectverwendetqueryals Schlüssel. Daquerydurch einenMutableStateunterstützt wird, wird der Effekt jedes Mal abgebrochen und neu gestartet, wenn sichqueryändert.- Damit die API nicht bei jedem Tastendruck aufgerufen wird, wartet der Effekttext 300 ms. Wenn sich die Abfrage vor Ablauf dieser Verzögerung ändert, wird der Effekt abgebrochen und kein Netzwerkaufruf ausgeführt.
In einer @Composable-Funktion können Sie auch die Überladung @Composable transform verwenden, die sich intern einen CoroutineScope merkt und standardmäßig onUpdate auf Dispatchers.Default ausführt. Das ist nützlich, wenn Sie eine rechenintensive Statusableitung (z. B. das Filtern oder Durchsuchen einer großen Liste) vom Haupt-UI-Thread auslagern möchten:
@Composable fun LogViewerScreen( entriesState: State<List<LogEntry>>, filterQueryState: State<String>, ) { // Calls rememberCoroutineScope() internally and runs onUpdate on // Dispatchers.Default so main-thread UI recomposition isn't blocked val matchingEntries by transform<List<LogEntry>>( initialValue = emptyList(), dispatcher = Dispatchers.Default, ) { val entries by entriesState val filterQuery by filterQueryState if (filterQuery.isBlank()) { entries } else { entries.filter { entry -> entry.matchesQuery(filterQuery) } } } // Render matchingEntries... }
Status mit listener beobachten
Wenn Sie auf Änderungen von State in Compose in einer Nicht-UI-Komponente (z. B. einem Zustandsmanager, Repository oder Hintergrunddienst) reagieren möchten, ohne einen abgeleiteten State<R> zu erzeugen, verwenden Sie listener.
listener ist eine suspend-Funktion, die einen @Composable () -> Unit-Block ausführt, bis die aufrufende Koroutine abgebrochen wird. Dabei wird die Funktion automatisch neu zusammengesetzt, wenn sich ein in onUpdate gelesener Status ändert:
class FocusSessionManager( private val stateStore: StateStore, private val notificationManager: NotificationManager, scope: CoroutineScope, ) { val session: State<FocusSession> = stateStore.getState(FocusSessionKey) init { scope.launch { listener { when (val current = session.value) { is FocusSession.Running -> { LaunchedEffect(current.endTimeMillis) { val remainingMillis = current.endTimeMillis - System.currentTimeMillis() if (remainingMillis > 0) { delay(remainingMillis) } stateStore.setState(FocusSessionKey, FocusSession.Idle) notificationManager.notify( NOTIFICATION_ID, buildSessionCompleteNotification(current.taskId), ) } } FocusSession.Idle, is FocusSession.Paused -> Unit } } } } // startFocus, pauseFocus, resumeFocus, endFocus... }
In diesem Beispiel beobachtet listener session.value. Wenn die Sitzung FocusSession.Running ist, wartet LaunchedEffect bis endTimeMillis, bevor die Sitzung auf FocusSession.Idle zurückgesetzt und eine Benachrichtigung über den Abschluss gesendet wird. Wenn der Nutzer die Sitzung pausiert oder vorzeitig beendet, verlässt der Running-Zweig die Komposition und bricht den Timer automatisch ab. Wenn scope abgebrochen wird, beendet listener die Beobachtung.
Gängige Muster
Die folgenden Muster zeigen, wie Sie transform für bedingte Logik verwenden, mehrere Streams kombinieren und den UI-Status in einem ViewModel erzeugen.
Bedingte Statusproduktion
Verwenden Sie standardmäßige Kotlin-if-else- oder when-Ausdrücke in transform, um je nach aktuellen Bedingungen zwischen Upstream-Zustandsquellen zu wechseln. Da das Lesetracking dynamisch ist, beobachtet transform nur den Zweig, der in der aktuellen Neuzusammensetzung ausgeführt wird:
var searchQuery by mutableStateOf("") val displayedTasks: State<List<Task>> = transform( initialValue = emptyList(), scope = coroutineScope, ) { if (searchQuery.isBlank()) { val recentTasks by tasksRepository.recentTasks.collectAsState( initial = emptyList(), ) recentTasks } else { val matchingTasks by remember(searchQuery) { tasksRepository.searchTasks(searchQuery) }.collectAsState(initial = emptyList()) matchingTasks } }
Mehrere Statusquellen kombinieren
Da Compose automatisch Statuslesevorgänge verfolgt, können Sie beliebig viele State<T>-Eigenschaften und Flow-Streams in einem einzigen transform-Block kombinieren. Im Gegensatz zu Flow.combine, das nur typisierte Überladungen für bis zu fünf Abläufe bietet, bevor auf einen nicht typisierten Array<Any?> zurückgegriffen wird, ist das Hinzufügen einer weiteren Eingabe in transform immer ein einzelner typsicherer Lesevorgang:
var showCompleted by mutableStateOf(false) var selectedCategory by mutableStateOf<Category?>(null) val dashboardState: State<DashboardUiState> = transform( initialValue = DashboardUiState(), scope = coroutineScope, ) { val tasks by tasksRepository.tasksFlow.collectAsState(initial = emptyList()) val sortOrder by userPrefsRepository.sortOrderFlow.collectAsState( initial = SortOrder.BY_DEADLINE, ) val focusSession by focusSessionManager.session val isOnline by networkMonitor.isOnline val visibleTasks = tasks .filter { task -> (showCompleted || !task.isCompleted) && (selectedCategory == null || task.category == selectedCategory) } .sortedWith(sortOrder.comparator) DashboardUiState( tasks = visibleTasks, selectedCategory = selectedCategory, focusSession = focusSession, isOnline = isOnline, ) }
transform in einem ViewModel verwenden
Die nicht zusammensetzbare transform-Überladung führt reaktive Logik in Nicht-UI-Ebenen (z. B. ViewModels oder Repositories) aus, während das Zustandsmodell von Compose verwendet wird. Sie wird so lange ausgeführt, wie die angegebene CoroutineScope aktiv ist. Sie können sie also an den Lebenszyklus einer Komponente binden, indem Sie viewModelScope in einem ViewModel übergeben:
class HomeViewModel( private val mediaRepository: MediaRepository, private val networkMonitor: NetworkMonitor, ) : ViewModel() { val uiState: State<HomeUiState> = transform<HomeUiState>( initialValue = HomeUiState.Loading, scope = viewModelScope, ) { val isOnline by networkMonitor.isOnline val tracks = if (isOnline) { mediaRepository.allTracksFlow.collectAsState(initial = null).value } else { mediaRepository.downloadedTracksFlow .collectAsState(initial = null).value } ?: return@transform HomeUiState.Loading HomeUiState.Success( tracks = tracks, isOnline = isOnline, ) } }
In diesem ViewModel liest uiState networkMonitor.isOnline und wechselt dynamisch zwischen der Erfassung von mediaRepository.downloadedTracksFlow im Offlinemodus und mediaRepository.allTracksFlow im Onlinemodus. Dabei wird ein einzelner State<HomeUiState> bereitgestellt, den UI-Kompositionen direkt lesen können. Weitere Informationen zum Kombinieren von transform mit MutableState und SavedStateHandle.saveable finden Sie unter Compose-Status in einem ViewModel verwenden.
Wähle zwischen transform, derivedStateOf und Flow
Anhand der folgenden Richtlinien können Sie zwischen transform, derivedStateOf und Kotlin Flow wählen:
derivedStateOf gegen transform
Sowohl derivedStateOf als auch transform erzeugen einen abgeleiteten State<T>, der die in seinem Block gelesenen Compose-Zustände automatisch verfolgt.
Wählen Sie zwischen ihnen aus, je nachdem, ob Sie einen Wert direkt aus vorhandenen State<T>-Objekten berechnen oder @Composable-Features wie collectAsState, produceState und LaunchedEffect benötigen:
| Funktion | derivedStateOf |
transform |
|---|---|---|
| Lambda-Typ | Standard-Kotlin-Lambda (() -> T) |
@Composable Lambda (@Composable (R) -> R) |
| Unterstützte Eingaben | Nur synchrone State<T>-Lesezugriffe |
State<T>, Kotlin-Flow (mit collectAsState), suspend-Funktionen und Callback APIs (mit produceState) |
| Ausführung und Konsistenz | Lazy und synchron, wenn .value gelesen wird; wird atomar innerhalb desselben Snapshots ohne initialValue oder CoroutineScope aktualisiert |
Führt eine aktive Komposition in einem CoroutineScope aus und setzt sie asynchron neu zusammen, nachdem ein Snapshot übernommen wurde. Erfordert einen initialValue. |
| Status und Nebenwirkungen | remember oder APIs mit Nebenwirkungen können nicht aufgerufen werden |
Unterstützt remember, LaunchedEffect, produceState und wiederverwendbare @Composable-Funktionen |
Verwenden Sie derivedStateOf, wenn alle Eingaben bereits State<T>-Objekte sind und Sie keine @Composable-APIs benötigen:
- Synchrone Ableitungen aus demselben Snapshot: Berechnen eines Werts oder einer gefilterten Liste aus vorhandenen
State<T>-Properties, ohne eineCoroutineScopezu verwalten oder einen Platzhalter-initialValuebereitzustellen. DaderivedStateOfsynchron ausgewertet wird, wenn.valuegelesen wird, sehen Aufrufer, die.valueunmittelbar nach dem Aktualisieren eines Eingabestatus lesen, immer das aktualisierte Ergebnis im selben Snapshot. - Häufigkeit der Observer-Aktualisierungen reduzieren: Berechnen eines Werts, der sich seltener ändert als seine Quelle
State<T>(z. B. Prüfen, ob ein Scroll-Offset einen Grenzwert überschreitet). Weitere Informationen finden Sie in der Dokumentation zuderivedStateOf.
Verwenden Sie transform, wenn für Ihre Ableitung die Compose-Laufzeit erforderlich ist:
State<T>mit asynchronen Streams kombinieren: Einen Kotlin-Flowaus einem Repository oder einer Datenbank mitcollectAsStatezusammen mitState<T>-Eingaben erfassen.- Upstream-Arbeit bei Eingabeänderungen neu starten: Mit
remember(key)im Block wird ein parametrisierterFlownur dann neu erstellt, wenn eine bestimmteState<T>-Eingabe aktualisiert wird. - Nebeneffekte ausführen oder Zwischenstatus speichern: Verwenden Sie
LaunchedEffect(z. B. zum Entprellen von Eingaben oder zum Ausführen von Timern),produceState(zum Laden von Daten aus einersuspend-Funktion oder zum Verpacken eines Callbacks mitawaitDispose) oderremember, um den Status über Neuzusammensetzungen hinweg beizubehalten.
Im folgenden Beispiel wird derivedStateOf mit einem transform-Block verglichen, der State<T> und ein Repository Flow kombiniert:
class TaskListViewModel( private val tasksRepository: TasksRepository, private val preferencesManager: TaskPreferencesManager, ) : ViewModel() { var searchQuery by mutableStateOf("") private set // Computing a value from existing State<T>: use derivedStateOf val hasSearchQuery: State<Boolean> = derivedStateOf { searchQuery.isNotBlank() } // Combining State<T> with a Flow and remember(key): use transform val uiState: State<TaskListUiState> = transform<TaskListUiState>( initialValue = TaskListUiState.Loading, scope = viewModelScope, ) { val showCompleted by preferencesManager.showCompleted val tasks by remember(showCompleted) { tasksRepository.observeTasks(showCompleted) }.collectAsState(initial = null) val currentTasks = tasks ?: return@transform TaskListUiState.Loading val filteredTasks = currentTasks.filter { task -> task.title.contains(searchQuery, ignoreCase = true) } TaskListUiState.Success(filteredTasks) } }
In diesem Beispiel prüft hasSearchQuery nur den Status searchQuery, sodass derivedStateOf ihn ohne CoroutineScope verzögert berechnet. Im Gegensatz dazu kombiniert uiState preferencesManager.showCompleted und searchQuery mit einem asynchronen Flow von tasksRepository und verwendet remember(showCompleted), um zu vermeiden, dass Flow neu erstellt wird, wenn sich nur searchQuery ändert.
Wann Flow anstelle von transform verwendet werden sollte
transform ist so konzipiert, dass es den aktuellen Wert des Status darstellt, nicht einen Stream von Ereignissen. Wenn sich ein Status vor der nächsten Neuzusammensetzung mehrmals ändert, verwendet Compose nur den neuesten Wert und überspringt Aktualisierungen, wenn der neue Wert dem vorherigen Wert entspricht (==). Verwenden Sie stattdessen Kotlin-Flow, wenn Ihre Logik jeden Wert der Reihe nach verarbeiten muss:
- Laufende Summen und Durchschnittswerte: Bei Berechnungen wie gleitenden Durchschnittswerten, kumulativen Summen oder dem Rückgängigmachen des Verlaufs (
runningFoldoderscan) muss jeder Wert einmal gezählt werden. In einemtransform-Block werden schnelle Aktualisierungen vor der Neuzusammensetzung übersprungen. Zwei identische Messwerte hintereinander (z. B. ein Sensor, der zweimal10.0ausgibt) lösen keine Neuzusammensetzung aus. Neuzusammensetzungen, die durch andere Zustände im selben Block ausgelöst werden, können den aktuellen Wert zweimal zählen. Berechnen Sie den laufenden Wert zuerst in einemFlowund erfassen Sie das Ergebnis dann intransform, wenn Sie es mit einem anderen Status kombinieren müssen. - Ereignisprotokollierung und Warteschlangen: Pipelines, die Nutzerinteraktionen protokollieren, in die Warteschlange gestellte Netzwerkanfragen verarbeiten oder Transaktionen aufzeichnen, bei denen jede Zwischenänderung (
A -> B -> CoderA -> B -> A) aufgezeichnet werden muss. - Einmalige Ereignisse: Aktionen wie das Abspielen eines Tons oder das Senden eines Hardwarebefehls, bei denen das zweimalige Senden desselben Befehls hintereinander zweimal ausgeführt werden muss.
transformerfordert eineninitialValueund überspringt aufeinanderfolgende gleiche Ausgaben. - Backpressure-sensitive Streams: Streams (z. B. Datei- oder Socket-Lesevorgänge), bei denen ein schneller Producer angehalten werden muss, wenn ein Consumer zurückfällt. Schreibvorgänge im Compose-Status werden nicht ausgesetzt, sondern überschreiben vorherige Werte.
Transformationen testen
Da Transformationen auf der Compose-Laufzeit und Kotlin-Koroutinen basieren, müssen beim Testen die asynchrone Ausführung und die Neuzusammensetzung des Status koordiniert werden. Verwenden Sie das Artefakt androidx.appstate.transform:transform-testing, um den Coroutine-Planer zu steuern und Neuzusammensetzungen in Unittests auszulösen.
Das androidx.appstate.transform.testing-Paket bietet folgende APIs:
package androidx.appstate.transform.testing
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.time.Duration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestResult
public fun transformTest(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend TransformTestScope.() -> Unit,
): TestResult
public sealed interface TransformTestScope : CoroutineScope {
public val testCoroutineScheduler: TestCoroutineScheduler
public val testDispatcher: TestDispatcher
public val backgroundScope: CoroutineScope
public fun advance()
public fun advanceTimeBy(duration: Duration)
public fun advanceUntilIdle()
}
transformTest umschließt runTest und führt den Testblock mit einem TransformTestScope-Empfänger aus:
backgroundScope: EinCoroutineScopezum Ausführen von Transformationen, die den Abschluss des Tests nicht blockieren sollten. Er wird automatisch abgebrochen, wenn der Test abgeschlossen ist, wodurch die Transformation beendet wird.testDispatcher: EinTestDispatcher, der mittestCoroutineSchedulerverknüpft ist. Übergib diesen Wert an den Parameterdispatchervontransform, damit die Neuzusammensetzung im Test-Scheduler ausgeführt wird, anstatt standardmäßigDispatchers.Defaultzu verwenden.advance: Führt ausstehende Koroutinen im Testplaner aus, sendet Compose-Snapshot-Anwendungsbenachrichtigungen (Snapshot.sendApplyNotifications) und setzt dietransformneu zusammen, damit die AusgabeStateaktualisiert wird.advanceTimeBy(duration)undadvanceUntilIdle: Stellen Sie die virtuelle Uhr auftestCoroutineSchedulerfür zeitabhängige Vorgänge vor.
Statusaktualisierungen testen
Wenn Sie einen transform testen möchten, übergeben Sie backgroundScope und testDispatcher aus TransformTestScope und rufen Sie dann advance auf, nachdem Sie einen Eingabestatus oder Flow aktualisiert haben:
@Test fun testStateUpdate() = transformTest { var sourceState by mutableStateOf(0) val state = transform( initialValue = 0, scope = backgroundScope, dispatcher = testDispatcher, ) { sourceState } assertThat(state.value).isEqualTo(0) // Modify the source state and advance to trigger recomposition. sourceState = 1 advance() assertThat(state.value).isEqualTo(1) }
Zeitbasierte Vorgänge testen
Wenn ein transform zeitbasierte Nebenwirkungen wie LaunchedEffect und delay verwendet (z. B. Entprellen von Suchanfragen), kombinieren Sie advanceTimeBy(duration) mit advance:
- Nachdem Sie einen Eingabestatus geändert haben, rufen Sie
advanceauf, damit dertransform-Block neu zusammengesetzt wird und dieLaunchedEffectstartet. - Rufen Sie
advanceTimeBy(duration)auf, um die virtuelle Uhr vorzustellen, und rufen Sie dannadvanceauf, um die für diesen Zeitstempel geplanten Aufgaben auszuführen und dietransformneu zu erstellen.
@Test fun testDebouncedSearch() = transformTest { var query by mutableStateOf("") val searchResults = transform<List<String>>( initialValue = emptyList(), scope = backgroundScope, dispatcher = testDispatcher, ) { var results by remember { mutableStateOf(emptyList<String>()) } LaunchedEffect(query) { if (query.isBlank()) { results = emptyList() return@LaunchedEffect } delay(300.milliseconds) results = api.search(query) } results } query = "compose" // Recompose to start the LaunchedEffect for the new query. advance() // Results don't update before the 300ms debounce delay completes. advanceTimeBy(200.milliseconds) advance() assertThat(searchResults.value).isEmpty() // Advance past the remaining 100ms and trigger recomposition. advanceTimeBy(100.milliseconds) advance() assertThat(searchResults.value).containsExactly("Compose State", "Compose UI") }
ViewModel oder State Holder testen
Da transform die Komposition im bereitgestellten scope ausführt und der Parameter dispatcher standardmäßig auf Dispatchers.Default gesetzt ist, sollten Klassen, die ein transform erstellen, einen CoroutineScope und einen CoroutineDispatcher als Konstruktorparameter akzeptieren. Übergeben Sie in einer ViewModel die eingefügte CoroutineScope an den ViewModel(coroutineScope)-Konstruktor und leiten Sie die CoroutineDispatcher an transform weiter:
class HomeViewModel( private val mediaRepository: MediaRepository, private val networkMonitor: NetworkMonitor, coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate), dispatcher: CoroutineDispatcher = Dispatchers.Default, ) : ViewModel(coroutineScope) { val uiState: State<HomeUiState> = transform<HomeUiState>( initialValue = HomeUiState.Loading, scope = viewModelScope, dispatcher = dispatcher, ) { val isOnline by networkMonitor.isOnline val tracks = if (isOnline) { mediaRepository.allTracksFlow.collectAsState(initial = null).value } else { mediaRepository.downloadedTracksFlow .collectAsState(initial = null).value } ?: return@transform HomeUiState.Loading HomeUiState.Success( tracks = tracks, isOnline = isOnline, ) } } @Test fun testHomeViewModelOfflineState() = transformTest { val mediaRepository = FakeMediaRepository() val networkMonitor = FakeNetworkMonitor() val viewModel = HomeViewModel( mediaRepository = mediaRepository, networkMonitor = networkMonitor, coroutineScope = backgroundScope, dispatcher = testDispatcher, ) val downloadedTrack = Track(id = "1", isDownloaded = true) val streamingTrack = Track(id = "2", isDownloaded = false) mediaRepository.emitAllTracks(listOf(downloadedTrack, streamingTrack)) mediaRepository.emitDownloadedTracks(listOf(downloadedTrack)) advance() assertThat(viewModel.uiState.value).isEqualTo( HomeUiState.Success( tracks = listOf(downloadedTrack, streamingTrack), isOnline = true, ) ) // Switch offline and verify it switches to downloadedTracksFlow. networkMonitor.setOnline(false) // First advance() recomposes into the offline branch (where collectAsState // initially returns null -> Loading) and starts collecting downloadedTracksFlow. advance() // Second advance() applies the collected downloadedTracksFlow value and // recomposes uiState with the offline Success state. advance() assertThat(viewModel.uiState.value).isEqualTo( HomeUiState.Success( tracks = listOf(downloadedTrack), isOnline = false, ) ) }
In diesem Test werden backgroundScope und testDispatcher an HomeViewModel übergeben, wodurch die viewModelScope- und transform-Komposition an den Testplaner gebunden wird. So verarbeitet jeder advance-Aufruf deterministisch Emissionen von FakeMediaRepository und FakeNetworkMonitor. Weitere Muster für die Verwendung von SavedStateHandle.saveable und Snapshot.withMutableSnapshot in einem ViewModel finden Sie unter Compose-Status in einem ViewModel verwenden.