安卓MVI架构真的来了?动手试着封装吧(一)上

简介: 安卓MVI架构真的来了?动手试着封装吧(一)

前言:由于框架本身也在不断地迭代,因此文章中的部分代码可能存在更新或者过时,如果你想阅读源码或者查看代码的在项目中的实际使用方法,可以查看笔者目前在维护的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之类的。

image.png

  从这里开始一切都变得怪异起来了,你往一个表示状态的容器里面填充了一些事件,而且使用了列表,则说明事件需要被消费掉,否则越填充越多,更严重的是会产生数据倒灌的问题,当你切换到手机主页再切换回APP的时候,UI会尝试从ViewModel的状态流中取数据,然后将本应该消费掉的Toast事件又取出来消费一遍,于是出现了下面的场景:

当一个用户输错了密码之后,APP提示“密码错误,请重试”,他切换到其他APP又切回来的时候,发现APP又继续提示“密码错误,请重试”,即使他没有做任何操作  、

image.png

  一切的问题根源都是来源于,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去删除列表中被消费的事件对象,这就问题解决了,谷歌爸爸赛高!对此,笔者再次重申:

image.png

  如果你是一个对代码坏味道敏感的人,可能已经隐隐约约闻到了一股屎味,没错请相信你的直觉。说好的响应式呢,结果还是要手动去维护事件的消费,万一我忘了呢,完蛋又出现bug了。


相关文章
|
7月前
|
Android开发 Python
Python封装ADB获取Android设备wifi地址的方法
Python封装ADB获取Android设备wifi地址的方法
156 0
|
Android开发
Android Http 请求封装及使用
Android Http 请求封装及使用
236 0
|
7月前
|
Android开发
Android 分享机顶盒项目的封装类《GridView》(二)(转)
Android 分享机顶盒项目的封装类《GridView》(二)(转)
48 2
|
4月前
|
Android开发 iOS开发
Android项目架构设计问题之将隐式跳转的逻辑进行抽象和封装如何解决
Android项目架构设计问题之将隐式跳转的逻辑进行抽象和封装如何解决
46 0
|
4月前
|
编解码 API 开发工具
Android平台轻量级RTSP服务模块二次封装版调用说明
本文介绍了Android平台上轻量级RTSP服务模块的二次封装实践,旨在简化开发流程,让开发者能更专注于业务逻辑。通过`LibPublisherWrapper`类提供的API,可在应用中轻松初始化RTSP服务、配置视频参数(如分辨率、编码类型)、启动与停止RTSP服务及流发布,并获取RTSP会话数量。此外,还展示了如何处理音频和视频数据的采集与推送。最后,文章提供了从启动服务到销毁资源的完整示例,帮助开发者快速集成实时流媒体功能。
|
6月前
|
移动开发 小程序 安全
基础入门-APP架构&小程序&H5+Vue语言&Web封装&原生开发&Flutter
基础入门-APP架构&小程序&H5+Vue语言&Web封装&原生开发&Flutter
|
6月前
|
Android开发 Kotlin
kotlin安卓开发【Jetpack Compose】:封装SnackBarUtil工具类方便使用
GPT-4o 是一个非常智能的模型,比当前的通义千问最新版本在能力上有显著提升。作者让GPT开发一段代码,功能为在 Kotlin 中使用 Jetpack Compose 框架封装一个 Snackbar 工具类,方便调用
|
7月前
|
XML Java Android开发
Android 分享机顶盒项目的封装类《GridView》(三)(转)
Android 分享机顶盒项目的封装类《GridView》(三)(转)
40 2
|
7月前
|
达摩院 安全 Java
80 PM撸代码之Android【武侠讲封装、继承、多态】
80 PM撸代码之Android【武侠讲封装、继承、多态】
48 0
|
API Android开发 Kotlin
安卓MVI架构真的来了?动手试着封装吧(三)下
安卓MVI架构真的来了?动手试着封装吧(三)
121 0