一、概念

| 概念 | 基于单向数据流,数据永远在一个环形结构中单向流动,便于追踪测试。用户事件→业务逻辑→更新状态→界面重组。 |
| 通信 | View→ViewModel:将用户操作以Intent形式通知给ViewModel,监听ViewModel中State的变化会自动更新到UI。 ViewModel→Model:对Intent类型分类判断做对应的逻辑处理,调用Model获取数据。 ViewModel→View:会获取的数据更新到State,State的变化会自动更新View。 |
- 模型 Model:业务逻辑(处理数据的逻辑和存取)。
- 视图 View:渲染UI(展示界面和数据的状态)。
- 意图 Intent:承载操作(点击、输入、获取列表数据等)。
- 状态 State:反应数据当前值。
1.1 唯一可信数据源
为了解决 MVVM 中 UI 订阅多个分散的状态(ViewModel中的LiveData/Flow)导致各种数据并行更新或数据相互依赖时,无法清晰掌握整个页面的状态。MVI使用 UiState 将所有状态聚合在一处(通过 data class 实现),UI刷新只依赖这一个数据源。
1.2 状态不可变
状态不应该被直接修改,而是重新赋值为一个包含新数据的副本。对象变更才能触发观察,属性变更才会引发重组。
1.3 数据单向流动
DataBinding 数据模型和视图一方发生变化就会同步到另一方,数据的流动是双向的,这样不便于追踪测试。MVI强调数据的源头只有一个,目的地也只有一个,永远在一个环形结构中单向流动,便于追踪测试。
1.4 事件驱动
MVVM 没有约束 UI 和 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 确保不可变,提供默认值方便初始化和减少 null 判断。根据多个状态派生出来的状态,定义在 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 定义用户事件 UiAction
- 命名采用:页面名称 + UiAction。
- 一般同 ViewModel 写在同一个 .kt 文件中,也可以单独写在一个 .kt 文件中。
- 采用 sealed interface 除了保证类型受控优化 when 判断,子类不带参数就定义成 data object 类型方便复用(用object是因为创建的实例无状态区别,data object 是它的优化版,不会打印必要信息)。将可能的用户行为全部定义成子类。
sealed interface DemoUiAction {
//按钮点击
data class OnClick(val url: String) : DemoUiAction
}
3.3 定义界面事件 UiEvent
如果只有 Toast 和 Dialog 这种简单的需求可省去,在 ViewModel 中直接用 ApplicationContext 弹 Toast,把 isShowDialog 整合进 UiState 中,ViewModel 直接改状态弹窗并通过 UiAction 响应 UI 中的操作。
- 命名采用:页面名称 + UiEvent。
- 一般同 ViewModel 写在同一个 .kt 文件中,也可以单独写在一个 .kt 文件中。
sealed interface DemoUiEvent {
data class ShowDialog(val str: String) : DemoUiEvent
}
3.4 ViewModel(处理用户事件+更新状态+发送界面事件)
- Compose 向外部(ViewModel)发送用户事件属于副作用,涉及并发安全问题(多核心优化重组,可能执行在非UI线程),因此考虑协程间通信使用 Channel 来实现。
- 事件只能被消费一次,不能回放造成粘性事件。(排除 StateFlow)
- 事件必须执行(消费代码若是执行在了生产后,也不能丢弃值)。(排除SharedFlow)
- ViewModel 向 UI 发送界面事件,像弹 Toast/Dialog 没有订阅者就应该丢弃事件,因此使用事件流 SharedFlow 来实现。
- 页面数据的初始化在 ViewModel 的 init{} 代码块中调用,因为在 Compose 中用 LaunchedEffects() 会受 Activity 重建影响而再次触发。(视情况而定,如果界面是 Pager 中的子页,Pager会缓存页面所以 LaunchedEffects() 不会跟随Activity重建而再次触发。这样反而不能通过判断 index 在子页显示时才初始化数据)
- 状态可用 MutableState 或 StateFlow 实现。MutableState 在普通使用下更简洁但仅限Compose,StateFlow 在并发修改时 update() 方法原子更新更简洁。
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 uiAction = Channel<DemoUiAction>()
//ViewModel初始化时:初始化数据,启动用户事件处理
init {
viewModelScope.launch {
initData()
handleAction()
}
}
//处理用户事件
private suspend fun handleAction() {
uiAction.consumeAsFlow().collect { action ->
when(action) {
is DemoUiAction.OnClick -> _uiEvent.emit(DemoUiEvent.ShowToast("点击了按钮"))
}
}
}
//暴露给UI发送用户事件(比直接在UI中获取Channel发送方便,包装成普通函数更方便外部调用)
fun dispatchEvent(action: DemoUiAction) {
viewModelScope.launch {
uiAction.send(action)
}
}
//(在业务代码里)更新状态和发送界面事件
private suspend fun initData() {
//状态设为加载中
uiState = uiState.copy(isLoading = true)
runCatching {
repository.getData()
}.onSuccess { data ->
//赋值成功结果,取消加载中
uiState = uiState.copy(success = it, isLoading = false)
}.onFailure {
//取消加载中,并弹Dialog
uiState = uiState.copy(isLoading = false)
_uiEvent.emit(DemoUiEvent.ShowDialog(it.message.toString()))
}
}
override fun onCleared() {
super.onCleared()
uiAction.close() //释放资源
}
}
3.5 UI(响应状态+处理界面事件+发送用户事件)
- 在屏幕级组合项获取ViewModel。
- 当 Activity 处于可交互期间(resume)才需要处理一次性事件。
@Composable
fun MainScreen(
viewModel: DemoViewModel = viewModel() //普通获取VM
// viewModel: DemoViewModel = viewModel(factory = DemoViewModelFactory(DemoRepository(DemoDataSource()))) //带参获取VM
) {
//只在Activity处于可交互时处理事件
LifecycleResumeEffect(Unit) {
lifecycleScope.launch {
viewModel.uiEvent.collect { event ->
when (event) {
is DemoUiEvent.ShowDialog -> {}
}
}
}
//必须调用,可清理资源
onPauseOrDispose {}
}
//读取状态并处理
ScreenContent(
data = viewModel.uiState.success,
onClick = { viewModel.dispatchAction(DemoAction.OnClick("url")) }
)
}
@Composable
private fun ScreenContent(
data: List<DemoBean>,
onClick: (String) -> Unit
) {
//将数据设置给子组件
}
四、封装 BaseViewModel
解决 ViewModel 中模板代码过多问题。
- 不要定义抽象方法 initData() 让子类实现图省事,如果子类实现在其中调用了 Repository 的方法,由于会先调用父类中的 init{} 代码块,此时子类的 Repository 还未初始化完成,initData() 会空指针异常。
abstract class BaseVM<State: UiState, Action: UiAction, Event: UiEvent> : ViewModel() {
var state by mutableStateOf(initUiState())
protected set
private val actionChannel = Channel<Action>()
protected val _eventSharedFlow = MutableSharedFlow<Event>() //protected暴露给子VM发射数据
val eventSharedFlow = _eventSharedFlow.asSharedFlow() //暴露给UI只能收集
//属性和init代码块按顺序执行,这里放在状态之后
init {
handleAction()
}
private suspend fun handleAction() {
viewModelScope.launch {
actionChannel.consumeAsFlow().collect {
onAction(it)
}
}
}
fun dispatchAction(action: Action) {
viewModelScope.launch {
actionChannel.send(action)
}
}
protected abstract fun initUiState(): State
protected abstract suspend fun onAction(action: Action)
override fun onCleared() {
super.onCleared()
actionChannel.close()
}
}
interface UiState
interface UiAction
interface UiEvent
子ViewModel中使用:
class DemoVM : BaseVM<DemoState, DemoAction, DemoEvent>() {
override fun initUiState() = DemoState()
override suspend fun onAction(action: DemoAction) = when (action) {
DemoAction.InitData -> initData()
}
override suspend fun initData() {
state = state.copy(name = "")
_eventSharedFlow.emit(DemoEvent.ShowToast(""))
}
}
data class DemoState(
val name: String = ""
) : UiState
sealed interface DemoAction : UiAction {
object InitData: DemoAction
}
sealed interface DemoEvent : UiEvent {
data class ShowToast(val str: String) : DemoEvent
}
UI中使用:
@Composable
fun DemoScreen( vm: DemoVM = viewModel()) {
LaunchedEffect(Unit) {
vm.eventSharedFlow.collect { event ->
when (event) {
is DemoEvent.ShowToast -> TODO()
}
}
}
}
文章介绍了MVVM架构中的MVI模式,强调了数据的单向流动和唯一可信数据源的概念,以及在Compose环境下如何处理ViewModel和Activity的生命周期。此外,还讨论了事件驱动的Intent机制和状态管理,提供了搭建项目的基本步骤,包括定义State、Event、处理事件和状态更新的ViewModel方法,以及UI层如何响应和发送事件。

1万+

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



