前言
本篇文章的閱讀對(duì)象是為了感覺好像了解MVI但是又不知道這玩意到底是個(gè)啥的讀者
想理解MVI 需要提前理解幾個(gè)東西
1.為什么推薦使用MVI湘纵,android 的MVI是基于什么提出的
2.android 的MVI是基于什么實(shí)現(xiàn)的,為什么要用這些
以上三點(diǎn)我先用最簡(jiǎn)短的語言以自己的理解先做一個(gè)解答
1,為什么推薦使用MVI甩十,MVI是基于什么提出的
答:主要為了ViewModel層和View層的交互由雙向轉(zhuǎn)化為單向胞四,并且規(guī)范交互數(shù)據(jù)傳輸
android端由mvc到mvp再到mvvm最后到mvi盔腔,每一次的變化都讓代碼分層更加清晰壕曼,目前MVVM的缺點(diǎn)是ViewModel和view的交互還是屬于雙向交互,viewModel和Model的處理界限也比較模糊寸齐,所以提出MVI欲诺,MVI其實(shí)是基于MVVM, 在View和ViewModel中增加了Intent來作為中間傳輸渺鹦,通過響應(yīng)編程更新UI實(shí)現(xiàn)的扰法。這樣不僅規(guī)范View與ViewModel交互,且將交互順序由View—>ViewModel->View 的雙向交互變?yōu)閂iew->Intent->ViewModel->State->View的環(huán)形交互毅厚,通過Intent和State來解決ViewModel與Model的界限模糊問題塞颁。
也就是說ViewModel現(xiàn)在可以不關(guān)心如何被view觸發(fā),如何刷新UI吸耿,也不關(guān)心當(dāng)前有多少數(shù)據(jù)模型祠锣,只用來維護(hù)Intent和state管理(再直白些就是intent就是view調(diào)用viewModel的中間層,state就是viewModel回調(diào)view的中間層咽安,model通過intent和state去管理伴网,看起來會(huì)更加簡(jiǎn)潔)
2,android 的MVI是基于什么實(shí)現(xiàn)的
目前android主流的MVI是基于協(xié)程+flow+viewModel去實(shí)現(xiàn)的
kotlin協(xié)程就不說了妆棒,省去接口回調(diào)澡腾,控制代碼執(zhí)行順序,線程切換kotlin的協(xié)程功不可沒
flow:中文翻譯成流和Stream容易混淆糕珊,flow是響應(yīng)式流蛋铆,會(huì)有配備一個(gè)生產(chǎn)者和一個(gè)消費(fèi)者(android可以理解成類似handler里的message,處理方式相似但是原理不同)
viewModel:jetpack家族放接,本來也可以自己寫,但是jetpack提供了可以管理生命周期的viewModel不比自己寫香么留特?
下面兩個(gè)文章看看更加有助理解mvi
kotlin 響應(yīng)式編程flow
https://juejin.cn/post/7034379406730592269
這篇文字幾乎和官方文檔寫的詳細(xì)程度差不多纠脾,但是解釋會(huì)更加友好
MVVM使用
http://www.reibang.com/p/f9d0688b241e
不喜歡看思路的可以通過這篇文章感受mvvm代碼的層次結(jié)構(gòu)
正片
這篇文章看完了能學(xué)會(huì)啥玛瘸?
1.flow在UI中簡(jiǎn)單用法
2.Intent是個(gè)啥
3.state是個(gè)啥
4.原來MVI這么簡(jiǎn)單
1:flow在UI中簡(jiǎn)單用法
為啥我看MVI要先看flow?
因?yàn)闆]有flow就沒有MVI的I的靈魂(如果你用rxjava或者自己創(chuàng)建監(jiān)聽者當(dāng)我沒說)
首先如果不知道flow怎么用的同學(xué)苟蹈,我得說說你了糊渊,kotlin好好學(xué)學(xué),mvvm都用kotlin寫了慧脱,mvi還想著java是不是太過分了!(只針對(duì)android)
首先掏出官方例子
//所有的collect方法都是suspend修飾的渺绒,所以扔了協(xié)程里
runBlocking {
//創(chuàng)建一個(gè)流
flow {
//用循環(huán)定義一個(gè)生產(chǎn)者
for (i in 1..10) {
//生產(chǎn)者發(fā)10個(gè)數(shù)
emit(i)
}
}.collect {//注冊(cè)這個(gè)流消費(fèi)者
//消費(fèi)者打印
println(it)
}
}
這個(gè)流很簡(jiǎn)單就是創(chuàng)建一個(gè)流,然后消費(fèi)打印菱鸥,用這段代碼中兩個(gè)方法比較重要宗兼,emit和collect,源碼就不分析了就是emit是生產(chǎn)者發(fā)送數(shù)據(jù)氮采,collect是消費(fèi)者接受數(shù)據(jù)
然后我們把這個(gè)例子稍微復(fù)雜化一點(diǎn)放到例子里
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學(xué)習(xí)")
runBlocking {
viewMode.flow.collect {
//將數(shù)字打印到textview上
tvClass addText "$it"
}
}
}
//做了個(gè)直接打印到textview的快捷方法殷绍,可以忽略
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
來看執(zhí)行結(jié)果
現(xiàn)在通過flow將文字展示到了UI上,但是有個(gè)問題鹊漠,我們的業(yè)務(wù)場(chǎng)景一般是觸發(fā)某個(gè)事件以后才會(huì)刷新UI主到,而且刷新UI我們只有一個(gè)或幾個(gè)結(jié)果,不是一連串的數(shù)字躯概,所以我們?cè)谶@個(gè)基礎(chǔ)上再次升級(jí)
首先flow這個(gè)方法已經(jīng)不是那么好用了登钥,我們引入一個(gè)新的概念StateFlow(我可以點(diǎn))
StateFlow由兩個(gè)API構(gòu)成MutableStateFlow和StateFlow,主要用來通過狀態(tài)類的變化來發(fā)送狀態(tài)變化流娶靡。原理大體就是通過get牧牢,set去監(jiān)聽狀態(tài)state變化,然后發(fā)送流固蛾,這里就不展開了结执,可以看各個(gè)不同版本的源碼
然后將viewModel中的flow改為StateFlow并加入兩個(gè)刷新UI的方法
class EnglishVM : BaseViewModel() {
//MutableStateFlow需要默認(rèn)傳入一個(gè)狀態(tài),我們隨便傳個(gè)1代表默認(rèn)狀態(tài)
val state = MutableStateFlow<Int>(1)
//將狀態(tài)改為2代表正在加載
fun doLoading(){
state.value = 2
}
//將狀態(tài)改為3代表加載完畢
fun finishLoading(){
state.value = 3
}
}
然后給activity增加兩個(gè)按鈕,添加點(diǎn)擊事件艾凯,分別調(diào)用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學(xué)習(xí)")
btnFinishLoading.setOnClickListener {
tvClass addText "btnFinishLoading 被點(diǎn)擊"
viewMode.finishLoading()
}
btnLoading.setOnClickListener {
tvClass addText "btnLoading 被點(diǎn)擊"
viewMode.doLoading()
}
GlobalScope.launch {
viewMode.state.collect {
tvClass addText "$it"
}
}
}
infix fun TextView.addText(text: String) {
this.text = "${this.text?.toString()}$text\n";
}
}
運(yùn)行并分別點(diǎn)擊LOADING和FINISH
好的一個(gè)簡(jiǎn)單的通過flow更新UI的效果已經(jīng)完畢了献幔,下面開始實(shí)現(xiàn)MVI
2:Intent是個(gè)啥
我可以很負(fù)責(zé)的告訴你,Intent就是個(gè)枚舉趾诗,而且是個(gè)特殊的枚舉蜡感,在kotlin中可以通過sealed關(guān)鍵字來生成封閉類,這個(gè)關(guān)鍵字生成的封閉類在when語句中可以不用謝else恃泪,而且由于是封閉類郑兴,所以可以通過數(shù)據(jù)對(duì)象來實(shí)現(xiàn)各種騷操作
比如下面的代碼
//寫個(gè)英語的意圖
sealed class EngLishIntent {
//用數(shù)據(jù)類表示加載英語方法
data class doLoadingEnglish(val num:Int):EngLishIntent()
//用匿名對(duì)象表示完成加載方法
object finishLoading:EngLishIntent()
}
但是怎么用這個(gè)Intent呢?又涉及到一個(gè)kotlin的概念Channel(我可以點(diǎn))
channel本來是用來做協(xié)程之間通訊的贝乎,而我們的view層的觸發(fā)操作和viewModel層獲取數(shù)據(jù)這個(gè)流程恰巧應(yīng)該是需要完全分離的情连,并且channel具備flow的特性,所以用channel來做view和viewModel的通訊非常適合
我們通過再把上面的例子览效,通過Intent來處理下
意圖代碼如下
sealed class EngLishIntent {
//用數(shù)據(jù)類表示加載英語方法
data class DoLoadingEnglish(val num:Int):EngLishIntent()
//用匿名對(duì)象表示完成加載方法
object FinishLoading:EngLishIntent()
}
viewModel將Intent引入
class EnglishVM : BaseViewModel() {
val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
val state = MutableStateFlow<Int>(1)
//初始化的時(shí)候?qū)hannel的消費(fèi)者綁定
init {
handleIntent();
}
//注冊(cè)消費(fèi)者
private fun handleIntent() {
viewModelScope.launch {
//將Channel轉(zhuǎn)化為flow却舀,并且注冊(cè)消費(fèi)者
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的入?yún)?后面會(huì)演示
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學(xué)習(xí)")
btnFinishLoading.setOnClickListener {
tvClass addText "btnFinishLoading 被點(diǎn)擊"
//協(xié)程方法統(tǒng)一提取,方便日后修改
doLaunch{
tvClass addText "send(EngLishIntent.FinishLoading)"
//拿到viewMode的englishIntent去傳遞意圖
viewMode.englishIntent.send(EngLishIntent.FinishLoading)
}
}
btnLoading.setOnClickListener {
tvClass addText "btnLoading 被點(diǎn)擊"
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";
}
}
然后看下點(diǎn)擊兩個(gè)按鈕后的運(yùn)行結(jié)果
結(jié)果和上次的結(jié)果沒什么太大的區(qū)別挽拔,而且感覺代碼還變復(fù)雜了辆脸,為什么要這么做?
注意看下面兩個(gè)圖
之前是直接使用viewModel提供的方法的螃诅,現(xiàn)在變成了傳輸intent里的枚舉啡氢,徹底將View和ViewModel解耦了,現(xiàn)在唯一耦合的就是viewModel持有的Intent了术裸,實(shí)現(xiàn)了業(yè)務(wù)解耦倘是,很棒棒
既然知道了通過intent能實(shí)現(xiàn)view發(fā)起事件對(duì)viewModel的解耦,那能不能實(shí)現(xiàn)ViewModel刷新view的解耦呢穗椅?
其實(shí)上面的代碼我們已經(jīng)通過flow實(shí)現(xiàn)了一大半了辨绊,現(xiàn)在把int類型轉(zhuǎn)換成一個(gè)枚舉讓代碼更加嚴(yán)謹(jǐn)就能完全解耦了,此時(shí)就能引入MVI的最后一個(gè)概念state了
3:state是個(gè)啥
state是個(gè)和Intent一樣的枚舉匹表,但是不同的是intent是個(gè)事件流门坷,state是個(gè)狀態(tài)流
首先我們先定義一個(gè)和Intent差不多的封裝類state
sealed class EnglishState {
object BeforeLoading:EnglishState()
object Loading:EnglishState()
object FinishLoading:EnglishState()
}
然后我們把之前的MutableStateFlow封裝起來,不給view層修改權(quán)限袍镀,已保證我們業(yè)務(wù)邏輯不會(huì)寫在UI層默蚌,并且把1、2苇羡、3等狀態(tài)改為剛剛創(chuàng)建的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的入?yún)?后面會(huì)演示
_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學(xué)習(xí)")
btnFinishLoading.setOnClickListener {
tvClass addText "btnFinishLoading 被點(diǎn)擊"
doLaunch{
tvClass addText "send(EngLishIntent.FinishLoading)"
viewModel.englishIntent.send(EngLishIntent.FinishLoading)
}
}
btnLoading.setOnClickListener {
tvClass addText "btnLoading 被點(diǎn)擊"
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";
}
}
分別點(diǎn)擊按鈕結(jié)果如下
到這里绸吸,一個(gè)基本的MVI就已經(jīng)成型了,我們結(jié)合實(shí)際請(qǐng)求设江,稍稍做些許改動(dòng)
4.原來MVI這么簡(jiǎn)單
我們先將ViewModel賦予真正的請(qǐng)求能力锦茁,提供一個(gè)基類(可以通過各種方法來)
open class BaseViewModel : ViewModel() {
var getClient: () -> Urls = {
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) //設(shè)置超時(shí)時(shí)間
.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)
}
//向協(xié)程提供一個(gè)全局異常,用來處理異常UI
fun <T> errorContext(err: (errorMessage:Throwable) -> Unit):CoroutineExceptionHandler {
return CoroutineExceptionHandler { _, e ->
err.invoke(e)
}
}
}
intent 修改修改叉存,加一個(gè)請(qǐng)求類型
sealed class EngLishIntent {
//獲取英語句子數(shù)據(jù)
data class DoLoadingEnglish(val num:Int):EngLishIntent()
//獲取新聞數(shù)據(jù)
object DoLoadingNews:EngLishIntent()
}
State也改改码俩,新增幾個(gè)數(shù)據(jù)狀態(tài)
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改改,帶有真正的網(wǎng)絡(luò)請(qǐng)求
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?:"請(qǐng)求異常")
// } + 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?:"請(qǐng)求異常")
// } + 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?:"請(qǐng)求異常")
} + Dispatchers.Main)) {
_state.value = EnglishState.Loading
_state.value = intentToState(intent)
_state.value = EnglishState.FinishLoading
}
}
}
最后把a(bǔ)ctivity的按鈕改改歼捏,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學(xué)習(xí)")
btnLoadingNews.setOnClickListener {
tvClass addText "btnLoadingNews 被點(diǎn)擊"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingNews)"
viewModel.englishIntent.send(EngLishIntent.DoLoadingNews)
}
}
btnLoadingEnglish.setOnClickListener {
tvClass addText "btnLoadingEnglish 被點(diǎn)擊"
doLaunch{
tvClass addText "send(EngLishIntent.DoLoadingEnglish)"
viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
}
}
//這里注意改成有生命周期的lifecycleScope 否則網(wǎng)絡(luò)請(qǐng)求回來這里管道就銷毀了
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 "標(biāo)題:${key.title}" addText "摘要:${key.summary}" addText "省份:${key.provinceName} 時(shí)間:${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 = "測(cè)試id",
@Query("showapi_sign") showapi_sign: String = "showapi_sign",
): ArrayList<EnglishKey>
@GET("/2217-4")
suspend fun getNewsListKeyByLaunch(
@Query("showapi_appid") id: String = "測(cè)試id",
@Query("showapi_sign") showapi_sign: String = "showapi_sign",
): ArrayList<NewsListKey>
點(diǎn)擊兩次按鈕后結(jié)果入下
一個(gè)簡(jiǎn)單的MVI網(wǎng)絡(luò)請(qǐng)求架構(gòu)到此結(jié)束
結(jié)尾
MVI其實(shí)主要思想是通過Intent將view和業(yè)務(wù)實(shí)現(xiàn)層分離稿存,達(dá)到通過意圖傳遞邏輯方法。所以不一定非要基于MVVM瞳秽,也適用于MVP瓣履,這次分享就到此結(jié)束了
最后感謝
https://blog.csdn.net/vitaviva/article/details/109406873
這篇文章提供的清晰簡(jiǎn)單的思路,代碼思路均由這篇文章獲取