一同养成写作习气!这是我参加「日新计划 4 月更文应战」的第4天,点击检查活动详情。
前言
最近看到不少介绍MVI
架构,即Model-View-Intent
的文章,有人留言说Google炒冷饭或许为了凑KPI“发明”了MVI
这么一个词。和后端的朋友描述了一下,他们听了第一印象也是和MVVM
如同区别不大。可是凭印象Google应该还没有到需求这样来凑数。
去看了一下官网,发现彻底没有说到MVI
这个词。。可是引荐的架构图确实是更新了,用来演示MVI
也确实很搭。
(官网图)
想了想,决议总结一下自己的发现,和掘友们一同讨论学习。
事例共享
看过一些剖析MVI
的文章,里面完成的办法各式各样,细节也不尽相同。乃至关于Model
边界的划分也会不一样。
下面先共享一下在特定场景下我的MVVM
和MVI
完成(不重要的细节会省略)。
场景
先预设一个场景,咱们的界面(View/Fragment
)里有一个锅。首要任务便是完成一道菜的烹饪:
flowchart LR
开战 --> 热油 --> 加菜 --> 加调料 --> 出锅
几个需求注意的点:
- 初始状况:开战
- 加入资料时:都是异步获取资料,再加入锅中
- 结束状况:出锅
本文首要是比较MVVM
和MVI
,这儿只共享这两种完成。
经典MVVM
为了加强比照,这儿的完成比较挨近Android Architecture Components
刚发布时官网的的代码架构和片段:
(其时的官网图)
// PotFragment.kt
class PotFragment {
...
// 调查是否点火
viewModel.fireStatus.observe(
viewLifecycleOwner,
Observer {
updateUi()
if (fireOn) addOil()
}
)
// 调查油温
viewModel.oilTemp.observe(
viewLifecycleOwner,
Observer {
updateUi()
if (oilHot) addIngredients()
}
)
// 调查菜熟没熟
viewModel.ingredientsStatus.observe(
viewLifecycleOwner,
Observer {
updateUi()
if (ingredientsCooked) {
// 加调料
addPowder(SALT)
addPowder(SOY_SAUCE)
}
}
)
// 调查油盐是否加完
viewModel.allPowderAdded.observe(
viewLifecycleOwner,
Observer {
// 出锅!
}
)
viewModel.loading.observe(
viewLifecycleOwner,
Observer {
if (loading) {
// 颠勺
} else {
// 放下锅
}
}
)
// 全部准备就绪,点火
turnOnFire()
...
}
// PotViewModel.kt
class PotViewModel(val repo: CookingRepository) {
private val _fireStatus = MutableLiveData<FireStatus>()
val fireStatus: LiveData<FireStatus> = _fireStatus
private val _oilTemp = MutableLiveData<OilTemp>()
val oilTemp: LiveData<OilTemp> = _oilTemp
private val _ingredientsStatus = MutableLiveData<IngredientsStatus>()
val ingredientsStatus: LiveData<IngredientsStatus> = _ingredientsStatus
// 一切调料加好了才更新。这儿Event内部会有flag提示这个LiveData的更新是否被运用过
//(当年咱们还真用这种方法完成过单次消费的LiveData)。
private val _allPowderAdded = MutableLiveData<Event<Boolean>>()
val allPowderAdded: LiveData<Event<Boolean>> = _allPowderAdded
// 假定现已完成逻辑从repo获取是否有还在进行的数据获取
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
fun turnOfFire() {}
// 假定下面都是异步获取资料,这儿简化一下代码
fun addOil() {
repo.fetchOil()
}
fun addIngredients() {
repo.fetchIngredients()
}
fun addPowder(val powderType: PowderType) {
repo.fetchPowder(powderType)
// 更新_allPowderAdded的逻辑会在这儿
}
...
}
特色:
- 运用多个
LiveData
调查不同的数据,并以此来更新UI
。每个LiveData
都是一个State
,每个View
有自己的State
。 -
UI
是否显示loading
由Repository
决议(是否有正在进行的数据读取)。 - 关于调查的
LiveData
要做出何种操作,UI
层的逻辑代码往往无法避免。
很久以前也听说过用状况机(state machine)办理UI
界面,可是思路仍是约束在运用多个LiveData
,运用时进行兼并。尽管状况更清晰了,可是关于代码的可维护性并没有明显的协助,乃至ViewModel
里还多了些兼并LiveData
以及状况办理的代码。代码形似还更杂乱了。后来发现了Redux
式的思路,才有了下面这个版本的MVI
完成。
MVI
下图是我对这个思路的了解:
- 单一信息源
- 单向/环形数据流
定义几个下面代码会用到的称号(不必细究命名,只需自己和团队觉得有意义叫什么都行):
- State:不管数据从哪里来,经过什么处理,都会归于现在的状况。
- Event:上图中的目的发生或代表的事情,也能够了解为
Intent
或许Action
,最终发生Event
让咱们更新State
。 - Reducer:驱动状况改动的中心。这个比如里能够幻想成厨师的手,用来改动锅的状况。
- Side effects:用户无感知,就当它是“额定效果”(或许“副作用”)。关于数据的请求或许记录上传用户操作的代码都归于此类。
下面开端展示伪代码:
// PotState.kt
sealed class PotState {
object Initial: CookingStatus()
object FireOn: CookingStatus()
class Cooking(val data: List<EdibleStuff>): CookingStatus()
object Finished: CookingStatus()
}
// CookEvent.kt
sealed class CookEvent {
object TurnOnFire(): CookEvent()
object RequestOil(): CookEvent()
object AddOil(): CookEvent()
class RequestIngredient(val ingredientType: IngredientType): CookEvent()
class AddIngredient(val ingredient: Ingredient): CookEvent()
class RequestPowder(val powderType: PowderType): CookEvent()
class AddPowder(val powder: Powder): CookEvent()
object ServeFood()
}
// models.kt
interface EdibleStuff
data class Oil(...) implements EdibleStuff
data class Ingredient(...) implements EdibleStuff
data class Powder(...) implements EdibleStuff
// PotReducer.kt
class PotReducer {
fun reduce(state: PotState, event: CookEvent) =
when (state) {
Initial -> reduceInitial(event)
FireOn -> reduceFireOn(event)
is Cooking -> reduceCooking(event)
Finished -> reduceFinished(state, event)
}
// 每个状况只承受某些特定的Event,其它的会疏忽(无法影响其时状况)
private fun reduceInitial(state: PotState, event: CookEvent) =
when (event) {
TurnOnFire -> flowOf(FireOn) // 生成一个Cooking状况并加好油
else -> // handle exception
}
private fun reduceFireOn(state: PotState, event: CookEvent) =
when (event) {
AddOil -> flowOf(Cooking(mutableListOf<Cooking>(Oil)) // 生成一个Cooking状况并加好油
else -> // handle exception
}
private fun reduceCooking(state: PotState, event: CookEvent) =
when (event) {
AddIngredient -> flowOf(state.apply { data.add(event.ingredient) }) // 加菜
AddPowder -> flowOf(state.apply { data.add(event.powder) }) // 加调料
else -> // handle exception
}
private fun reduceFinished(state: PotState, event: CookEvent) =
when (event) {
ServeFood -> flowOf(Finished) // 出锅
else -> // handle exception
}
}
// PotViewModel.kt
class PotViewModel(val potReducer: PotReducer, val repo: CookingRepository) {
...
var potState: PotState = Initial
// 生成下一状况,更新Flow
fun processEvent(event: CookEvent) =
potReducer.reduce(potState, event)
.updateState()
.handleSideEffects(event)
.launchIn(viewModelScope)
// 关于不直接影响UI的事情,作为side effects处理
private fun handleSideEffects(event: CookEvent) =
onEach { event ->
when (event) {
is RequestOil -> fetchOil()
is RequestIngredient -> fetchIngredient(...)
is RequestPowder -> fetchPowder(...)
}
}
// 收到Repository传来的食料,发动新Event:增加入锅
private fun fetchOil() = repo.fetchOil().onEach { processEvent(AddOil) }.collect()
// fetchIngredient(...) 与 fetchPowder(...) 也相似
...
}
// PotFragment.kt
class PotFragment {
...
@Composable
fun Pot(viewModel: PotViewModel) {
val state by viewModel.potState.collectAsState()
Column {
//Render toolbar
Toolbar(...)
//Render screen content
when (state) {
FireOn -> // render UI
is Cooking -> // render UI
Finished -> // render UI:出锅!
}
}
}
// 准备就绪,挑个合适的时机开战
viewModel.processEvent(TurnOnFire)
...
}
特色:
- Fragment/Activity只担任渲染
- 用户目的会发生Event,并被ViewModel中的Reducer处理
- 特定的状况下,只会接纳能被处理的Event
剖析
经典MVVM
长处:
- 比较
MVC
或许MVP
,信任我们都了解。
缺陷:
- 每个
View
有自己的State
。许多View
混合在一同时,代码和咱们的思路都容易变混乱。审核代码也需求对大局有很好的了解。 - 需求调查的数据多了之后,
LiveData
办理能够变得很杂乱。 - 能够看到,
Fragment
中不管何时都在调查并接纳一切LiveData
的更新。细心想想,其实这当中是包含了一些逻辑的。比如说,开战之后咱们不希望接纳加调料的操作。这些逻辑不容易单独拿出来写测验,一般要被包含在Fragment的测验离。
MVI
长处:
-
State
是single source of truth
,单一信息源,不必忧虑各个View
的状况处处都是,乃至相互冲突。 - 伴跟着预设的状况值,能够承受的目的
Intent
或许操作Action
也能够预设。不在计划里的目的/操作不会对UI界面发生影响,也不会有额定效果。审核代码只需求了解新增的目的对某一两个受影响的状况就足够,不必把整个界面的内容都复盘一遍。单元测验也是相似。也算是符合关注点分离(Separation of Concerns)。
缺陷:
- 跟着View变得杂乱,能够有的状况以及能承受的目的也会敏捷胀大。
- 文件数量变多(这个和从MVC到MVP的感觉有点像)。
- 新手学习、了解起来不容易。
比较
两种架构都有优缺陷。
因为我们都了解MVVM
,新团队的承受度必定会好。
有些缺陷也能够想办法改进。例如MVI
的状况胀大能够经过划分为几个小的分状况来缓解。
关于杂乱的场景,我个人更倾向于选用MVI
的大局状况办理的思路。首要仍是觉得传统MVVM
每次增加新的LiveData
时(当然现在常常用Flow
),需求细心检查其它一切的View
或许LiveData
,生怕漏掉什么改动,不利于高效开发和维护。
总结
我以为传统的MVVM
和MVI
首要的区别仍是在于大局状况办理。并且这个大局状况办理的思路用传统MVVM
架构也能完成,许多人觉得MVI
和MVVM
差不多的原因或许正是如此。 其实也家常便饭,不少设计模式两两之间也很相似,但并不妨碍我们给他们安上不同的姓名。只需咱们把握住中心概念,合理运用,叫什么姓名也不重要。正如官方的建议:
就算叫MVI
只是为了唬人,让人一听到就知道你运用了Redux/State machine
的思路,而不是“经典”的安卓版MVVM
,如同也是个不错的理由。
题外话
从官网架构图的改动发生的联想:
ViewModel 化身 LifecycleObserver
最近看到不少文章共享他们关于让ViewModel
也lifecycle-aware
的实验。从官方文档看,UI elements
和State holders
(在我看来便是Fragment/Activity
和ViewModel
)也在被视作一个全体的UI Layer
。不知道以后是不是会有这么一个趋势。
有时分,不经意间就会错过一些有趣有用的想法。回想2017年的时分,听到WeWork
的员工共享他们克己的Declarative UI
库。其时觉得都不能预览,应该不会好用到哪去吧。没想到后来官方发布了Compose
,预览功用都加入了Android Studio
。
选择性运用的 Domain Layer
也许是跟着这几年Clean Architecture
的热度上升,看到不少团队开端加入范畴层。官方引荐的架构图(开头说到)中也加入了Domain Layer (optional)
。增加这么一层确实能够协助咱们解耦部分逻辑。