8.1 ViewModel 在 Compose 中的角色
在 Android 传统开发中,ViewModel 的作用是持久的屏幕状态管理 ,它在配置变更(如屏幕旋转)后存活。在 Compose 中,这个角色没有变化------ViewModel 依然是业务逻辑与 UI 展示之间的桥梁。

8.2 viewModel() 函数
kotlin
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun ProductScreen(
categoryId: String,
viewModel: ProductViewModel = viewModel() // 自动获取或创建
) {
// ...
}
viewModel() 函数是一个 Composable 函数,它会:
- 获取当前
LocalViewModelStoreOwner(通常是 Activity/Fragment) - 从中获取或创建 ViewModel 实例
- 在配置变更后保持实例
- 在 ViewModelStoreOwner 销毁后自动清理
带参数工厂
kotlin
// 如果 ViewModel 需要构造参数
class ProductViewModel(
private val productId: String,
private val repository: ProductRepository
) : ViewModel() {
// ...
}
// 创建 Factory
class ProductViewModelFactory(
private val productId: String,
private val repository: ProductRepository = ProductRepository()
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ProductViewModel(productId, repository) as T
}
}
// 使用
@Composable
fun ProductDetailScreen(productId: String) {
val factory = remember(productId) {
ProductViewModelFactory(productId)
}
val viewModel: ProductViewModel = viewModel(factory = factory)
// ...
}
或者使用 SavedStateHandle(推荐的方式,无需 Factory):
kotlin
class ProductViewModel(
savedStateHandle: SavedStateHandle,
private val repository: ProductRepository = ProductRepository()
) : ViewModel() {
private val productId: String = savedStateHandle["productId"]!!
// SavedStateHandle 中的值会自动在配置变更和进程终止后恢复
var searchQuery by savedStateHandle.getStateFlow("searchQuery", "")
private set
}
8.3 UiState 设计模式
密封类模式
kotlin
sealed interface ProductUiState {
data object Loading : ProductUiState
data class Success(
val product: Product,
val isFavorite: Boolean = false
) : ProductUiState
data class Error(val message: String) : ProductUiState
}
class ProductViewModel : ViewModel() {
private val _uiState = MutableStateFlow<ProductUiState>(ProductUiState.Loading)
val uiState: StateFlow<ProductUiState> = _uiState.asStateFlow()
fun loadProduct(id: String) {
viewModelScope.launch {
_uiState.value = ProductUiState.Loading
try {
val product = repository.getProduct(id)
_uiState.value = ProductUiState.Success(product)
} catch (e: Exception) {
_uiState.value = ProductUiState.Error(e.message ?: "Unknown error")
}
}
}
}
// UI 使用
@Composable
fun ProductScreen(viewModel: ProductViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is ProductUiState.Loading -> LoadingIndicator()
is ProductUiState.Success -> ProductDetail(state.product, state.isFavorite)
is ProductUiState.Error -> ErrorScreen(state.message)
}
}
单一数据类模式
kotlin
data class ProductUiState(
val product: Product? = null,
val isLoading: Boolean = false,
val error: String? = null,
val isFavorite: Boolean = false,
val quantity: Int = 1
)
class ProductViewModel : ViewModel() {
private val _uiState = MutableStateFlow(ProductUiState())
val uiState: StateFlow<ProductUiState> = _uiState.asStateFlow()
fun setQuantity(qty: Int) {
_uiState.update { it.copy(quantity = qty.coerceIn(1, 99)) }
}
}
如何选择:
| 模式 | 适合场景 |
|---|---|
sealed interface |
互斥的 UI 状态(Loading / Success / Error) |
| 单一 data class | 可组合的 UI 属性(多个独立字段) |
| 混合 | sealed interface 包裹 data class(常用推荐) |
推荐的混合模式
kotlin
// 屏幕级 UiState
sealed interface ScreenState<out T> {
data object Loading : ScreenState<Nothing>
data class Success<T>(val data: T) : ScreenState<T>
data class Error(val message: String) : ScreenState<Nothing>
}
// 页面具体状态
data class HomeUiData(
val banners: List<Banner> = emptyList(),
val products: List<Product> = emptyList(),
val searchHistory: List<String> = emptyList()
)
// ViewModel
class HomeViewModel : ViewModel() {
private val _uiState = MutableStateFlow<ScreenState<HomeUiData>>(
ScreenState.Loading
)
val uiState: StateFlow<ScreenState<HomeUiData>> = _uiState.asStateFlow()
}
// Composable
@Composable
fun HomeScreen(viewModel: HomeViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is ScreenState.Loading -> LoadingIndicator()
is ScreenState.Success -> HomeContent(state.data)
is ScreenState.Error -> ErrorView(state.message)
}
}
8.4 事件处理(一次性事件)
Snackbar、导航、Toast 等一次性事件不需要状态来驱动 UI 展示:
kotlin
// ViewModel
class OrderViewModel : ViewModel() {
private val _events = Channel<OrderEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow()
fun placeOrder() {
viewModelScope.launch {
try {
repository.placeOrder(cart)
_events.send(OrderEvent.OrderPlaced("Order #12345 confirmed"))
} catch (e: Exception) {
_events.send(OrderEvent.Error(e.message ?: "Order failed"))
}
}
}
}
sealed interface OrderEvent {
data class OrderPlaced(val message: String) : OrderEvent
data class Error(val message: String) : OrderEvent
}
// Composable
@Composable
fun OrderScreen(viewModel: OrderViewModel = viewModel()) {
val snackbarHostState = remember { SnackbarHostState() }
// 收集一次性事件
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is OrderEvent.OrderPlaced -> {
snackbarHostState.showSnackbar(event.message)
}
is OrderEvent.Error -> {
snackbarHostState.showSnackbar(event.message)
}
}
}
}
// ...
}
Channel vs SharedFlow :一次性事件用
Channel(不保留历史),UI 状态用StateFlow(保留最新值)。使用 Channel 可以防止屏幕旋转后重新播放旧事件。
8.5 实战:完整的列表搜索+收藏
kotlin
// ViewModel
class ProductListViewModel(
private val repository: ProductRepository = ProductRepository()
) : ViewModel() {
private val _uiState = MutableStateFlow(ProductListUiState())
val uiState: StateFlow<ProductListUiState> = _uiState.asStateFlow()
private val _events = Channel<ProductListEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow()
init {
loadProducts()
}
fun onSearchQueryChanged(query: String) {
_uiState.update { it.copy(searchQuery = query) }
}
fun onSearch() {
loadProducts()
}
fun onToggleFavorite(productId: String) {
_uiState.update { state ->
state.copy(
products = state.products.map { product ->
if (product.id == productId) {
product.copy(isFavorite = !product.isFavorite)
} else product
}
)
}
viewModelScope.launch {
repository.toggleFavorite(productId)
_events.send(ProductListEvent.FavoriteToggled)
}
}
fun onRefresh() {
loadProducts()
}
private fun loadProducts() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
try {
val query = _uiState.value.searchQuery
val products = repository.searchProducts(query)
_uiState.update { it.copy(products = products, isLoading = false) }
} catch (e: Exception) {
_uiState.update { it.copy(error = e.message, isLoading = false) }
}
}
}
}
data class ProductListUiState(
val products: List<Product> = emptyList(),
val searchQuery: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
sealed interface ProductListEvent {
data object FavoriteToggled : ProductListEvent
}
// Composable
@Composable
fun ProductListScreen(
viewModel: ProductListViewModel = viewModel(),
onProductClick: (String) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is ProductListEvent.FavoriteToggled -> {
snackbarHostState.showSnackbar("收藏状态已更新")
}
}
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
SearchTopBar(
query = uiState.searchQuery,
onQueryChange = viewModel::onSearchQueryChanged,
onSearch = viewModel::onSearch
)
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
when {
uiState.isLoading && uiState.products.isEmpty() -> LoadingIndicator()
uiState.error != null && uiState.products.isEmpty() ->
ErrorView(uiState.error!!, onRetry = viewModel::onRefresh)
else -> ProductList(
products = uiState.products,
onProductClick = onProductClick,
onToggleFavorite = viewModel::onToggleFavorite,
onRefresh = viewModel::onRefresh,
isRefreshing = uiState.isLoading
)
}
}
}
}
8.6 ViewModel 在 Compose 中的陷阱
不要在 Composable 中创建 ViewModel
kotlin
// ❌ 错误:每次重组都可能创建新的 ViewModel
@Composable
fun BadScreen() {
val viewModel = remember { ProductViewModel() }
// ...
}
// ✅ 正确:使用 viewModel() 函数,它由 ViewModelStoreOwner 管理
@Composable
fun GoodScreen() {
val viewModel: ProductViewModel = viewModel()
// ...
}
不要在 ViewModel 中持有 Composable 引用
kotlin
// ❌ 错误:ViewModel 不能持有 Activity/Context/Composable 引用
class BadViewModel : ViewModel() {
var someState by mutableStateOf(...) // 不推荐在 VM 中使用
}
// ✅ 正确
class GoodViewModel : ViewModel() {
private val _state = MutableStateFlow(...)
val state: StateFlow<...> = _state.asStateFlow()
}
不要忘记清理资源
kotlin
class MyViewModel : ViewModel() {
init {
viewModelScope.launch {
// viewModelScope 的生命周期跟随 ViewModel
// 不需要手动取消协程
}
}
}
8.7 本章小结
| 内容 | 要点 |
|---|---|
| viewModel() | Compose 中获取 ViewModel 的标准函数 |
| UiState 设计 | sealed interface 或 data class,保持不可变 |
| 一次性事件 | 用 Channel,不用 StateFlow |
| 混合模式 | ScreenState<T> + UiData 联合使用 |
| 配置变更 | ViewModel 天然支持,搭配 rememberSaveable |
| 生命周期 | collectAsStateWithLifecycle 防止后台更新 |
下一篇:Compose 副作用详解------生命周期管理的正确姿势。