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 Flow operatörleri olmadan tek bir tutarlı durumda birleştirin.
  • Debouncing ve throttling: LaunchedEffect gibi standart Compose yan etkilerini kullanarak zamana dayalı davranışlar (ör. arama sorgularını debounce etme) uygulayın.
  • Koşullu işlemler: Standart if-else veya when ifadelerini 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 bir MutableState tarafından desteklenir.
  • transform, boş bir listenin initialValue değerini, bir CoroutineScope değerini ve sondaki lambda olarak onUpdate değerini alır.
  • onUpdate bloğunun içinde remember, LaunchedEffect çalışırken results durumunu yeniden oluşturmalar boyunca korur.
  • LaunchedEffect, anahtar olarak query alır. query, bir MutableState tarafından desteklendiğinden, query her 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: CoroutineScope yönetmeden veya yer tutucu initialValue sağlamadan mevcut State<T> özelliklerinden bir değer veya filtrelenmiş liste hesaplama. derivedStateOf, .value okunduğunda eşzamanlı olarak değerlendirildiğinden, bir giriş durumunu güncelledikten hemen sonra .value değ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çin derivedStateOf belgelerini 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 birlikte collectAsState kullanarak bir depodan veya veritabanından Kotlin Flow toplama.
  • 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ş bir Flow öğesini yeniden oluşturmak için blok içinde remember(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 (suspend işlevinden veri yüklemek veya geri çağırmayı awaitDispose ile sarmalamak için) ya da yeniden oluşturma işlemleri arasında durumu korumak için remember kullanma.

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 (runningFold ya da scan) gibi hesaplamalarda her değerin bir kez sayılması gerekir. transform bloğunda, yeniden oluşturma işleminden önceki hızlı güncellemeler atlanır, arka arkaya iki aynı okuma (ör. bir sensörün iki kez 10.0 yayması) 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. Önce Flow içinde çalışan değeri hesaplayın, ardından diğer durumlarla birleştirmeniz gerekiyorsa sonucu transform iç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 -> C veya A -> 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, initialValue gerektirir 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 bir CoroutineScope. Test tamamlandığında otomatik olarak iptal edilir ve dönüşüm durdurulur.
  • testDispatcher: testCoroutineScheduler ile bağlantılı bir TestDispatcher. Bunu transform öğesinin dispatcher parametresine ileterek yeniden oluşturma işleminin varsayılan olarak Dispatchers.Default yerine 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 ve transform öğesini yeniden oluşturur. Böylece çıkış State güncellenir.
  • advanceTimeBy(duration) ve advanceUntilIdle: Zamana bağlı işlemler için testCoroutineScheduler ü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:

  1. Bir giriş durumunu değiştirdikten sonra advance işlevini çağırın. Böylece transform bloğu yeniden oluşturulur ve LaunchedEffect başlatılır.
  2. 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 ve transform öğesini yeniden oluşturmak için advance iş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.