android MVI到底是什么
前言
本篇文章的阅读对象是为了感觉好像了解MVI但是又不知道这玩意到底是个啥的读者
想理解MVI 需要提前理解几个东西
1.为什么推荐使用MVI,android 的MVI是基于什么提出的
2.android 的MVI是基于什么实现的,为什么要用这些
以上三点我先用最简短的语言以自己的理解先做一个解答
1,为什么推荐使用MVI,MVI是基于什么提出的
答:主要为了ViewModel层和View层的交互由双向转化为单向,并且规范交互数据传输
android端由mvc到mvp再到mvvm最后到mvi,每一次的变化都让代码分层更加清晰,目前MVVM的缺点是ViewModel和view的交互还是属于双向交互,viewModel和Model的处理界限也比较模糊,所以提出MVI,MVI其实是基于MVVM, 在View和ViewModel中增加了Intent来作为中间传输,通过响应编程更新UI实现的。这样不仅规范View与ViewModel交互,且将交互顺序由View—>ViewModel->View 的双向交互变为View->Intent->ViewModel->State->View的环形交互,通过Intent和State来解决ViewModel与Model的界限模糊问题。
也就是说ViewModel现在可以不关心如何被view触发,如何刷新UI,也不关心当前有多少数据模型,只用来维护Intent和state管理(再直白些就是intent就是view调用viewModel的中间层,state就是viewModel回调view的中间层,model通过intent和state去管理,看起来会更加简洁)
2,android 的MVI是基于什么实现的
目前android主流的MVI是基于协程+flow+viewModel去实现的
kotlin协程就不说了,省去接口回调,控制代码执行顺序,线程切换kotlin的协程功不可没
flow:中文翻译成流和Stream容易混淆,flow是响应式流,会有配备一个生产者和一个消费者(android可以理解成类似handler里的message,处理方式相似但是原理不同)
viewModel:jetpack家族,本来也可以自己写,但是jetpack提供了可以管理生命周期的viewModel不比自己写香么?
下面两个文章看看更加有助理解mvi
kotlin 响应式编程flow
https://juejin.cn/post/7034379406730592269
这篇文字几乎和官方文档写的详细程度差不多,但是解释会更加友好
MVVM使用
https://www.jianshu.com/p/f9d0688b241e
不喜欢看思路的可以通过这篇文章感受mvvm代码的层次结构
正片
这篇文章看完了能学会啥?
1.flow在UI中简单用法
2.Intent是个啥
3.state是个啥
4.原来MVI这么简单
1:flow在UI中简单用法
为啥我看MVI要先看flow?
因为没有flow就没有MVI的I的灵魂(如果你用rxjava或者自己创建监听者当我没说)
首先如果不知道flow怎么用的同学,我得说说你了,kotlin好好学学,mvvm都用kotlin写了,mvi还想着java是不是太过分了!(只针对android)
首先掏出官方例子
//所有的collect方法都是suspend修饰的,所以扔了协程里
runBlocking {
//创建一个流
flow {
//用循环定义一个生产者
for (i in 1..10) {
//生产者发10个数
emit(i)
}
}.collect {//注册这个流消费者
//消费者打印
println(it)
}
}
这个流很简单就是创建一个流,然后消费打印,用这段代码中两个方法比较重要,emit和collect,源码就不分析了就是emit是生产者发送数据,collect是消费者接受数据
然后我们把这个例子稍微复杂化一点放到例子里
ViewModel代码
class EnglishVM : ViewModel() {
var flow=flow<Int> {
for (i in 1..10) {
emit(i)
}
}
}
这是activity代码
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI学习")
runBlocking {
viewMode.flow.collect {
//将数字打印到textview上
tvClass addText "$it"
}
}
}
//做了个直接打印到textview的快捷方法,可以忽略
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
来看执行结果
现在通过flow将文字展示到了UI上,但是有个问题,我们的业务场景一般是触发某个事件以后才会刷新UI,而且刷新UI我们只有一个或几个结果,不是一连串的数字,所以我们在这个基础上再次升级
首先flow这个方法已经不是那么好用了,我们引入一个新的概念StateFlow(我可以点)
StateFlow由两个API构成MutableStateFlow和StateFlow,主要用来通过状态类的变化来发送状态变化流。原理大体就是通过get,set去监听状态state变化,然后发送流,这里就不展开了,可以看各个不同版本的源码
然后将viewModel中的flow改为StateFlow并加入两个刷新UI的方法
class EnglishVM : BaseViewModel() {
//MutableStateFlow需要默认传入一个状态,我们随便传个1代表默认状态
val state = MutableStateFlow<Int>(1)
//将状态改为2代表正在加载
fun doLoading(){
state.value = 2
}
//将状态改为3代表加载完毕
fun finishLoading(){
state.value = 3
}
}
然后给activity增加两个按钮,添加点击事件,分别调用doLoading和finishLoading
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI学习")
btnFinishLoading.setOnClickListener {
tvClass addText "btnFinishLoading 被点击"
viewMode.finishLoading()
}
btnLoading.setOnClickListener {
tvClass addText "btnLoading 被点击"
viewMode.doLoading()
}
GlobalScope.launch {
viewMode.state.collect {
tvClass addText "$it"
}
}
}
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
运行并分别点击LOADING和FINISH
运行结果
好的一个简单的通过flow更新UI的效果已经完毕了,下面开始实现MVI
2:Intent是个啥
我可以很负责的告诉你,Intent就是个枚举,而且是个特殊的枚举,在kotlin中可以通过sealed关键字来生成封闭类,这个关键字生成的封闭类在when语句中可以不用谢else,而且由于是封闭类,所以可以通过数据对象来实现各种骚操作
比如下面的代码
//写个英语的意图
sealed class EngLishIntent {
//用数据类表示加载英语方法
data class doLoadingEnglish(val num:Int):EngLishIntent()
//用匿名对象表示完成加载方法
object finishLoading:EngLishIntent()
}
但是怎么用这个Intent呢?又涉及到一个kotlin的概念Channel(我可以点)
channel本来是用来做协程之间通讯的,而我们的view层的触发操作和viewModel层获取数据这个流程恰巧应该是需要完全分离的,并且channel具备flow的特性,所以用channel来做view和viewModel的通讯非常适合
我们通过再把上面的例子,通过Intent来处理下
意图代码如下
sealed class EngLishIntent {
//用数据类表示加载英语方法
data class DoLoadingEnglish(val num:Int):EngLishIntent()
//用匿名对象表示完成加载方法
object FinishLoading:EngLishIntent()
}
viewModel将Intent引入
class EnglishVM : BaseViewModel() {
val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
val state = MutableStateFlow<Int>(1)
//初始化的时候将channel的消费者绑定
init {
handleIntent();
}
//注册消费者
private fun handleIntent() {
viewModelScope.launch {
//将Channel转化为flow,并且注册消费者
englishIntent.consumeAsFlow().collect {
//这里的it和Channel<EngLishIntent>泛型保持一致,所以it是封闭类(特殊枚举类)
when(it){
//判断是FinishLoading 将state.value=3
is EngLishIntent.FinishLoading->{state.value=3}
//判断是DoLoadingEnglish 将state.value=1
is EngLishIntent.DoLoadingEnglish->{
//此处可以通过 it. 拿到DoLoadingEnglish的入参 后面会演示
state.value=2}
}
}
}
}
然后再把Activity改改
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI学习")
btnFinishLoading.setOnClickListener {
tvClass addText "btnFinishLoading 被点击"
//协程方法统一提取,方便日后修改
doLaunch{
tvClass addText "send(EngLishIntent.FinishLoading)"
//拿到viewMode的englishIntent去传递意图
viewMode.englishIntent.send(EngLishIntent.FinishLoading)
}
}
btnLoading.setOnClickListener {
tvClass addText "btnLoading 被点击"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingEnglish)"
viewMode.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
}
}
GlobalScope.launch {
viewMode.state.collect {
tvClass addText "$it"
}
}
}
fun doLaunch(block: suspend CoroutineScope.() -> Unit){
GlobalScope.launch {
block.invoke(this)
}
}
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
然后看下点击两个按钮后的运行结果
运行结果
结果和上次的结果没什么太大的区别,而且感觉代码还变复杂了,为什么要这么做?
注意看下面两个图
原始方法
Intent
之前是直接使用viewModel提供的方法的,现在变成了传输intent里的枚举,彻底将View和ViewModel解耦了,现在唯一耦合的就是viewModel持有的Intent了,实现了业务解耦,很棒棒
既然知道了通过intent能实现view发起事件对viewModel的解耦,那能不能实现ViewModel刷新view的解耦呢?
其实上面的代码我们已经通过flow实现了一大半了,现在把int类型转换成一个枚举让代码更加严谨就能完全解耦了,此时就能引入MVI的最后一个概念state了
3:state是个啥
state是个和Intent一样的枚举,但是不同的是intent是个事件流,state是个状态流
首先我们先定义一个和Intent差不多的封装类state
sealed class EnglishState {
object BeforeLoading:EnglishState()
object Loading:EnglishState()
object FinishLoading:EnglishState()
}
然后我们把之前的MutableStateFlow封装起来,不给view层修改权限,已保证我们业务逻辑不会写在UI层,并且把1、2、3等状态改为刚刚创建的EnglishState
class EnglishVM : BaseViewModel() {
val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<EnglishState>(EnglishState.BeforeLoading)
val state: StateFlow<EnglishState>
get() = _state
init {
handleIntent();
}
private fun handleIntent() {
viewModelScope.launch {
englishIntent.consumeAsFlow().collect {
when(it){
is EngLishIntent.FinishLoading->{
_state.value=EnglishState.FinishLoading
}
is EngLishIntent.DoLoadingEnglish->{
//此处可以通过 it. 拿到DoLoadingEnglish的入参 后面会演示
_state.value=EnglishState.Loading
}
}
}
}
}
}
然后把Activity的打印UI更新部分通过state做不同的逻辑处理
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI学习")
btnFinishLoading.setOnClickListener {
tvClass addText "btnFinishLoading 被点击"
doLaunch{
tvClass addText "send(EngLishIntent.FinishLoading)"
viewModel.englishIntent.send(EngLishIntent.FinishLoading)
}
}
btnLoading.setOnClickListener {
tvClass addText "btnLoading 被点击"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingEnglish)"
viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
}
}
lifecycleScope.launch {
viewModel.state.collect {
when(it){
is EnglishState.BeforeLoading->{
tvClass addText "初始化页面"
}
is EnglishState.Loading ->{
tvClass addText "加载中..."
}
is EnglishState.FinishLoading ->{
tvClass addText "加载完毕..."
}
}
}
}
}
fun doLaunch(block: suspend CoroutineScope.() -> Unit){
GlobalScope.launch {
block.invoke(this)
}
}
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
分别点击按钮结果如下
image.png
到这里,一个基本的MVI就已经成型了,我们结合实际请求,稍稍做些许改动
4.原来MVI这么简单
我们先将ViewModel赋予真正的请求能力,提供一个基类(可以通过各种方法来)
open class BaseViewModel : ViewModel() {
var getClient: () -> Urls = {
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) //设置超时时间
.retryOnConnectionFailure(true)
val logInterceptor = HttpLoggingInterceptor()
// if (BuildConfig.DEBUG) {
// //显示日志
// logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
// } else {
// logInterceptor.setLevel(HttpLoggingInterceptor.Level.NONE)
// }
client.addInterceptor(GsonInterceptor())
Retrofit.Builder()
.client(client.build())
.baseUrl("https://route.showapi.com/")
.addConverterFactory(ViewModelGsonConverterFactory())
.build().create(Urls::class.java)
}
//向协程提供一个全局异常,用来处理异常UI
fun <T> errorContext(err: (errorMessage:Throwable) -> Unit):CoroutineExceptionHandler {
return CoroutineExceptionHandler { _, e ->
err.invoke(e)
}
}
}
intent 修改修改,加一个请求类型
sealed class EngLishIntent {
//获取英语句子数据
data class DoLoadingEnglish(val num:Int):EngLishIntent()
//获取新闻数据
object DoLoadingNews:EngLishIntent()
}
State也改改,新增几个数据状态
sealed class EnglishState {
object BeforeLoading:EnglishState()
object Loading:EnglishState()
object FinishLoading:EnglishState()
data class EnglishData(val list:List<EnglishKey>):EnglishState()
data class NewsData(val list:List<NewsListKey>):EnglishState()
data class ErrorData(val error:String):EnglishState();
}
viewmodel改改,带有真正的网络请求
class EnglishVM : BaseViewModel() {
val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<EnglishState>(EnglishState.BeforeLoading)
val state: StateFlow<EnglishState>
get() = _state
init {
handleIntent();
}
private fun handleIntent() {
viewModelScope.launch {
englishIntent.consumeAsFlow().collect {
//这两种写法太冗余了
// is EngLishIntent.DoLoadingEnglish -> loadingEnglish()
// is EngLishIntent.DoLoadingNews -> loadingEnglish()
commentLoading(it)
}
}
}
suspend fun intentToState(intent:EngLishIntent):EnglishState{
when (intent) {
//加载英语句子
is EngLishIntent.DoLoadingEnglish ->
return EnglishState.EnglishData(getClient.invoke().getEnglishWordsByLaunch(5))
//加载新闻句子
is EngLishIntent.DoLoadingNews ->
return EnglishState.NewsData(getClient.invoke().getNewsListKeyByLaunch())
}
}
////加载英语句子
// private fun loadingEnglish() {
// viewModelScope.launch(context = (errorContext {
// _state.value = EnglishState.FinishLoading
// _state.value = EnglishState.ErrorData(it.message?:"请求异常")
// } + Dispatchers.Main)) {
// _state.value = EnglishState.Loading
// _state.value = EnglishState.EnglishData(getClient.invoke().getEnglishWordsByLaunch(5))
// _state.value = EnglishState.FinishLoading
// }
// }
//加载新闻
// private fun loadingNews() {
// viewModelScope.launch(context = (errorContext {
// _state.value = EnglishState.FinishLoading
// _state.value = EnglishState.ErrorData(it.message?:"请求异常")
// } + Dispatchers.Main)) {
// _state.value = EnglishState.Loading
// _state.value = EnglishState.NewsData(getClient.invoke().getNewsListKeyByLaunch())
// _state.value = EnglishState.FinishLoading
// }
// }
private fun commentLoading(intent:EngLishIntent) {
viewModelScope.launch(context = (errorContext {
_state.value = EnglishState.FinishLoading
_state.value = EnglishState.ErrorData(it.message?:"请求异常")
} + Dispatchers.Main)) {
_state.value = EnglishState.Loading
_state.value = intentToState(intent)
_state.value = EnglishState.FinishLoading
}
}
}
最后把activity的按钮改改,UI刷新逻辑改改变成这样
class MVIEnglishActivity :BaseActivity() {
val viewModel :EnglishVM by viewModels<EnglishVM>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent(R.layout.act_mvi_english_class)
setTitle("MVI学习")
btnLoadingNews.setOnClickListener {
tvClass addText "btnLoadingNews 被点击"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingNews)"
viewModel.englishIntent.send(EngLishIntent.DoLoadingNews)
}
}
btnLoadingEnglish.setOnClickListener {
tvClass addText "btnLoadingEnglish 被点击"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingEnglish)"
viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
}
}
//这里注意改成有生命周期的lifecycleScope 否则网络请求回来这里管道就销毁了
lifecycleScope.launch {
viewModel.state.collect {
when(it){
is EnglishState.BeforeLoading->{
tvClass addText "初始化页面"
}
is EnglishState.Loading ->{
tvClass addText "加载中..."
}
is EnglishState.FinishLoading ->{
tvClass addText "加载完毕..."
}
is EnglishState.EnglishData->{
for (key in it.list){
tvClass addText key.english addText key.chinese
}
}
is EnglishState.NewsData->{
for (key in it.list){
tvClass addText "标题:${key.title}" addText "摘要:${key.summary}" addText "省份:${key.provinceName} 时间:${key.updateTime}"
}
}
}
}
}
}
fun doLaunch(block: suspend CoroutineScope.() -> Unit){
GlobalScope.launch {
block.invoke(this)
}
}
infix fun TextView.addText(text: String) :TextView{
this.text = "${this.text?.toString()}$text\n";
return this
}
}
最后附上接口
interface Urls {
@GET("/1211-1")
suspend fun getEnglishWordsByLaunch(
@Query("count") count: Int?,
@Query("showapi_appid") id: String = "测试id",
@Query("showapi_sign") showapi_sign: String = "showapi_sign",
): ArrayList<EnglishKey>
@GET("/2217-4")
suspend fun getNewsListKeyByLaunch(
@Query("showapi_appid") id: String = "测试id",
@Query("showapi_sign") showapi_sign: String = "showapi_sign",
): ArrayList<NewsListKey>
点击两次按钮后结果入下
image.png
一个简单的MVI网络请求架构到此结束
结尾
MVI其实主要思想是通过Intent将view和业务实现层分离,达到通过意图传递逻辑方法。所以不一定非要基于MVVM,也适用于MVP,这次分享就到此结束了
最后感谢
https://blog.csdn.net/vitaviva/article/details/109406873
这篇文章提供的清晰简单的思路,代码思路均由这篇文章获取