找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1817

积分

0

好友

235

主题
发表于 15 小时前 | 查看: 1| 回复: 0

关于 mutableStateOfStateFlow 的争论只是开始。在 Jetpack Compose 中设计一个健壮的状态管理系统,需要建立在单一数据源和单向数据流等原则之上的清晰架构。但我们如何将这些原则转化为代码?如何设计 State 数据类和 Intent 密封接口?加载初始数据的最佳实践又是什么——是 initonStart,还是其他完全不同的方法?

你将学到什么

我们将讨论完整的、受 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 组合项都会重组。
  • ButtononClick 是间接的,或者说 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 在重组过程中缓存任何值。remembermutableStateOf 是完全独立的概念。

  • 没有 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  
}

优点

  • 代码量最少
  • 在 UI 中无需转换

缺点

  • 没有内置的线程安全更新

加载初始数据

使用 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




上一篇:Android应用逆向分析与安全检测利器:AppMessenger v4.6.3 开源工具详解
下一篇:掌握Python可变参数:深入理解*args与**kwargs的用法与场景
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-1 19:27 , Processed in 0.490059 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表