前言:由于框架本身也在不断地迭代,因此文章中的部分代码可能存在更新或者过时,如果你想阅读源码或者查看代码的在项目中的实际使用方法,可以查看笔者目前在维护的compose项目:Spacecraft: 《Spacecraft - 我的安卓技术实践平台》-查看代码请进入develop分支 (gitee.com)
关于安卓的MVI架构
近些年安卓的架构发展的真是非常迅速,笔者入行不久,就已经从MVC,MVP一路干到MVVM,自以为对MVVM非常熟悉,略有心得的时候,谷歌稍稍的更新了开发者文档(应用架构指南 | Android 开发者 | Android Developers (google.cn))
虽然通篇没有涉及到MVI,但是许多业内的小伙伴,特别是前端开发表示,开发文档中提到的单向数据流,唯一数据源,不正是MVI区别于MVVM的最显著的特征吗?
作为一个常年上班摸鱼钻研的新油条,果断研究起来,于是在翻阅了谷歌的开发文档、掘金上大佬写的文章以及阅读了几个开源MVI架构项目之后,自己也动手折腾了一个小DEMO,表示真香,但是也发现了一些问题。
注意:如果你对MVI架构没有任何认识,请在掘金阅读相关MVI架构文章或者阅读谷歌开发者文档之后,再继续阅读下文
遇到的小问题
1. 状态?事件!
MVI架构中,特别是谷歌推崇的开发模式,是将整个页面的状态存放于单一的类中,而且这个类必须是Kotlin的data class,因为kotlin的这个特殊的类自带了copy功能,非常方便去更新部分的属性,于是我们就有了下面的一个类:
data class NewsUiState( val isSignedIn: Boolean = false, val isPremium: Boolean = false, val newsItems: List<NewsItemUiState> = listOf(), val userMessages: List<Message> = listOf() ) data class NewsItemUiState( val title: String, val body: String, val bookmarked: Boolean = false, ... )
Ok,我们有了一个Ui的状态,其实如果你懂电影或游戏中的帧的概念的话,这个UiState实际上就是页面的一帧或很多帧,这样解释或许不恰当,但是足够你理解这个概念,也就是说,ViewModel只需要向Ui提供当前的状态就好了,至于UI拿到这个数据之后如何去展示显示UI,就和ViewModel没关系了。
目前为止,一切都很美好,数据流是单向流向viewModel,响应式...
但是,如果你注意到NewsUiState里面有个属性userMessages,在文档中,这个属性被用来充当ViewModel需要向Ui发送的通知,例如Toast之类的。
从这里开始一切都变得怪异起来了,你往一个表示状态的容器里面填充了一些事件,而且使用了列表,则说明事件需要被消费掉,否则越填充越多,更严重的是会产生数据倒灌的问题,当你切换到手机主页再切换回APP的时候,UI会尝试从ViewModel的状态流中取数据,然后将本应该消费掉的Toast事件又取出来消费一遍,于是出现了下面的场景:
当一个用户输错了密码之后,APP提示“密码错误,请重试”,他切换到其他APP又切回来的时候,发现APP又继续提示“密码错误,请重试”,即使他没有做任何操作 、
一切的问题根源都是来源于,UiState表示的是一种状态而非一种事件容器,因此如果你把事件填充进去,Ui就会尝试反复取出他,执行特定的逻辑,于是Toast被反复调用了。
此刻大多数人的第一反应是:Ui去更新viewModel中UiState的值。但是别忘了,MVI可是单项数据流动的呀,UI可不能去直接修改viewModel中的值!
正当笔者大呼谷歌RNM退钱的时候,发现谷歌在文档中写了解决方案,如下:
lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { uiState -> uiState.userMessages.firstOrNull()?.let { userMessage -> // TODO: Show Snackbar with userMessage. // Once the message is displayed and // dismissed, notify the ViewModel. viewModel.userMessageShown(userMessage.id) } ... } } }
哇哦,谷歌爸爸真的好聪明呀,既然Ui不能直接修改viewModel的值,那viewModel就提供一个方法给Ui调用不就行了,每次UI消费了这些一次性事件,就去调用一次viewModel提供的方法,然后viewModel去删除列表中被消费的事件对象,这就问题解决了,谷歌爸爸赛高!对此,笔者再次重申:
如果你是一个对代码坏味道敏感的人,可能已经隐隐约约闻到了一股屎味,没错请相信你的直觉。说好的响应式呢,结果还是要手动去维护事件的消费,万一我忘了呢,完蛋又出现bug了。