一、概念

| 概念 | 基于单向数据流,数据永远在一个环形结构中单向流动,便于追踪测试。用户事件→业务罗技→更新状态→界面重组。 |
| 通信 | View→ViewModel:将用户操作以Intent形式通知给ViewModel,监听ViewModel中State的变化会自动更新到UI。 ViewModel→Model:对Intent类型分类判断做对应的逻辑处理,调用Model获取数据。 ViewModel→View:会获取的数据更新到State,State的变化会自动更新View。 |
- 模型 Model:处理数据的逻辑和存取。
- 视图 View:展示界面和数据的状态。
- 意图 Intent:代表用户的操作(点击、输入、获取列表数据等)。
- 状态 State:反应数据当前值。
1.1 唯一可信数据源
为了解决 MVVM 中 UI 订阅多个分散的状态(ViewModel中的LiveData/Flow)导致各种数据并行更新或数据相互依赖时,无法清晰掌握整个页面的状态。MVI使用 UiState 将所有状态整合在一处(通过 data class 实现),UI刷新只依赖这一个数据源。
1.2 数据单向流动
DataBinding 数据模型和视图一方发生变化就会同步到另一方,数据的流动是双向的,这样不便于追踪测试。MVI强调数据的源头只有一个,目的地也只有一个。数据从 Data Layer 流向 UI Layer 的 ViewModel 中,ViewModel 将数据转换成 State 给 UI Element 更新。
1.3 事件驱动
MVVM 没有约束 View 和 ViewModel 的交互方式,MVI将用户操作统一封装到 Intent 实现了屏蔽(通过密封接口实现),View只能发送已被定义好的事件让 ViewModel 统一处理,通过对事件类型的识别做出相应的业务处理并更新状态,采用 Channel 保证并发安全。
二、单 Activity 架构的问题
Compose 通过 AndroidComposeView 来与 Activity 交互,使用单 Activity 页面跳转都能在 Compose 内部完成。Navigation不仅支持 View 的 单Activity+多Fragment 架构,也支持 Compose 的 单Activity+多Composable 架构。
2.1 ViewModel的销毁
Compose 中的 viewModel() 函数可以从任何组合项中获取 ViewModel,考虑到函数的生命周期和作用域,应在屏幕级组合函数中获取 ViewModel 实例,也就是被 Activity、Navigation目的地调用的根级组合项。不要直接将 ViewModel 实例传递给子组合项用,而是传递子组合项所需要的数据或函数(即状态提升)。
- 如果根组合项托管在 Activity 中,不同组合项中调用 viewModel() 获取相同类型的 ViewModel 将会是同一个实例。因为是 单Activity 架构,绑定的作用域是同一个ViewModelStoreOwner,也因此 ViewModel 的生命周期不会随组合项的销毁而回收。
- 如果根组合项托管在 Navigstion 目的地中,不同组合项中调用 viewModel() 获取相同类型的 ViewModel 是不同的实例。因为作用域被限定在了目的地,ViewModel的生命周期会跟随目的地从返回站中弹出而清除。
2.2 处理生命周期
2.2.1 可组合项的生命周期处理
对于组合函数的生命周期:onActive 首次挂载到组件树、onCommit 重组刷新、onDispose 从组件树上移除,可以通过附带效应来监听。
| LaunchedEffect | 第一次调用Compose函数时执行(首次进入页面)。 |
| DisposableEffect | 需要重写 onDispose() 函数当页面退出时调用(退出页面时释放资源)。 |
| SideEffect | Compose函数每次执行都会调用该方法(每次重组时)。 |
2.2.2 Activity 的生命周期获取
三、搭建项目
3.1 定义界面状态 UiState
- 命名采用:页面名称 + UiState。
- 一般同 ViewModel 写在同一个 .kt 文件中,也可以单独写在一个 .kt 文件中。
- 采用 data class 因为自带 copy() 功能,非常方便更新部分属性。界面刷新用到的状态全都定义成属性,集中在一起实现唯一可信数据源。属性使用 val 确保不可变。根据多个状态派生出来的状态,定义在 data class 的类体中。
- UI 所需要的某些状态若是相互独立,不要定义在同一个数据类中,刷新频率高的那个会造成低的频繁更新。(属性内容无变化会跳过重组也还好,分开更方便阅读)
data class DemoUiState(
val isLoading: Boolean = false,
val success: List<DemoBean> = emptyList(),
val isLogin: Boolean = false, //是否登录
val isPremium: Boolean= false //是不是会员
) {
val canDownload: Boolean = isLogin && isPremium //派生状态
}
3.2 定义用户事件 UiEffect
- 命名采用:页面名称 + UiEffect。
- 一般同 ViewModel 写在同一个 .kt 文件中,也可以单独写在一个 .kt 文件中。
- 采用 sealed interface 除了保证类型受控优化 when 判断,子类不带参数就定义成 data object 类型方便复用(用object是因为创建的实例无状态区别,data object 是它的优化版,不会打印必要信息)。将可能的用户行为全部定义成子类。
sealed interface DemoUiEffect {
//按钮点击
data class OnClick(val url: String) : DemoUiEffect
//初始化数据
object InitData: DemoUiEffect
}
3.3 定义界面事件 UiEvent
- 命名采用:页面名称 + UiEvent。
- 一般同 ViewModel 写在同一个 .kt 文件中,也可以单独写在一个 .kt 文件中。
sealed interface DemoUiEvent {
data class ShowToast(val str: String) : DemoUiEvent
data class ShowDialog(val str: String) : DemoUiEvent
}
3.4 处理用户事件+更新状态+发送界面事件(ViewModel)
Compose 向外部(ViewModel)发送用户事件属于副作用,涉及并发安全问题(多核心优化重组,可能执行在非UI线程),因此考虑协程间通信使用 Channel 来实现。
- 事件只能被消费一次,不能回放造成粘性事件。(排除 StateFlow)
- 事件必须执行(消费代码若是执行在了生产后,也不能丢弃值)。(排除SharedFlow)
ViewModel 向 UI 发送界面事件,像弹 Toast/Dialog 没有订阅者就应该丢弃事件,因此使用事件流 SharedFlow 来实现。
class DemoViewModel(
private val repository: DemoRepository
) : ViewModel() {
//暴露给UI订阅的界面状态
var uiState by mutableStateOf(DemoUiState())
private set
//暴露给UI订阅的界面事件
private val _uiEvent = MutableSharedFlow<DemoUiEvent>()
val uiEvent = _uiEvent.asSharedFlow()
//定义用来通信的Channel
private val uiEffect = Channel<DemoUiEffect>()
//初始化时就启动用户事件处理
init { handleEvent() }
//处理用户事件
private fun handleEvent() {
viewModelScope.launch {
uiEffect.consumeAsFlow().collect { event ->
when(event) {
is DemoUiEffect.OnClick -> _uiEvent.emit(DemoUiEvent.ShowToast("点击了按钮"))
DemoUiEffect.InitData -> initData()
}
}
}
}
//暴露给UI发送用户事件(比直接在UI中获取Channel发送方便)
fun dispatchEvent(event: DemoUiEffect) {
viewModelScope.launch {
uiEffect.send(event)
}
}
//(在业务代码里)更新状态和发送界面事件
private suspend fun initData() {
//状态设为加载中
uiState = uiState.copy(isLoading = true)
runCatching {
repository.getData()
}.onSuccess { data ->
//赋值成功结果,取消加载中
uiState = uiState.copy(success = it, isLoading = false)
}.onFailure {
//取消加载中,并弹Toast
uiState = uiState.copy(isLoading = false)
_uiEvent.emit(DemoUiEvent.ShowToast(it.message.toString()))
}
}
override fun onCleared() {
super.onCleared()
uiEffect.close() //释放资源
}
}
3.5 响应状态+处理界面事件+发送用户事件(UI)
- 在屏幕级组合项获取ViewModel。
- 当 Activity 处于可交互期间(resume)才需要处理一次性事件。
@Composable
fun MainScreen(
viewModel: DemoViewModel = viewModel() //普通获取VM
// viewModel: DemoViewModel = viewModel(factory = DemoViewModelFactory(DemoRepository(DemoDataSource()))) //带参获取VM
) {
//发送用户事件(这里的数据初始化只需要执行一次,避免每次重组都执行)
LaunchedEffect(Unit) {
viewModel.dispatchEvent(DemoUiEffect.InitData)
}
//只在Activity处于可交互时处理事件
LifecycleResumeEffect(Unit) {
lifecycleScope.launch {
viewModel.uiEvent.collect { event ->
when (event) {
is DemoUiEvent.ShowDialog -> {}
is DemoUiEvent.ShowToast -> Toast.makeText(APP.context, event.str, Toast.LENGTH_SHORT).show()
}
}
}
//必须调用,可清理资源
onPauseOrDispose {}
}
//读取状态并处理
Content(
data = viewModel.uiState.success,
onClick = { viewModel.dispatchEvent(DemoEvent.OnClick("url")) }
)
}
@Composable
private fun Content(
data: List<DemoBean>,
onClick: (String) -> Unit
) {
//将数据设置给子组件
}
四、封装 BaseViewModel
解决 ViewModel 中模板代码过多问题。
abstract class BaseVM<UiState, UiEffect, UiEvent> : ViewModel() {
var uiState by mutableStateOf(initUiState())
protected set
protected val uiEffect = Channel<UiEffect>()
protected val _uiEvent = MutableSharedFlow<UiEvent>()
val uiEvent = _uiEvent.asSharedFlow()
init {
handleUiEffect()
}
private fun handleUiEffect() {
viewModelScope.launch {
uiEffect.consumeAsFlow().collect { effect ->
onUiEffect(effect)
}
}
}
fun dispatchUiEffect(effect: UiEffect) {
viewModelScope.launch {
uiEffect.send(effect)
}
}
protected abstract fun initUiState(): UiState
protected abstract suspend fun onUiEffect(effect: UiEffect)
override fun onCleared() {
super.onCleared()
uiEffect.close()
}
}
调用起来简洁很多:
class DemoVM : BaseVM<DemoUiState, DemoUiEffect, DemoUiEvent>() {
override fun initUiState() = DemoUiState()
override suspend fun onUiEffect(effect: DemoUiEffect) = when (effect) {
DemoUiEffect.InitData -> initData()
}
suspend fun initData() {
uiState = uiState.copy(name = "")
_uiEvent.emit(DemoUiEvent.ShowToast(""))
}
}
data class DemoUiState(
val name: String = ""
)
sealed interface DemoUiEffect {
object InitData: DemoUiEffect
}
sealed interface DemoUiEvent {
data class ShowToast(val str: String) : DemoUiEvent
}
文章介绍了MVVM架构中的MVI模式,强调了数据的单向流动和唯一可信数据源的概念,以及在Compose环境下如何处理ViewModel和Activity的生命周期。此外,还讨论了事件驱动的Intent机制和状态管理,提供了搭建项目的基本步骤,包括定义State、Event、处理事件和状态更新的ViewModel方法,以及UI层如何响应和发送事件。
1万+

被折叠的 条评论
为什么被折叠?



