第八篇:ViewModel + Compose——生产级状态管理实践

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 函数,它会:

  1. 获取当前 LocalViewModelStoreOwner(通常是 Activity/Fragment)
  2. 从中获取或创建 ViewModel 实例
  3. 在配置变更后保持实例
  4. 在 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 副作用详解------生命周期管理的正确姿势。

相关推荐
恋猫de小郭6 小时前
Amper 正式转正 Kotlin Toolchain ,Gradle 未来何去何从
android·前端·flutter
plainGeekDev7 小时前
ButterKnife → ViewBinding
android·java·kotlin
成都大菠萝21 小时前
Android Car CarProperty 车辆信号链路
android
敲代码的鱼21 小时前
PDF 预览与签名批注写回 支持安卓 iOS 鸿蒙 UTS插件
android·前端·ios
时光足迹1 天前
uni-app 视频通话实战:康复师与患者视频问诊的 6 个致命 Bug 与解决方案
android·ios·uni-app
Coffeeee1 天前
闲聊几句,Android老哥们,你们多久没做技改需求了
android·程序员·代码规范
萝卜er1 天前
Fragment 生命周期与状态恢复-《Android深水区(四)》
android
萝卜er1 天前
Intent 显式、隐式与 PendingIntent-《Android深水区(五)》
android
Kapaseker1 天前
一文吃透 Kotlin 集合操作符
android·kotlin