Android Compose 多接口數據依賴管理:ViewModel 狀態共享最佳實踐
📌 問題背景
在 Jetpack Compose 開發中,經常遇到以下場景:
- 頁面由多個獨立接口數據組成(如
Part1
、Part2
) Part2
的某些 UI 需要依賴Part1
的數據(如顯示Part1
的某個字段)- 如何高效管理數據依賴,避免重復請求或狀態混亂?
本文將介紹 3 種主流方案,并分析其適用場景。
🚀 方案 1:共享 ViewModel(推薦同一頁面使用)
核心思想
- 使用同一個
ViewModel
管理Part1
和Part2
的數據 Part2
直接讀取Part1
的狀態,無需額外接口調用
代碼實現
class SharedViewModel : ViewModel() {// Part1 數據private val _part1Data = mutableStateOf<Part1Data?>(null)val part1Data: State<Part1Data?> = _part1Data// Part2 數據private val _part2Data = mutableStateOf<Part2Data?>(null)val part2Data: State<Part2Data?> = _part2Datafun loadAllData() {viewModelScope.launch {_part1Data.value = repo.getPart1Data() // 先加載 Part1_part2Data.value = repo.getPart2Data() // 再加載 Part2}}
}@Composable
fun ParentScreen(viewModel: SharedViewModel = viewModel()) {val part1Data by viewModel.part1Dataval part2Data by viewModel.part2DataLaunchedEffect(Unit) { viewModel.loadAllData() }Column {Part1UI(part1Data)Part2UI(part2Data, part1Data) // 將 part1Data 傳遞給 Part2}
}@Composable
fun Part2UI(part2Data: Part2Data?, part1Data: Part1Data?) {Text("Part2 數據: ${part2Data?.value}")part1Data?.let { Text("來自 Part1 的數據: ${it.someField}") }
}
? 優點
- 狀態集中管理,避免數據不一致
- 天然支持依賴關系(如
Part2
依賴Part1
的某個字段) - 代碼簡潔,適合同一頁面內的組件通信
? 缺點
- 不適用于跨頁面場景(如
Part1
和Part2
屬于不同屏幕)
🌉 方案 2:參數傳遞(跨頁面場景)
核心思想
- 通過 Navigation 或 Composable 參數顯式傳遞
Part1
的數據 - 適用于
Part1
和Part2
屬于不同頁面的情況
代碼實現(Navigation Compose)
// 導航定義
NavHost(navController, startDestination = "part1") {composable("part1") { Part1Screen { part1Data ->navController.navigate("part2/${part1Data.id}") }}composable("part2/{part1Id}") { backStackEntry ->val part1Id = backStackEntry.arguments?.getString("part1Id") ?: ""Part2Screen(part1Id)}
}// Part1 頁面
@Composable
fun Part1Screen(onNavigate: (Part1Data) -> Unit) {val part1Data by viewModel<Part1ViewModel>().part1DataButton(onClick = { part1Data?.let(onNavigate) }) {Text("進入 Part2")}
}// Part2 頁面
@Composable
fun Part2Screen(part1Id: String) {val part2Data by viewModel<Part2ViewModel>().part2DataText("Part1 的 ID: $part1Id")Text("Part2 數據: ${part2Data?.value}")
}
? 優點
- 明確的數據流向,適合跨頁面通信
- 符合 Compose Navigation 最佳實踐
? 缺點
- 需要手動管理參數傳遞
- 如果數據較復雜,可能需序列化(如
Parcelable
)
🌐 方案 3:CompositionLocal(全局共享)
核心思想
- 使用
CompositionLocal
提供全局可訪問的數據 - 適合 主題、用戶信息等全局狀態,但不推薦濫用
代碼實現
// 定義全局數據
val LocalPart1Data = staticCompositionLocalOf<Part1Data?> { null }// 在根 Composable 提供數據
@Composable
fun App() {val part1Data by viewModel<Part1ViewModel>().part1DataCompositionLocalProvider(LocalPart1Data provides part1Data) {NavHost(...) // 所有子組件可讀取 part1Data}
}// 任意子組件讀取
@Composable
fun Part2Component() {val part1Data = LocalPart1Data.currentText("Part1 數據: ${part1Data?.someField}")
}
? 優點
- 避免層層傳遞參數(Prop Drilling)
- 適合 真正全局 的數據(如用戶信息、配置)
? 缺點
- 過度使用會導致代碼難以維護
- 應僅用于 低頻變更 的數據
📊 方案對比
方案 | 適用場景 | 優點 | 缺點 |
---|---|---|---|
共享 ViewModel | 同一頁面/同一 NavGraph | 狀態集中,依賴管理方便 | 不適用于跨頁面 |
參數傳遞 | 跨頁面跳轉 | 明確數據流,符合 Navigation 規范 | 需手動管理參數 |
CompositionLocal | 全局數據(如用戶信息) | 避免 Prop Drilling | 濫用會導致代碼難以維護 |
🎯 終極選擇建議
- 同一頁面內組件共享數據 → 方案 1(共享 ViewModel)
- 跨頁面數據傳遞 → 方案 2(參數傳遞 + Navigation)
- 真正全局的數據(如用戶信息)→ 方案 3(CompositionLocal)
💡 高級技巧
- 如果
Part2
必須在Part1
加載完成后才能請求,可使用LaunchedEffect
+ 狀態監聽:LaunchedEffect(part1Data) {if (part1Data != null) {viewModel.loadPart2(part1Data.id)} }
- 使用
Flow.combine
合并多個接口數據流:val uiState = combine(part1Flow, part2Flow) { p1, p2 ->UiState.Success(p1, p2) }.stateIn(viewModelScope, SharingStarted.Lazily, UiState.Loading)
📚 總結
在 Compose 中管理多接口數據依賴時,優先選擇共享 ViewModel,其次是參數傳遞,謹慎使用 CompositionLocal
。正確管理狀態依賴能讓代碼更清晰、更易維護!