تبدیل کردن بخشی از Android Jetpack.
Transform کتابخانهای است که بر روی زمان اجرای Compose ساخته شده است و یک بلوک @Composable
را بدون واسط کاربر اجرا میکند تا ورودیها را به Compose State تبدیل کند.
هرگاه هر State خواندهشده در بلوک تغییر کند، بلوک بهطور خودکار
State برگرداندهشده را بازترکیب و بهروز میکند—به شما اجازه میدهد از
مدل بازترکیب و ردیابی وابستگی Compose در ViewModels، مخازن، یا
سرویسهای پسزمینهای استفاده کنید.
موارد استفاده
- تبدیل دادههای واکنشی: تبدیل جریانهای داده خام (مانند پاسخهای شبکه یا پُرسمانهای پایگاه داده) به حالت آماده برای رابط کاربری.
- ترکیب کردن چندین ورودی: چندین منبع حالت مستقل (مانند ورودی کاربر، تنظیمات، و وضعیت شبکه) را بدون نیاز به عملگرهای پیچیده
Flowدر یک حالت منسجم واحد ادغام کنید. - لرزشگیری و کنترل سرعت: رفتار مبتنی بر زمان (مانند
لرزشگیری پرسمانهای جستجو) را بااستفاده از عوارض جانبی استاندارد Compose
مانند
LaunchedEffectپیادهسازی کنید. - عملیات شرطی: منطق جمعآوری وضعیت متفاوتی را براساس شرایط فعلی بااستفاده از عبارتهای استاندارد
if-elseیاwhenاجرا کنید.
نمای کلی میانای برنامهسازی کاربردی
بسته androidx.appstate.transform عملکردهای زیر را ارائه میدهد:
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,
)
ایجاد تبدیل
میتوانید تبدیلها را بهصورت مستقل (بااستفاده از
CoroutineScope ارائهشده) یا در یک «ترکیببندی» موجود ایجاد کنید. پارامترهای کلیدی برای هر
تبدیلی initialValue و onUpdate است. onUpdate lambda مقدار برونداد قبلی R را بهعنوان پارامتر خود دریافت میکند (در اولین ترکیببندی به initialValue مقداردهی اولیه میشود). میتوانید این پارامتر را برگردانید تا آخرین حالت منتشرشده را حفظ کنید
بهجای اینکه به حالت بارگیری بازنشانی کنید، مثل
{ previous -> flow.collectAsState(initial = null).value ?: previous }.
برای مثال، میتوانید از transform برای مشاهده تغییرات در رشته پرسمان جستجو
و لرزشگیری محاسبات پرهزینه مانند تماسهای شبکه استفاده کنید:
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 }
در این گزیده:
queryباMutableStateپشتیبانی میشود که یک نوار جستجوی UI composable میتواند آن را بخواند و بهروز کند.-
transformیکinitialValueاز فهرست خالی، یکCoroutineScope، وonUpdateرا بهعنوان لامبدا دنبالهدار میگیرد. - درون بلوک
onUpdate،rememberحالتresultsرا در سراسر بازترکیبها حفظ میکند درحالیکهLaunchedEffectاجرا میشود. LaunchedEffectqueryرا بهعنوان کلید میگیرد. چونqueryتوسطMutableStateپشتیبانی میشود، هربار کهqueryتغییر میکند، جلوه لغو و دوباره راهاندازی میشود.- برای جلوگیری از فراخوانی میانای برنامهسازی کاربردی در هر ضربه کلید، بدنه اثر ۳۰۰ میلیثانیه صبر میکند. اگر پرسمان قبلاز تکمیل آن تأخیر تغییر کند، اثر لغو میشود و هیچ فراخوانی شبکهای انجام نمیشود.
درون تابع @Composable، میتوانید از سربارگذاری @Composable transform نیز استفاده کنید که CoroutineScope را بهطور داخلی بهخاطر میسپارد و onUpdate را بهطور پیشفرض در Dispatchers.Default اجرا میکند. این کار زمانی مفید است که میخواهید یک اشتقاق حالت پرهزینه (مانند فیلتر کردن یا جستجوی یک فهرست بزرگ) را از رشته اصلی واسط کاربر تخلیه کنید:
@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
وقتی میخواهید به تغییرات Compose State در مؤلفه غیررابط کاربری (مانند
مدیر وضعیت، مخزن، یا سرویس پسزمینهای) بدون تولید State<R> مشتقشده واکنش نشان دهید،
از listener استفاده کنید.
listener یک تابع suspend است که یک بلوک @Composable () -> Unit را اجرا میکند
تا زمانی که کوروتین فراخوان لغو شود، و هر زمان که هر وضعیت خواندهشده در داخل onUpdate تغییر کند، بهطور خودکار بازترکیب میشود:
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... }
در این مثال، listener session.value را مشاهده میکند. وقتی جلسه
FocusSession.Running است، LaunchedEffect تا endTimeMillis صبر میکند و سپس
جلسه را به FocusSession.Idle بازنشانی میکند و اعلان تکمیل
پست میکند. اگر کاربر جلسه را زودتر از موعد موقتاً متوقف کند یا بهپایان برساند، شاخه Running
ترکیببندی را ترک میکند و زمانسنج را بهطور خودکار لغو میکند. وقتی scope لغو شود، listener مشاهده را متوقف میکند.
الگوهای رایج
الگوهای زیر نحوه استفاده از transform را برای منطق شرطی،
ترکیب کردن چند جاریسازی، و تولید حالت میانای کاربر در ViewModel نشان میدهد.
تولید حالت شرطی
از عبارات استاندارد Kotlin if-else یا when در transform استفاده کنید تا
بین منابع حالت بالادستی براساس شرایط فعلی جابهجا شوید. چون ردیابی خواندن پویا است، transform فقط شاخهای را که در بازترکیب فعلی اجرا میشود مشاهده میکند:
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 } }
ترکیب کردن چند منبع ایالتی
چون Compose بهطور خودکار خواندنهای وضعیت را ردیابی میکند، میتوانید هر تعداد
State<T> ویژگی و Flow جریان را در یک بلوک transform ترکیب کنید. برخلاف
Flow.combine، که فقط اضافهبار تایپشده را برای حداکثر
پنج جریان ارائه میدهد و سپس به Array<Any?> تایپنشده برمیگردد، افزودن ورودی دیگر
در transform همیشه یک خواندن ایمن تایپی تکی است:
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 در ViewModel
بار اضافی transform غیرقابلساخت منطق واکنشی را در لایههای غیرواسط کاربر (مانند ViewModels یا مخازن) اجرا میکند، درحالیکه از مدل حالت Compose استفاده میکند. تا زمانی که CoroutineScope ارائهشده فعال باشد، اجرا میشود، بنابراین میتوانید آن را با ارسال viewModelScope در داخل ViewModel به چرخه حیات مؤلفه گره بزنید:
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 this ViewModel, uiState reads networkMonitor.isOnline and dynamically
switches between collecting mediaRepository.downloadedTracksFlow when offline
and mediaRepository.allTracksFlow when online, exposing a single
State<HomeUiState> that UI composables can read directly. برای جزئیات بیشتر درباره
ترکیب کردن transform با MutableState و SavedStateHandle.saveable، به
استفاده از «وضعیت Compose» در ViewModel مراجعه کنید.
از بین transform، derivedStateOf، و Flow انتخاب کنید
از دستورالعملهای زیر برای انتخاب بین transform، derivedStateOf،
و Kotlin Flow استفاده کنید:
derivedStateOf مقابل transform
هر دو derivedStateOf و transform یک
State<T> مشتقشده تولید میکنند که بهطور خودکار وضعیتهای Compose خواندهشده در بلوک خود را ردیابی میکند.
براساس اینکه مقدار را مستقیماً از اشیاء State<T> موجود محاسبه میکنید یا به ویژگیهای @Composable مثل collectAsState، produceState، و LaunchedEffect نیاز دارید، یکی از آنها را انتخاب کنید:
| قابلیت | derivedStateOf |
transform |
|---|---|---|
| نوع لامبدا | لامبدا Kotlin استاندارد (() -> T) |
@Composable لامبدا (@Composable (R) -> R) |
| ورودیهای پشتیبانیشده | فقط خواندن همگام State<T> |
State<T>، Kotlin Flow (بااستفاده از collectAsState)، توابع suspend، و میاناهای برنامهسازی کاربردی callback (بااستفاده از produceState) |
| اجرا و ثبات | Lazy and synchronous when .value is read; updates atomically within the same snapshot without an initialValue or CoroutineScope |
«ترکیببندی» فعالی را در CoroutineScope اجرا میکند و پساز ثبت عکس آنی، بهصورت ناهمگام بازترکیب میکند؛ به initialValue نیاز دارد |
| حالت و عوارض جانبی | نمیتوان remember یا میاناهای برنامهسازی کاربردی عوارض جانبی را فراخوانی کرد |
از توابع remember، LaunchedEffect، produceState، و توابع قابلاستفاده مجدد @Composable پشتیبانی میکند |
وقتی همه ورودیها ازقبل شیء State<T> هستند و به میاناهای برنامهسازی کاربردی @Composable نیاز ندارید، از derivedStateOf استفاده کنید:
- مشتقات همزمان و همعکس: محاسبه مقدار یا فهرست فیلترشده
از ویژگیهای
State<T>موجود بدون مدیریتCoroutineScopeیا ارائه جایبانinitialValue. چونderivedStateOfبهطور همزمان هنگام خوانده شدن.valueارزیابی میکند، تماسگیرندگانی که.valueرا بلافاصله پساز بهروزرسانی وضعیت ورودی میخوانند، همیشه نتیجه بهروزرسانیشده را در همان عکس آنی مشاهده میکنند. - کاهش دفعات بهروزرسانی ناظران: محاسبه مقداری که کمتر از منبع خود
State<T>تغییر میکند (مانند بررسی اینکه آیا جابهجایی پیمایش از آستانه فراتر میرود یا نه). برای جزئیات بیشتر، بهderivedStateOfمستندات مراجعه کنید.
وقتی اشتقاق شما به زمان اجرای Compose نیاز دارد، از transform استفاده کنید:
- ترکیب کردن
State<T>با جریانهای ناهمگام: جمعآوری کردنFlowKotlin از مخزن یا پایگاه داده بااستفاده ازcollectAsStateدرکنار ورودیهایState<T>. - راهاندازی مجدد کار بالادستی هنگام تغییر ورودیها: استفاده از
remember(key)در داخل بلوک برای بازآفرینیFlowپارامتریشده فقط زمانی که ورودیState<T>خاصی بهروز میشود. - اجرای عوارض جانبی یا بهخاطر سپردن حالت میانی: استفاده از
LaunchedEffect(برای مثال، برای لرزشگیری ورودیها یا اجرای زمانسنجها)،produceState(برای بار کردن دادهها از تابعsuspendیا پیچیدن فراخوانی متقابل باawaitDispose)، یاrememberبرای حفظ حالت در ترکیبهای مجدد.
مثال زیر derivedStateOf را با بلوک transform مقایسه میکند که
State<T> و مخزن Flow را ترکیب میکند:
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) } }
در این مثال، hasSearchQuery فقط وضعیت searchQuery را بررسی میکند، بنابراین
derivedStateOf آن را بهصورت تنبل و بدون CoroutineScope محاسبه میکند. درمقابل،
uiState preferencesManager.showCompleted و searchQuery را با
Flow ناهمگام از tasksRepository ترکیب میکند و از remember(showCompleted) استفاده میکند تا
از بازآفرینی Flow وقتی فقط searchQuery تغییر میکند جلوگیری کند.
چه زمانی باید از Flow بهجای transform استفاده کرد
transform برای نشان دادن مقدار فعلی وضعیت طراحی شده است، نه
جریان رویدادها. اگر حالتی چندین بار قبلاز ترکیببندی مجدد بعدی تغییر کند، Compose فقط از جدیدترین مقدار استفاده میکند و وقتی مقدار جدید با مقدار قبلی برابر (==) باشد، از بهروزرسانیها صرفنظر میکند. وقتی منطق شما باید هر مقدار را بهترتیب پردازش کند،
بهجای آن از Kotlin Flow استفاده کنید:
- مجموعها و میانگینهای جاری: محاسباتی مانند میانگینهای متحرک، مجموعهای تجمعی، یا واگرد کردن سابقه (
runningFoldیاscan) نیاز دارند که هر مقدار یکبار شمرده شود. در بلوکtransform، بهروزرسانیهای سریع قبلاز بازترکیب نادیده گرفته میشوند، دو خوانش یکسان پشتسرهم (مثل حسگری که10.0را دو بار منتشر میکند) بازترکیب را راهاندازی نمیکنند، و بازترکیبهایی که توسط حالتهای دیگر در همان بلوک راهاندازی میشوند میتوانند مقدار فعلی را دو بار بشمارند. ابتدا مقدار درحال اجرا را درFlowمحاسبه کنید، سپس اگر لازم است آن را با وضعیت دیگری ترکیب کنید، نتیجه را درtransformجمعآوری کنید. - ثبت رویداد و صفها: خطوط لولهای که تعاملات کاربر را ثبت میکنند، درخواستهای شبکه در صف را پردازش میکنند، یا تراکنشهایی را که در آنها هر تغییر واسطهای (
A -> B -> CیاA -> B -> A) باید ثبت شود ضبط میکنند. - رویدادهای یکباره: کنشهایی مانند پخش صدا یا ارسال فرمان سختافزاری، که در آن ارسال دو بار متوالی یک فرمان باید دو بار اجرا شود.
transformبهinitialValueنیاز دارد و بروندادهای برابر متوالی را رد میکند. - جاریسازیهای حساس به فشار برگشتی: جاریسازیهایی (مانند خواندن فایل یا سوکت) که در آنها تولیدکننده سریع باید درصورت عقب افتادن مصرفکننده متوقف شود. نوشتنهای حالت Compose تعلیق نمیشوند و درعوض مقادیر قبلی را رونویسی میکنند.
تبدیلهای آزمایشی
ازآنجاییکه تبدیلها به زمان اجرای Compose و Kotlin Coroutines متکی هستند، آزمایش
آنها نیازمند هماهنگی اجرای ناهمگام و ترکیب مجدد حالت است. از
مصنوع androidx.appstate.transform:transform-testing برای کنترل
زمانبند روال همکار و راهاندازی بازترکیبها در آزمونهای واحد استفاده کنید.
بسته androidx.appstate.transform.testing میاناهای برنامهسازی کاربردی زیر را ارائه میدهد:
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 را میپیچد و بلوک آزمایشی شما را با گیرنده TransformTestScope اجرا میکند:
backgroundScope:CoroutineScopeبرای اجرای تبدیلهایی که نباید تکمیل آزمایش را مسدود کنند. با پایان یافتن آزمایش، بهطور خودکار لغو میشود و تبدیل متوقف میشود.testDispatcher:TestDispatcherپیوندشده بهtestCoroutineScheduler. این را به پارامترdispatcherازtransformارسال کنید تا بازترکیب بهجای پیشفرضDispatchers.Default، روی زمانبند آزمایشی اجرا شود.advance: کوروتینهای معلق را در زمانبند آزمایشی اجرا میکند، اعلانهای اعمال عکس آنی Compose را ارسال میکند (Snapshot.sendApplyNotifications)، وtransformرا بازترکیب میکند تا خروجیStateبهروز شود.advanceTimeBy(duration)وadvanceUntilIdle: ساعت مجازی را درtestCoroutineSchedulerبرای عملیات وابسته به زمان جلو ببرید.
بهروزرسانیهای وضعیت آزمایش
برای آزمایش transform، backgroundScope و testDispatcher را از
TransformTestScope رد کنید، سپس advance را پساز بهروزرسانی وضعیت ورودی یا
Flow فراخوانی کنید:
@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) }
آزمایش عملیات زمانمحور
وقتی transform از عوارض جانبی مبتنی بر زمان مانند LaunchedEffect و
delay (مانند پرسمانهای جستجوی لرزشگیر) استفاده میکند، advanceTimeBy(duration)
را با advance ترکیب کنید:
- پساز اصلاح وضعیت ورودی،
advanceرا فراخوانی کنید تا بلوکtransformدوباره ترکیب شود وLaunchedEffectرا شروع کند. advanceTimeBy(duration)را فراخوانی کنید تا ساعت مجازی جلو برود، سپسadvanceرا فراخوانی کنید تا تکالیف زمانبندیشده در آن مُهر زمان اجرا شود وtransformرا بازترکیب کنید.
@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 یا نگهدارنده حالت
چون transform «ترکیببندی» خود را در scope ارائهشده اجرا میکند و
پارامتر dispatcher آن بهطور پیشفرض روی Dispatchers.Default تنظیم شده است، کلاسهایی که
transform ایجاد میکنند باید CoroutineScope و CoroutineDispatcher را بهعنوان
پارامترهای سازنده بپذیرند. در ViewModel،
CoroutineScope تزریقشده را به سازنده ViewModel(coroutineScope) ارسال کنید و
CoroutineDispatcher را به transform بازارسال کنید:
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, ) ) }
در این آزمایش، ارسال backgroundScope و testDispatcher به
HomeViewModel، «ترکیببندی» viewModelScope و transform آن را به
زمانبند آزمایش مقید میکند، بنابراین هر فراخوانی advance بهطور قطعی انتشارات
FakeMediaRepository و FakeNetworkMonitor را پردازش میکند. برای الگوهای بیشتر درباره استفاده از
SavedStateHandle.saveable و Snapshot.withMutableSnapshot در ViewModel،
استفاده از «وضعیت Compose» در ViewModel را ببینید.