关于 mutableStateOf 和 StateFlow 的争论只是开始。在 Jetpack Compose 中设计一个健壮的状态管理系统,需要建立在单一数据源和单向数据流等原则之上的清晰架构。但我们如何将这些原则转化为代码?如何设计 State 数据类和 Intent 密封接口?加载初始数据的最佳实践又是什么——是 init、onStart,还是其他完全不同的方法?
你将学到什么
我们将讨论完整的、受 MVI 启发的蓝图,以及如何在项目中实现它:
- State 和 Intent 类
- ViewModel 作为状态持有者的角色
- 加载数据的最佳实践
状态管理的 5 条规则
- 单向数据流:状态从你的逻辑层向下流向 UI,事件从 UI 向上流向你的逻辑层。
- 单一数据源:给定屏幕的所有状态都由单一位置(如 ViewModel)拥有和存储。
- 状态不可变性:状态不应被直接修改;相反,你应该创建一个包含更新值的新副本。
- 易于测试:业务逻辑与 UI 分离,使得无需 UI 即可轻松对状态变化进行单元测试。
- 持久化配置变更:状态保存在生命周期感知组件(如 ViewModel)中,因此可以经受住屏幕旋转等事件。
什么是 STATE?
- 状态 只是一个可以随时间变化的值。
- UI 状态 是一个可以随时间变化且该值会影响我们 UI 的值。
- Compose 状态 是一个可以随时间变化且该值会影响我们 Compose UI 的值。
State 在 Compose 中如何工作?
State 是一种机制,用于通知正确的或依赖的组合项关于变更的信息。Compose 状态的变更将再次调用那些正确的组合函数来读取它(重组)。
@Composable
fun MyCounter(modifier: Modifier = Modifier){
var counter by remember{
mutableStateOf(0)
}
Button(
onClick = { counter++ }
){
Text(
text = "Counter: $counter"
)
}
}
- 每次
counter 值改变时,Text 组合项都会重组。
Button 的 onClick 是间接的,或者说 lambda 函数并不直接以 counter 结束。
- 因此,
Text 组合项只在每次 Compose 变更后重组。
有状态 vs 无状态组合项
有状态组合项 是指创建并管理自身状态的组合项。它是自包含的,通常只需最少的设置即可“开箱即用”。
@Composable
fun MyCounter(modifier: Modifier = Modifier){
var counter by remember{
mutableStateOf(0)
}
Button(
onClick = { counter++ }
){
Text(
text = "Counter: $counter"
)
}
}
无状态组合项 本身不持有任何状态。它接收所有需要显示的数据作为参数,并将任何事件(如点击)传递给其父级处理。这被称为 状态提升。
@Composable
fun MyCounter(
counter: Int,
onClick: () -> Unit
){
Button(
onClick = onClick
){
Text(
text = "Counter: $counter"
)
}
}
Remember 和 Compose State
remember 在重组过程中缓存任何值。remember 和 mutableStateOf 是完全独立的概念。
- 没有
remember,当组合函数被再次调用或在重组期间,compose 状态将被重置。
- 它无法在配置变更中存活。
val counter1: IntState = remember{
mutableIntStateOf(0)
}
val counter2: IntState by remember{
mutableIntStateOf(0)
}
Text(counter1.value.toString())
Text(counter2.toString())
rememberSaveable 将值缓存在一个 Bundle 中,它将能在配置变更甚至进程死亡后存活。
val counter1: IntState = rememberSaveable{
mutableIntStateOf(0)
}
val counter2: IntState by rememberSaveable{
mutableIntStateOf(0)
}
Text(counter1.value.toString())
Text(counter2.toString())
单向数据流
状态持有者 是一个管理我们状态的类,它是 单一数据源(当你想要更新状态时,你只需要关注这个类)。
状态持有者 — — — — — — — — — — —状态 — — — — — — — — — — — — — — — → UI
状态持有者 ←— — — — — — — — — — UI 事件 —— — — — — — — — — — — — — UI
val username = stateHolder.username
val profilePictureUrl = stateHolder.profilePictureUrl
val posts = stateHolder.posts
val isLoading = stateHolder.isLoading
val category = stateHolder.category
val bio = stateHolder.bio
val followerCount = stateHolder.followerCount
//...
ProfileScreen(
username = username,
profilePictureUrl = profilePictureUrl,
posts = posts,
isLoading = isLoading,
category = category,
bio = bio,
followerCount = followerCount,
//...
onSelectCategory = stateHolder :: onSelectCategory,
onRefresh = stateHolder :: onRefresh,
onLikePost = stateHolder :: onLikePost,
onCommentClick = stateHolder :: onCommentClick,
//...
)
基于 MVI 的状态管理系统
视图 (组合项) → 意图 (密封接口) → ViewModel → 模型,然后
视图 (组合项) ← 状态 (数据类) ← ViewModel ← 模型
模型
- 模型是一个广义的术语,在 MVVM/MVI 模式中,它指的是包含项目范围需求的数据持有者类。
data class Profile(
val username: String,
val pictureUrl: String,
val bio: String,
val followerCount: Int
)
data class User(
val id: String,
val username: String
)
data class Post(
val id: String,
val content: String,
val commentCount: Int,
val author: User
)
状态
- 单一数据类有助于捆绑相关的状态。
- ViewModel 只需要暴露一个单一实例。
- 只允许使用
val。
意图
sealed interface ProfileAction {
data class OnSelectCategory(val category: PostCategory): ProfileAction
data object OnRefresh: ProfileAction
data class OnLikePost(val postId: String): ProfileAction
data object OnCommentClick: ProfileAction
}
ViewModel
- 你 UI 的大脑。
- 接收意图并更新状态。
- 在 Android 开发中具有特殊作用,用于勾勒 Activity 的生命周期。
class ProfileViewModel(
private val postRepository: PostRepository
): ViewModel() {
private val _state = MutableStateFlow(ProfileState())
val state = _state.asStateFlow()
fun onAction(action: ProfileAction) {
when (action) {
ProfileAction.OnCommentClick -> onCommentClick()
is ProfileAction.OnLikePost -> onLikePost(action.postId)
ProfileAction.OnRefresh -> onRefresh()
is ProfileAction.OnSelectCategory -> onSelectCategory(action.category)
}
}
private fun onCommentClick() { /**/ }
private fun onLikePost(postId: String) { /**/ }
private fun onRefresh() { /**/ }
private fun onSelectCategory(category: PostCategory) { /**/ }
}
- 完全控制状态更新。
- 状态更新逻辑与 UI 代码解耦,允许对其进行单元测试。
private fun onRefresh() {
viewModelScope.launch {
_state.update {
it.copy( isLoading = true )
}
val posts = postRepository.loadPosts()
_state.update {
it.copy( isLoading = false, posts = posts )
}
}
}
视图
错误的方法
@Composable
private fun ProfileScreen(
viewModel: ProfileViewModel = koinViewModel(),
){
val state by viewModel.state.collectAsStateWithLifecycle()
SwipeRefreshLayout(
onRefresh = {
viewModel.onAction(ProfileAction.OnRefresh)
},
modifier = Modifier.fillMaxSize()
){
// Profile screen UI
}
}
// 这会破坏预览组合项
更好的方法
@Composable
fun ProfileScreenroot(
navController: NavHostController,
viewModel: ProfileViewModel = koinViewModel(),
){
val state by viewModel.state.collectAsStateWithLifecycle()
ProfileScreen(
state = state,
onAction = viewModel::onAction
)
}
@Composable
private fun ProfileScreen(
state: ProfileState,
onAction: (ProfileAction) -> Unit
){
SwipeRefreshLayout(
onRefresh = {
onAction(ProfileAction.OnRefresh)
},
modifier = Modifier.fillMaxSize()
){
// Profile screen UI
}
}
// 如果我们为 ProfileScreen() 构造预览,它不会破坏
// 因为现在它是一个无状态组合项
选择哪种可观察状态持有者?
mutableStateOf() vs StateFlow vs LiveData
- LiveData 相对于其他两者没有优势。
- 所以,是
mutableStateOf() 对 StateFlow。
使用 StateFlow
class ProfileViewModel(): ViewModel() {
private val _state = MutableStateFlow(ProfileState())
val state = _state.asStateFlow()
fun updateState() {
_state.update {
it.copy(isLoading = true)
}
}
}
@Composable
fun ObserveState(modifier: Modifier = Modifier) {
val viewModel = koinViewModel<ProfileViewModel>()
val state by viewModel.collectAsStateWithLifecycle()
}
优点
- 内置的线程安全更新
- 支持响应式编程
- 与协程良好集成
缺点
使用 MutableStateOf
class ProfileViewModel(): ViewModel() {
var state by mutableStateOf(ProfileState())
private set
fun updateState() {
state = state.copy(isLoading = true)
}
}
@Composable
fun ObserveState(modifier: Modifier = Modifier) {
val viewModel = koinViewModel<ProfileViewModel>()
val state = viewModel.state
}
优点
缺点
加载初始数据
使用 LaunchedEffect
LaunchedEffect(true){
viewModel.loadData()
}
使用 init
- 简单直观
- 能在配置变更中存活
- 这导致加载初始数据与 viewModel 的创建耦合
class ProfileViewModel(): ViewModel(){
private val _state = MutableStateFlow(ProfileState())
val state = _state.asStateFlow()
init{
viewModelScope.launch{
loadProfile()
}
}
}
使用 onStart
- 在测试中完全控制
- 每个屏幕只调用一次
- 需要一个标志位
private var hasLoadedProfile = false
val state = _state
.onStart {
if (!hasLoadedProfile) {
loadProfile()
hasLoadedProfile = true
}
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000L),
_state.value
)
结论
正如我们所看到的,Jetpack Compose 中健壮的状态管理不是寻找灵丹妙药,而是关于严谨的架构。通过拥抱 UDF 的原则,你可以构建可预测、可测试且没有困扰复杂应用程序的细微错误的 UI。受 MVI 启发的蓝图为我们指明了一条清晰的道路:将 UI 的属性捆绑到一个不可变的 State 数据类中,将用户交互捆绑到一个 Action 密封接口中。让你的 ViewModel 成为单一数据源,以受控的、线程安全的方式协调状态更新——最好利用 StateFlow 的强大功能。最后,通过设计无状态组合项,你可以创建一个可重用、可预览的 UI 组件库。如果你想寻找更多类似的避坑指南和实战经验,可以关注云栈社区的讨论。
原文链接: https://kabi20.medium.com/compose-state-management-in-kotlin-07cea6111c68