Transform Android Jetpack'in bir parçasıdır.
Transform, girişleri Compose State'e dönüştürmek için kullanıcı arayüzü olmadan bir @Composable bloğu çalıştıran, Compose çalışma zamanı üzerine inşa edilmiş bir kitaplıktır.
Blok içinde okunan herhangi bir State değiştiğinde blok otomatik olarak yeniden oluşturulur ve döndürülen State değerini günceller. Böylece, Compose'un bağımlılık izleme ve yeniden oluşturma modelini ViewModel'lerde, depolarda veya arka plan hizmetlerinde kullanabilirsiniz.
Kullanım alanları
- Reaktif veri dönüştürme: Ham veri akışlarını (ör. ağ yanıtları veya veritabanı sorguları) kullanıcı arayüzüne hazır duruma dönüştürün.
- Birden fazla girişi birleştirme: Birden fazla bağımsız durum kaynağını (ör. kullanıcı girişi, ayarlar ve ağ durumu) karmaşık
Flowoperatörleri olmadan tek bir tutarlı durumda birleştirin. - Debouncing ve throttling:
LaunchedEffectgibi standart Compose yan etkilerini kullanarak zamana dayalı davranışlar (ör. arama sorgularını debounce etme) uygulayın. - Koşullu işlemler: Standart
if-elseveyawhenifadelerini kullanarak mevcut koşullara göre farklı durum toplama mantığı çalıştırın.
API'ye genel bakış
androidx.appstate.transform paketi aşağıdaki işlevleri sağlar:
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,
)
Dönüşüm oluşturma
Dönüşümleri tek başına (sağlanan bir CoroutineScope kullanarak) veya mevcut bir oluşturma içinde oluşturabilirsiniz. Herhangi bir dönüşüm için temel parametreler initialValue ve onUpdate'dir. onUpdate lambda, önceki çıkış değerini R parametre olarak alır (ilk oluşturmada initialValue olarak başlatılır). Yükleme durumuna sıfırlamak yerine son yayınlanan durumu korumak için bu parametreyi döndürebilirsiniz (ör. { previous -> flow.collectAsState(initial = null).value ?: previous }).
Örneğin, bir arama sorgusu dizesindeki değişiklikleri gözlemlemek ve ağ çağrıları gibi maliyetli hesaplamaları geciktirmek için transform kullanabilirsiniz:
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 }
Bu snippet'te:
query, bir arama çubuğu kullanıcı arayüzü composable'ının okuyup güncelleyebileceği birMutableStatetarafından desteklenir.transform, boş bir listenininitialValuedeğerini, birCoroutineScopedeğerini ve sondaki lambda olarakonUpdatedeğerini alır.onUpdatebloğunun içinderemember,LaunchedEffectçalışırkenresultsdurumunu yeniden oluşturmalar boyunca korur.LaunchedEffect, anahtar olarakqueryalır.query, birMutableStatetarafından desteklendiğinden,queryher değiştiğinde efekt iptal edilir ve yeniden başlatılır.- API'nin her tuş vuruşunda çağrılmasını önlemek için efekt gövdesi 300 ms bekler. Sorgu bu gecikme tamamlanmadan değişirse efekt iptal edilir ve ağ çağrısı yapılmaz.
@Composable işlevinin içinde, CoroutineScope değerini dahili olarak hatırlayan ve varsayılan olarak Dispatchers.Default üzerinde onUpdate çalıştıran @Composable transform aşırı yüklemesini de kullanabilirsiniz. Bu, maliyetli bir durum türetme işlemini (büyük bir listeyi filtreleme veya listede arama yapma gibi) ana kullanıcı arayüzü iş parçacığından boşaltmak istediğinizde kullanışlıdır:
@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... }
listener ile durumu gözlemleme
Türetilmiş bir State<R> üretmeden kullanıcı arayüzü olmayan bir bileşendeki (durum yöneticisi, depo veya arka plan hizmeti gibi) Compose State değişikliklerine tepki vermek istediğinizde listener kullanın.
listener, çağıran eş yordam iptal edilene kadar bir @Composable () -> Unit bloğu çalıştıran ve onUpdate içinde okunan herhangi bir durum değiştiğinde otomatik olarak yeniden oluşturan bir suspend işlevidir:
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... }
Bu örnekte listener, session.value'i gözlemler. Oturum FocusSession.Running olduğunda LaunchedEffect, oturumu FocusSession.Idle olarak sıfırlamadan ve tamamlama bildirimi yayınlamadan önce endTimeMillis tarihine kadar bekler. Kullanıcı oturumu duraklatır veya erken sonlandırırsa Running dalı, oluşturmadan ayrılır ve zamanlayıcıyı otomatik olarak iptal eder. scope iptal edildiğinde listener gözlemlemeyi durdurur.
Yaygın kalıplar
Aşağıdaki kalıplar, koşullu mantık için transform kullanmayı, birden fazla akışı birleştirmeyi ve ViewModel içinde kullanıcı arayüzü durumu oluşturmayı gösterir.
Koşullu durum üretimi
Mevcut koşullara göre üst akış durum kaynakları arasında geçiş yapmak için transform içinde standart Kotlin if-else veya when ifadelerini kullanın. Okuma izleme dinamik olduğundan transform yalnızca mevcut yeniden oluşturma işleminde çalışan dalı gözlemler:
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 } }
Birden fazla durum kaynağını birleştirme
Compose, durum okumalarını otomatik olarak izlediği için istediğiniz sayıda State<T> özelliğini ve Flow akışını tek bir transform bloğunda birleştirebilirsiniz. Yalnızca beş akışa kadar yazılı aşırı yüklemeler sağlayan ve ardından yazılmamış bir Array<Any?>'ye geri dönen Flow.combine'nin aksine, transform'e başka bir giriş eklemek her zaman tek bir tür güvenli okumadır:
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, ) }
ViewModel'de transform kullanma
Oluşturulabilir olmayan transform aşırı yüklemesi, Compose'un durum modelini kullanırken kullanıcı arayüzü olmayan katmanlarda (ör. ViewModel'ler veya depolar) reaktif mantığı çalıştırır. Sağlanan CoroutineScope etkin olduğu sürece çalışır. Bu nedenle, ViewModel içine viewModelScope ileterek bir bileşenin yaşam döngüsüne bağlayabilirsiniz:
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, ) } }
Bu ViewModel içinde uiState, networkMonitor.isOnline değerini okur ve çevrimdışıyken mediaRepository.downloadedTracksFlow, çevrimiçiyken mediaRepository.allTracksFlow toplama arasında dinamik olarak geçiş yapar. Böylece, kullanıcı arayüzü bileşenlerinin doğrudan okuyabileceği tek bir State<HomeUiState> gösterir. transform ile MutableState ve SavedStateHandle.saveable'yi birleştirme hakkında daha fazla bilgi için ViewModel'de Compose durumunu kullanma başlıklı makaleyi inceleyin.
transform, derivedStateOf ve Flow arasından seçim yapın
transform, derivedStateOf ve Kotlin Flow arasında seçim yapmak için aşağıdaki yönergeleri kullanın:
derivedStateOf - transform
Hem derivedStateOf hem de transform, kendi bloğunda okunan Compose durumlarını otomatik olarak izleyen türetilmiş bir State<T> üretir.
Mevcut State<T> nesnelerinden doğrudan bir değer hesaplayıp hesaplamadığınıza veya collectAsState, produceState ve LaunchedEffect gibi @Composable özelliklerine ihtiyacınız olup olmadığına göre bunlar arasında seçim yapın:
| Kapasite | derivedStateOf |
transform |
|---|---|---|
| Lambda türü | Standart Kotlin lambda (() -> T) |
@Composable lambda (@Composable (R) -> R) |
| Desteklenen girişler | Yalnızca eşzamanlı State<T> okuma |
State<T>, Kotlin Flow (collectAsState kullanarak), suspend işlevleri ve geri çağırma API'leri (produceState kullanarak) |
| Uygulama ve tutarlılık | .value okunduğunda geç ve eşzamanlı; initialValue veya CoroutineScope olmadan aynı anlık görüntü içinde atomik olarak güncellenir |
Etkin bir oluşturmayı CoroutineScope içinde çalıştırır ve bir anlık görüntü kaydedildikten sonra eşzamansız olarak yeniden oluşturur; initialValue gerektirir. |
| Durum ve yan etkiler | remember veya yan etki API'leri çağrılamaz |
remember, LaunchedEffect, produceState ve yeniden kullanılabilir @Composable işlevlerini destekler. |
Tüm girişler zaten State<T> nesneleriyse ve @Composable API'lerine ihtiyacınız yoksa derivedStateOf kullanın:
- Eşzamanlı, aynı anlık görüntü türetmeleri:
CoroutineScopeyönetmeden veya yer tutucuinitialValuesağlamadan mevcutState<T>özelliklerinden bir değer veya filtrelenmiş liste hesaplama.derivedStateOf,.valueokunduğunda eşzamanlı olarak değerlendirildiğinden, bir giriş durumunu güncelledikten hemen sonra.valuedeğerini okuyan çağrıcılar, güncellenen sonucu her zaman aynı anlık görüntüde görür. - Gözlemcilerin güncelleme sıklığını azaltma: Kaynağından
State<T>daha az sıklıkta değişen bir değer hesaplama (ör. kaydırma ofsetinin bir eşiği aşıp aşmadığını kontrol etme). Daha fazla bilgi içinderivedStateOfbelgelerini inceleyin.
Türetme işleminiz için Compose çalışma zamanı gerektiğinde transform kullanın:
State<T>ile eşzamansız akışları birleştirme:State<T>girişleriyle birliktecollectAsStatekullanarak bir depodan veya veritabanından KotlinFlowtoplama.- Girişler değiştiğinde üst akış çalışmasını yeniden başlatma: Yalnızca belirli bir
State<T>girişi güncellendiğinde parametreleştirilmiş birFlowöğesini yeniden oluşturmak için blok içinderemember(key)kullanma. - Yan etkileri çalıştırma veya ara durumu hatırlama:
LaunchedEffect(ör. girişleri geciktirmek veya zamanlayıcıları çalıştırmak için),produceState(suspendişlevinden veri yüklemek veya geri çağırmayıawaitDisposeile sarmalamak için) ya da yeniden oluşturma işlemleri arasında durumu korumak içinrememberkullanma.
Aşağıdaki örnekte, derivedStateOf ile State<T> ve bir depo Flow'yi birleştiren transform bloğu karşılaştırılmaktadır:
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) } }
Bu örnekte hasSearchQuery yalnızca searchQuery durumunu inceler. Bu nedenle derivedStateOf, CoroutineScope olmadan geç hesaplama yapar. Buna karşılık uiState, preferencesManager.showCompleted ve searchQuery'yi tasksRepository'den gelen eşzamansız bir Flow ile birleştirir ve yalnızca searchQuery değiştiğinde Flow'yi yeniden oluşturmaktan kaçınmak için remember(showCompleted) kullanır.
transform yerine Flow ne zaman kullanılır?
transform, bir etkinlik akışını değil, durumun mevcut değerini temsil edecek şekilde tasarlanmıştır. Bir durum, bir sonraki yeniden oluşturma işleminden önce birden fazla kez değişirse Compose yalnızca en son değeri kullanır ve yeni değer önceki değere eşitse (==) güncellemeleri atlar. Mantığınızın her değeri sırayla işlemesi gerektiğinde Kotlin Flow kullanın:
- Çalışan toplamlar ve ortalamalar: Hareketli ortalamalar, kümülatif toplamlar veya geri alma geçmişi (
runningFoldya dascan) gibi hesaplamalarda her değerin bir kez sayılması gerekir.transformbloğunda, yeniden oluşturma işleminden önceki hızlı güncellemeler atlanır, arka arkaya iki aynı okuma (ör. bir sensörün iki kez10.0yayması) yeniden oluşturma işlemini tetiklemez ve aynı bloktaki diğer durumlar tarafından tetiklenen yeniden oluşturma işlemleri mevcut değeri iki kez sayabilir. ÖnceFlowiçinde çalışan değeri hesaplayın, ardından diğer durumlarla birleştirmeniz gerekiyorsa sonucutransformiçinde toplayın. - Olay günlüğü ve kuyruklar: Kullanıcı etkileşimlerini günlüğe kaydeden, sıraya alınmış ağ isteklerini işleyen veya her ara değişikliğin (
A -> B -> CveyaA -> B -> A) kaydedilmesi gereken işlemleri kaydeden ardışık düzenler. - Tek seferlik etkinlikler: Ses çalma veya donanım komutu gönderme gibi, aynı komutun art arda iki kez gönderildiğinde iki kez çalıştırılması gereken işlemler.
transform,initialValuegerektirir ve ardışık eşit çıkışları atlar. - Geri basınç hassasiyetli akışlar: Tüketici geride kaldığında hızlı bir üreticinin askıya alınması gereken akışlar (ör. dosya veya soket okumaları). Compose durum yazma işlemleri askıya alınmaz ve önceki değerlerin üzerine yazılır.
Test dönüşümleri
Dönüşümler, Compose çalışma zamanına ve Kotlin eş yordamlarına dayandığından bunları test etmek için eşzamansız yürütme ve durum yeniden oluşturma işlemlerinin koordine edilmesi gerekir. Eş yordam zamanlayıcıyı kontrol etmek ve birim testlerinde yeniden oluşturmaları tetiklemek için androidx.appstate.transform:transform-testing yapısını kullanın.
androidx.appstate.transform.testing paketi aşağıdaki API'leri sağlar:
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, runTest ifadesini sarmalar ve test bloğunuzu bir TransformTestScope alıcısıyla yürütür:
backgroundScope: Testin tamamlanmasını engellememesi gereken dönüşümleri çalıştırmak için kullanılan birCoroutineScope. Test tamamlandığında otomatik olarak iptal edilir ve dönüşüm durdurulur.testDispatcher:testCoroutineSchedulerile bağlantılı birTestDispatcher. Bunutransformöğesinindispatcherparametresine ileterek yeniden oluşturma işleminin varsayılan olarakDispatchers.Defaultyerine test planlayıcısında çalışmasını sağlayın.advance: Test zamanlayıcısında bekleyen eş yordamları çalıştırır, Compose anlık görüntü uygulama bildirimleri (Snapshot.sendApplyNotifications) gönderir vetransformöğesini yeniden oluşturur. Böylece çıkışStategüncellenir.advanceTimeBy(duration)veadvanceUntilIdle: Zamana bağlı işlemler içintestCoroutineSchedulerüzerindeki sanal saati ileri alın.
Test durumu güncellemeleri
transform test etmek için TransformTestScope üzerinden backgroundScope ve testDispatcher iletin, ardından bir giriş durumunu veya Flow güncelledikten sonra advance çağırın:
@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) }
Zamana dayalı işlemleri test etme
Bir transform, LaunchedEffect ve delay gibi zamana dayalı yan etkiler kullandığında (ör. arama sorgularını geciktirme) advanceTimeBy(duration) ile advance öğelerini birleştirin:
- Bir giriş durumunu değiştirdikten sonra
advanceişlevini çağırın. Böylecetransformbloğu yeniden oluşturulur veLaunchedEffectbaşlatılır. - Sanal saati ileri almak için
advanceTimeBy(duration)işlevini çağırın, ardından bu zaman damgasında planlanan görevleri çalıştırmak vetransformöğesini yeniden oluşturmak içinadvanceişlevini çağırın.
@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 veya durum bilgisi depolayıcıyı test etme
transform, oluşturma işlemini sağlanan scope içinde çalıştırdığından ve dispatcher parametresini varsayılan olarak Dispatchers.Default şeklinde ayarladığından transform oluşturan sınıflar, kurucu parametreleri olarak CoroutineScope ve CoroutineDispatcher kabul etmelidir. ViewModel içinde, eklenen CoroutineScope öğesini ViewModel(coroutineScope) oluşturucusuna iletin ve CoroutineDispatcher öğesini transform öğesine yönlendirin:
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, ) ) }
Bu testte, backgroundScope ve testDispatcher değerlerini HomeViewModel öğesine iletmek, viewModelScope ve transform oluşturma işlemlerini test planlayıcısına bağlar. Böylece her advance çağrısı, FakeMediaRepository ve FakeNetworkMonitor öğelerinden gelen emisyonları kesin olarak işler. ViewModel içinde SavedStateHandle.saveable ve Snapshot.withMutableSnapshot kullanmayla ilgili daha fazla kalıp için ViewModel'de Compose State'i kullanma başlıklı makaleyi inceleyin.