Biến đổi Một phần của Android Jetpack.
Biến đổi là một thư viện được xây dựng trên thời gian chạy Compose, chạy một khối @Composable không có giao diện người dùng để biến đổi dữ liệu đầu vào thành State của Compose.
Bất cứ khi nào có thay đổi về State được đọc bên trong khối, khối đó sẽ tự động
tái cấu trúc và cập nhật State được trả về, cho phép bạn sử dụng mô hình
theo dõi phần phụ thuộc và tái cấu trúc của Compose trong ViewModel, kho lưu trữ hoặc
dịch vụ nền.
Trường hợp sử dụng
- Chuyển đổi dữ liệu phản ứng: Chuyển đổi luồng dữ liệu thô (chẳng hạn như phản hồi mạng hoặc truy vấn cơ sở dữ liệu) thành trạng thái sẵn sàng cho giao diện người dùng.
- Kết hợp nhiều dữ liệu đầu vào: Hợp nhất nhiều nguồn trạng thái độc lập (chẳng hạn như dữ liệu đầu vào của người dùng, chế độ cài đặt và trạng thái mạng) thành một trạng thái gắn kết duy nhất mà không cần các toán tử
Flowphức tạp. - Chống dội phím và điều tiết: Triển khai hành vi dựa trên thời gian (chẳng hạn như chống dội phím cho cụm từ tìm kiếm) bằng cách sử dụng hiệu ứng phụ chuẩn của Compose như
LaunchedEffect. - Toán tử điều kiện: Chạy logic thu thập trạng thái khác nhau dựa trên các điều kiện hiện tại bằng cách sử dụng biểu thức
if-elsehoặcwhenchuẩn.
Tổng quan về API
Gói androidx.appstate.transform cung cấp các hàm sau:
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,
)
Tạo phép biến đổi
Bạn có thể tạo phép biến đổi riêng (bằng cách sử dụng CoroutineScope được cung cấp) hoặc trong một Thành phần hiện có. Các tham số chính cho mọi phép biến đổi là initialValue và onUpdate. Hàm lambda onUpdate nhận giá trị đầu ra trước đó R làm tham số (được khởi tạo thành initialValue trong thành phần đầu tiên). Bạn có thể trả về tham số này để giữ trạng thái phát ra gần đây nhất thay vì đặt lại về trạng thái đang tải, chẳng hạn như { previous -> flow.collectAsState(initial = null).value ?: previous }.
Ví dụ: bạn có thể sử dụng transform để quan sát các thay đổi đối với chuỗi cụm từ tìm kiếm và gỡ bỏ các phép tính tốn kém như lệnh gọi mạng:
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 }
Trong đoạn mã này:
queryđược hỗ trợ bởiMutableState, một thành phần kết hợp giao diện người dùng thanh tìm kiếm có thể đọc và cập nhật.transformlấyinitialValuecủa một danh sách trống,CoroutineScopevàonUpdatelàm hàm lambda theo sau.- Bên trong khối
onUpdate,remembergiữ trạng tháiresultstrên các lần kết hợp lại trong khiLaunchedEffectchạy. LaunchedEffectlấyquerylàm khoá. Vìqueryđược hỗ trợ bởiMutableState, nên hiệu ứng này sẽ bị huỷ và chạy lại mỗi khiquerythay đổi.- Để tránh gọi API trên mỗi lần nhấn phím, phần nội dung hiệu ứng sẽ đợi 300 mili giây. Nếu truy vấn thay đổi trước khi độ trễ đó hoàn tất, hiệu ứng sẽ bị huỷ và không có lệnh gọi mạng nào được thực hiện.
Bên trong hàm @Composable, bạn cũng có thể sử dụng phương thức nạp chồng @Composable transform. Phương thức này ghi nhớ CoroutineScope trong nội bộ và chạy onUpdate trên Dispatchers.Default theo mặc định. Điều này rất hữu ích khi bạn muốn giảm tải một
phép lấy trạng thái tốn kém (chẳng hạn như lọc hoặc tìm kiếm một danh sách lớn) khỏi
luồng giao diện người dùng chính:
@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... }
Quan sát trạng thái bằng listener
Khi bạn muốn phản ứng với các thay đổi về State của Compose trong một thành phần không phải giao diện người dùng (chẳng hạn như trình quản lý trạng thái, kho lưu trữ hoặc dịch vụ nền) mà không tạo ra State<R> phái sinh, hãy sử dụng listener.
listener là một hàm suspend chạy khối @Composable () -> Unit cho đến khi coroutine gọi bị huỷ, tự động kết hợp lại bất cứ khi nào trạng thái đọc bên trong onUpdate thay đổi:
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... }
Trong ví dụ này, listener quan sát session.value. Khi phiên là FocusSession.Running, LaunchedEffect sẽ đợi cho đến endTimeMillis trước khi đặt lại phiên thành FocusSession.Idle và đăng thông báo hoàn tất. Nếu người dùng tạm dừng hoặc kết thúc phiên sớm, nhánh Running sẽ rời khỏi thành phần và tự động huỷ bộ hẹn giờ. Khi scope bị huỷ, listener sẽ ngừng quan sát.
Mẫu kiểm thử phổ biến
Các mẫu sau đây cho thấy cách sử dụng transform cho logic có điều kiện,
kết hợp nhiều luồng và tạo trạng thái giao diện người dùng trong ViewModel.
Tạo trạng thái có điều kiện
Sử dụng biểu thức if-else hoặc when chuẩn của Kotlin bên trong transform để chuyển đổi giữa các nguồn trạng thái ngược dòng dựa trên điều kiện hiện tại. Vì tính năng theo dõi lượt đọc là động, nên transform chỉ quan sát nhánh chạy trên quá trình kết hợp lại hiện tại:
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 } }
Kết hợp nhiều nguồn trạng thái
Vì Compose tự động theo dõi các lượt đọc trạng thái, nên bạn có thể kết hợp số lượng thuộc tính State<T> và luồng Flow bất kỳ trong một khối transform. Không giống như Flow.combine (chỉ cung cấp các phương thức nạp chồng được nhập cho tối đa 5 luồng trước khi quay lại Array<Any?> không được nhập), việc thêm một dữ liệu đầu vào khác trong transform luôn là một lượt đọc an toàn về kiểu:
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, ) }
Sử dụng transform trong ViewModel
Quá tải transform không kết hợp chạy logic phản ứng trong các lớp không phải giao diện người dùng (chẳng hạn như ViewModel hoặc kho lưu trữ) trong khi sử dụng mô hình trạng thái của Compose. Hàm này chạy miễn là CoroutineScope được cung cấp đang hoạt động, vì vậy, bạn có thể liên kết hàm này với vòng đời của một thành phần bằng cách truyền viewModelScope vào bên trong 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, ) } }
Trong ViewModel này, uiState đọc networkMonitor.isOnline và chuyển đổi linh hoạt giữa việc thu thập mediaRepository.downloadedTracksFlow khi không có mạng và mediaRepository.allTracksFlow khi có mạng, hiển thị một State<HomeUiState> mà các thành phần kết hợp giao diện người dùng có thể đọc trực tiếp. Để biết thêm thông tin chi tiết về cách kết hợp transform với MutableState và SavedStateHandle.saveable, hãy xem bài viết Sử dụng Trạng thái Compose trong ViewModel.
Chọn giữa transform, derivedStateOf và Flow
Hãy sử dụng các nguyên tắc sau để chọn giữa transform, derivedStateOf và Flow của Kotlin:
derivedStateOf đấu với transform
Cả derivedStateOf và transform đều tạo ra một State<T> dẫn xuất tự động theo dõi các trạng thái Compose được đọc bên trong khối của nó.
Chọn giữa các lớp này dựa trên việc bạn đang tính toán giá trị trực tiếp từ các đối tượng State<T> hiện có hay cần các tính năng @Composable như collectAsState, produceState và LaunchedEffect:
| Chức năng | derivedStateOf |
transform |
|---|---|---|
| Kiểu lambda | Biểu thức lambda Kotlin chuẩn (() -> T) |
@Composable lambda (@Composable (R) -> R) |
| Thông tin đầu vào được hỗ trợ | Chỉ đọc State<T> đồng bộ |
State<T>, Kotlin Flow (sử dụng collectAsState), hàm suspend và Callback API (sử dụng produceState) |
| Thực thi và tính nhất quán | Lười và đồng bộ khi .value được đọc; cập nhật tự động trong cùng một ảnh chụp nhanh mà không có initialValue hoặc CoroutineScope |
Chạy một composition đang hoạt động trong CoroutineScope và recomposition không đồng bộ sau khi snapshot được xác nhận; yêu cầu initialValue |
| Trạng thái và hiệu ứng phụ | Không thể gọi remember hoặc API hiệu ứng phụ |
Hỗ trợ các hàm remember, LaunchedEffect, produceState và @Composable có thể sử dụng lại |
Sử dụng derivedStateOf khi tất cả dữ liệu đầu vào đều là đối tượng State<T> và bạn không cần API @Composable:
- Các dẫn xuất đồng bộ, cùng ảnh chụp nhanh: Tính toán một giá trị hoặc danh sách đã lọc từ các thuộc tính
State<T>hiện có mà không cần quản lýCoroutineScopehoặc cung cấp phần giữ chỗinitialValue. VìderivedStateOfđánh giá đồng bộ khi.valueđược đọc, nên phương thức gọi đọc.valuengay sau khi cập nhật trạng thái đầu vào luôn quan sát kết quả đã cập nhật trong cùng một ảnh chụp nhanh. - Giảm tần suất cập nhật của trình quan sát: Tính toán một giá trị thay đổi ít thường xuyên hơn nguồn
State<T>(chẳng hạn như kiểm tra xem độ lệch cuộn có vượt quá ngưỡng hay không). Để biết thêm thông tin, hãy xem tài liệu vềderivedStateOf.
Sử dụng transform khi việc lấy dữ liệu yêu cầu thời gian chạy Compose:
- Kết hợp
State<T>với luồng không đồng bộ: Thu thậpFlowKotlin từ một kho lưu trữ hoặc cơ sở dữ liệu bằng cách sử dụngcollectAsStatecùng với dữ liệu đầu vàoState<T>. - Khởi động lại công việc ngược dòng khi dữ liệu đầu vào thay đổi: Sử dụng
remember(key)bên trong khối để tạo lạiFlowcó tham số chỉ khi một dữ liệu đầu vàoState<T>cụ thể cập nhật. - Chạy hiệu ứng phụ hoặc ghi nhớ trạng thái trung gian: Sử dụng
LaunchedEffect(ví dụ: để gỡ lỗi đầu vào hoặc chạy bộ hẹn giờ),produceState(để tải dữ liệu từ hàmsuspendhoặc gói lệnh gọi lại bằngawaitDispose) hoặcrememberđể giữ trạng thái trong các lần kết hợp lại.
Ví dụ sau đây so sánh derivedStateOf với một khối transform kết hợp State<T> và một kho lưu trữ 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) } }
Trong ví dụ này, hasSearchQuery chỉ kiểm tra trạng thái searchQuery, vì vậy, derivedStateOf sẽ tính toán trạng thái đó một cách lười biếng mà không cần CoroutineScope. Ngược lại, uiState kết hợp preferencesManager.showCompleted và searchQuery với một Flow không đồng bộ từ tasksRepository và sử dụng remember(showCompleted) để tránh tạo lại Flow khi chỉ có searchQuery thay đổi.
Trường hợp sử dụng Flow thay vì transform
transform được thiết kế để biểu thị giá trị hiện tại của trạng thái, chứ không phải là một luồng sự kiện. Nếu một trạng thái thay đổi nhiều lần trước lần kết hợp lại tiếp theo, thì Compose chỉ sử dụng giá trị mới nhất và bỏ qua các bản cập nhật khi giá trị mới bằng (==) giá trị trước đó. Thay vào đó, hãy sử dụng Flow của Kotlin khi logic của bạn phải xử lý mọi giá trị theo thứ tự:
- Tổng và trung bình cộng luỹ tiến: Các phép tính như trung bình động, tổng tích luỹ hoặc nhật ký huỷ (
runningFoldhoặcscan) cần tính từng giá trị một lần. Trong khốitransform, các bản cập nhật nhanh trước khi kết hợp lại sẽ bị bỏ qua, hai lần đọc giống hệt nhau liên tiếp (chẳng hạn như cảm biến phát ra10.0hai lần) không kích hoạt quá trình kết hợp lại và các quá trình kết hợp lại do các trạng thái khác kích hoạt trong cùng một khối có thể tính giá trị hiện tại hai lần. Trước tiên, hãy tính giá trị đang chạy trongFlow, sau đó thu thập kết quả trongtransformnếu bạn cần kết hợp kết quả đó với trạng thái khác. - Ghi nhật ký sự kiện và hàng đợi: Quy trình ghi nhật ký hoạt động tương tác của người dùng, xử lý các yêu cầu mạng trong hàng đợi hoặc ghi lại các giao dịch mà mọi thay đổi trung gian (
A -> B -> ChoặcA -> B -> A) đều phải được ghi lại. - Sự kiện một lần: Các hành động như phát âm thanh hoặc gửi lệnh phần cứng, trong đó việc gửi cùng một lệnh hai lần liên tiếp phải chạy hai lần.
transformyêu cầuinitialValuevà bỏ qua các kết quả đầu ra bằng nhau liên tiếp. - Luồng nhạy cảm với áp suất ngược: Luồng (chẳng hạn như đọc tệp hoặc ổ cắm) trong đó trình tạo nhanh phải tạm ngưng khi trình tiêu thụ bị chậm lại. Các hoạt động ghi trạng thái của Compose không tạm ngưng mà sẽ ghi đè các giá trị trước đó.
Kiểm thử các phép biến đổi
Vì các phép biến đổi dựa vào thời gian chạy Compose và Kotlin Coroutine, nên việc kiểm thử các phép biến đổi này đòi hỏi phải điều phối quá trình thực thi không đồng bộ và kết hợp lại trạng thái. Sử dụng cấu phần phần mềm androidx.appstate.transform:transform-testing để kiểm soát trình lập lịch biểu coroutine và kích hoạt quá trình kết hợp lại trong kiểm thử đơn vị.
Gói androidx.appstate.transform.testing cung cấp các API sau:
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 gói runTest và thực thi khối kiểm thử bằng trình nhận TransformTestScope:
backgroundScope: MộtCoroutineScopeđể chạy các phép biến đổi không được chặn quá trình hoàn tất kiểm thử. Phạm vi này sẽ tự động bị huỷ khi quá trình kiểm thử kết thúc, từ đó dừng quá trình biến đổi.testDispatcher: MộtTestDispatcherđược liên kết vớitestCoroutineScheduler. Truyền giá trị này đến tham sốdispatchercủatransformđể quá trình kết hợp lại chạy trên trình lập lịch biểu kiểm thử thay vì mặc định làDispatchers.Default.advance: Chạy coroutine đang chờ xử lý trên trình lập lịch kiểm thử, gửi thông báo áp dụng ảnh chụp nhanh Compose (Snapshot.sendApplyNotifications) và kết hợp lạitransformđể cập nhật kết quảState.advanceTimeBy(duration)vàadvanceUntilIdle: Chuyển đồng hồ ảo trêntestCoroutineSchedulercho các thao tác phụ thuộc vào thời gian.
Kiểm thử thông tin cập nhật trạng thái
Để kiểm thử transform, hãy truyền backgroundScope và testDispatcher từ TransformTestScope, sau đó gọi advance sau khi cập nhật trạng thái đầu vào hoặc 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) }
Kiểm thử các thao tác dựa trên thời gian
Khi transform sử dụng các hiệu ứng phụ dựa trên thời gian như LaunchedEffect và delay (chẳng hạn như gỡ lỗi cụm từ tìm kiếm), hãy kết hợp advanceTimeBy(duration) với advance:
- Sau khi sửa đổi trạng thái đầu vào, hãy gọi
advanceđể khốitransformkết hợp lại và bắt đầuLaunchedEffect. - Gọi
advanceTimeBy(duration)để chuyển đồng hồ ảo về phía trước, sau đó gọiadvanceđể chạy các tác vụ được lên lịch tại dấu thời gian đó và kết hợp lạitransform.
@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") }
Kiểm thử ViewModel hoặc phần tử giữ trạng thái
Vì transform chạy Composition trong scope được cung cấp và đặt tham số dispatcher mặc định thành Dispatchers.Default, nên các lớp tạo transform phải chấp nhận CoroutineScope và CoroutineDispatcher làm tham số hàm khởi tạo. Trong ViewModel, hãy truyền CoroutineScope được chèn vào hàm khởi tạo ViewModel(coroutineScope) và chuyển tiếp CoroutineDispatcher đến 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, ) ) }
Trong kiểm thử này, việc truyền backgroundScope và testDispatcher vào HomeViewModel sẽ liên kết composition viewModelScope và transform với trình lập lịch kiểm thử, vì vậy, mỗi lệnh gọi advance sẽ xử lý một cách tất định các lượt phát từ FakeMediaRepository và FakeNetworkMonitor. Để biết thêm các mẫu về cách sử dụng SavedStateHandle.saveable và Snapshot.withMutableSnapshot trong ViewModel, hãy xem bài viết Sử dụng Trạng thái Compose trong ViewModel.