這是 MVI 架構(gòu)的第三篇从橘,系列文章目錄如下:
Android 架構(gòu)之 MVI 雛形 | 響應(yīng)式編程 + 單向數(shù)據(jù)流 + 唯一可信數(shù)據(jù)源
Android 架構(gòu)之 MVI 初級(jí)體 | Flow 替換 LiveData 重構(gòu)數(shù)據(jù)鏈路
Android 架構(gòu)之 MVI 完全體 | 重新審視 MVVM 之殤撒妈,PartialChange & Reducer 來(lái)拯救
Android 架構(gòu)之 MVI 究極體 | 狀態(tài)和事件分道揚(yáng)鑣疚察,粘性不再是問題其中第一篇剖析了 MVI 的概念,第二篇是 MVI 在項(xiàng)目實(shí)戰(zhàn)中的初級(jí)應(yīng)用宾袜,而這一篇將重構(gòu)上篇的代碼捻艳,以展示 MVI 的完全體。
MVI 架構(gòu)有三大關(guān)鍵詞:“唯一可信數(shù)據(jù)源”+“單向數(shù)據(jù)流”+“響應(yīng)式編程”庆猫,以及一些關(guān)鍵概念认轨,比如Intent,State。理解這些概念之后月培,能更輕松地閱讀本文嘁字。(強(qiáng)烈建議從第一篇開始閱讀)
引子
在上一篇中,用 MVI 重構(gòu)了“新聞流”這個(gè)業(yè)務(wù)場(chǎng)景杉畜。本篇在此基礎(chǔ)上進(jìn)一步拓展纪蜒,引入 MVI 中兩個(gè)重要的概念PartialChange和Reducer。
假設(shè)“新聞流”這個(gè)業(yè)務(wù)場(chǎng)景寻行,用戶可以觸發(fā)如下行為:
初始化新聞流
上拉加載更多新聞
舉報(bào)某條新聞
在 MVVM 中霍掺,這些行為被表達(dá)為 ViewModel 的一個(gè)方法調(diào)用。在 MVI 中被稱為意圖Intent拌蜘,它們不再是一個(gè)方法調(diào)用杆烁,而是一個(gè)數(shù)據(jù)。通臣蛭裕可被這樣定義:
sealedclassFeedsIntent{dataclassInit(valtype:Int,valcount:Int):FeedsIntent()dataclassMore(valtimestamp:Long,valcount:Int):FeedsIntent()dataclassReport(valid:Long):FeedsIntent()}
這樣做使得界面意圖都以數(shù)據(jù)的形式流入到一個(gè)流中兔魂,好處是,可以用流的方式統(tǒng)一管理所有意圖举娩。更詳細(xì)的講解可以點(diǎn)擊Android 架構(gòu)之 MVI | 響應(yīng)式編程 + 單向數(shù)據(jù)流 + 唯一可信數(shù)據(jù)源析校。
產(chǎn)品文檔定義了所有的用戶意圖Intent,而設(shè)計(jì)稿定義了所有的界面狀態(tài)State:
dataclassNewsState(valdata:List<News>,// 新聞列表valisLoading:Boolean,// 是否正在首次加載valisLoadingMore:Boolean,// 是否正在上拉加載更多valerrorMessage:String,// 加載錯(cuò)誤信息 toastvalreportToast:String,// 舉報(bào)結(jié)果 toast){companionobject{// 新聞流的初始狀態(tài)valinitial=NewsState(data=emptyList(),isLoading=true,isLoadingMore=false,errorMessage="",reportToast="")}}
在 MVI 中铜涉,把界面的一次展示理解為單個(gè) State 的一次渲染智玻。相較于 MVVM 中一個(gè)界面可能被分拆為多個(gè) LiveData,State 這種唯一數(shù)據(jù)源降低了復(fù)雜度芙代,使得代碼容易維護(hù)吊奢。
有了 Intent 和 State,整個(gè)界面刷新的過(guò)程就形成了一條單向數(shù)據(jù)流纹烹,如下圖所示:
MVI 就是用“響應(yīng)式編程”的方式將這條數(shù)據(jù)流中的若干 Intent 轉(zhuǎn)換成唯一 State页滚。初級(jí)的轉(zhuǎn)換方式是直接將 Intent 映射成 State,詳細(xì)分析可以點(diǎn)擊如何把業(yè)務(wù)代碼越寫越復(fù)雜铺呵?(二)| Flow 替換 LiveData 重構(gòu)數(shù)據(jù)鏈路裹驰,更加 MVI。
PartialChange
理論上 Intent 是無(wú)法直接轉(zhuǎn)換為 State 的片挂。因?yàn)?Intent 只表達(dá)了用戶觸發(fā)的行為幻林,而行為產(chǎn)生的結(jié)果才對(duì)應(yīng)一個(gè) State贞盯。更具體的說(shuō),“上拉加載更多新聞”可能產(chǎn)生三個(gè)結(jié)果:
正在加載更多新聞沪饺。
加載更多新聞成功邻悬。
加載更多新聞失敗。
其中每一個(gè)結(jié)果都對(duì)應(yīng)一個(gè) State随闽。“單向數(shù)據(jù)流”內(nèi)部的數(shù)據(jù)變換詳情如下:
每一個(gè)意圖會(huì)產(chǎn)生若干個(gè)結(jié)果肝谭,每個(gè)結(jié)果對(duì)應(yīng)一個(gè)界面狀態(tài)掘宪。
上圖看著有“很多條”數(shù)據(jù)流,但同一時(shí)間只可能有一條起作用攘烛。上圖看著會(huì)在 ViewModel 內(nèi)部形成各種 State魏滚,但暴露給界面的還是唯一 State。
因?yàn)樗幸鈭D產(chǎn)生的所有可能的結(jié)果都對(duì)應(yīng)于一個(gè)唯一 State 實(shí)例坟漱,所以每個(gè)意圖產(chǎn)生的結(jié)果只引起 State 部分字段的變化鼠次。比如 Init.Success 只會(huì)影響 NewsState.data 和 NewsState.isLoading。
在 MVI 框架中芋齿,意圖 Intent 產(chǎn)生的結(jié)果稱為部分變化PartialChange腥寇。
總結(jié)一下:
MVI 框架中用數(shù)據(jù)流來(lái)理解界面刷新。
數(shù)據(jù)流的起點(diǎn)是界面發(fā)出的意圖(Intent)觅捆,一個(gè)意圖會(huì)產(chǎn)生若干結(jié)果赦役,它們稱為 PartialChange,一個(gè) PartialChange 對(duì)應(yīng)一個(gè) State 實(shí)例栅炒。
數(shù)據(jù)流的終點(diǎn)是界面對(duì) State 的觀察而進(jìn)行的一次渲染掂摔。
連續(xù)的狀態(tài)
界面展示的變化是“連續(xù)的”,即界面新狀態(tài)總是由上一次狀態(tài)變化而來(lái)赢赊。就像連環(huán)畫一樣乙漓,下一幀是基于上一幀的偏移量。
這種基于老狀態(tài)產(chǎn)生新狀態(tài)的行為稱為Reduce释移,用一個(gè) lambda 表達(dá)即是(oldState: State) -> State叭披。
界面發(fā)出的不同意圖會(huì)生成不同的結(jié)果,每種結(jié)果都有各自的方法進(jìn)行新老狀態(tài)的變換秀鞭。比如“上拉加載更多新聞”和“舉報(bào)新聞”趋观,前者在老狀態(tài)的尾部追加數(shù)據(jù),而后者是在老狀態(tài)中刪除數(shù)據(jù)锋边。
基于此皱坛,Reduce 的 lambda 可作如下表達(dá):(oldState: State, change: PartialChange) -> State,即新狀態(tài)由老狀態(tài)和 PartialChange 共同決定豆巨。
通常 PartialChange 被定義成密封接口剩辟,而 Reduce 定義為內(nèi)部方法:
// 新聞流的部分變化sealedinterfaceFeedsPartialChange{// 描述如何從老狀態(tài)變化為新狀態(tài)funreduce(oldState:NewsState):NewsState}
這是 PartialChange 的抽象定義,新聞流場(chǎng)景中,它應(yīng)該有三個(gè)實(shí)現(xiàn)類贩猎,分別是 Init熊户,More,Report吭服。其中 Init 的實(shí)現(xiàn)如下:
sealedclassInit:FeedsPartialChange{// 在初始化新聞流流場(chǎng)景下嚷堡,老狀態(tài)如何變化成新狀態(tài)overridefunreduce(oldState:NewsState):NewsState=// 對(duì)初始化新聞流能產(chǎn)生的所有結(jié)果分類討論,并基于老狀態(tài)拷貝構(gòu)建新狀態(tài)when(this){Loading->oldState.copy(isLoading=true)isSuccess->oldState.copy(data=news,//方便地訪問Success攜帶的數(shù)據(jù)isLoading=false,isLoadingMore=false,errorMessage="")isFail->oldState.copy(data=emptyList(),isLoading=false,isLoadingMore=false,errorMessage=error)}// 加載中objectLoading:Init()// 加載成功dataclassSuccess(valnews:List<News>):Init()// 加載失敗dataclassFail(valerror:String):Init()}
初始化新聞流的 PartialChange 也被實(shí)現(xiàn)為密封的艇棕,密封產(chǎn)生的效果是蝌戒,在編譯時(shí),其子類的全集就已經(jīng)全部確定沼琉,不允許在運(yùn)行時(shí)動(dòng)態(tài)新增子類北苟,且所有子類必須內(nèi)聚在一個(gè)包名下。
這樣做的好處是降低界面刷新的復(fù)雜度打瘪,即有限個(gè) Intent 會(huì)產(chǎn)生有限個(gè) PartialChange友鼻,且它們唯一對(duì)應(yīng)一個(gè) State。出 bug 的時(shí)候只需從三處找問題:1. Intent 是否發(fā)射闺骚? 2. 是否生成了既定的 PartialChange彩扔? 3. reduce 算法是否有問題?
將 reduce 算法定義在 PartialChange 內(nèi)部葛碧,就能很方便地獲取 PartialChange 攜帶的數(shù)據(jù)借杰,并基于它構(gòu)建新狀態(tài)。
用同樣的思路进泼,More 和 Report 的定義如下:
sealedclassMore:FeedsPartialChange{overridefunreduce(oldState:NewsState):NewsState=when(this){Loading->oldState.copy(isLoading=false,isLoadingMore=true,errorMessage="")isSuccess->oldState.copy(data=oldState.data+news,// 新數(shù)據(jù)追加在老數(shù)據(jù)后isLoading=false,isLoadingMore=false,errorMessage="")isFail->oldState.copy(isLoadingMore=false,isLoading=false,errorMessage=error)}objectLoading:More()dataclassSuccess(valnews:List<News>):More()dataclassFail(valerror:String):More()}sealedclassReport:FeedsPartialChange{overridefunreduce(oldState:NewsState):NewsState=when(this){isSuccess->oldState.copy(// 在老數(shù)據(jù)中刪除舉報(bào)新聞data=oldState.data.filterNot{it.id==id},reportToast="舉報(bào)成功")Fail->oldState.copy(reportToast="舉報(bào)失敗")}classSuccess(valid:Long):Report()objectFail:Report()}
狀態(tài)的變換
Intent蔗衡,PartialChange,Reduce乳绕,State 定義好了绞惦,是時(shí)候看看如何用流的方式把它們串聯(lián)起來(lái)!
總體來(lái)說(shuō)洋措,狀態(tài)是這樣變換的:Intent -> PartialChange -(Reduce)-> State
1. Intent 流入济蝉,State 流出
classStateFlowActivity:AppCompatActivity(){privatevalnewsViewModelbylazy{ViewModelProvider(this,NewsViewModelFactory(NewsRepo(this)))[NewsViewModel::class.java]}// 將所有意圖通過(guò) merge 進(jìn)行合流privatevalintentsbylazy{merge(flowOf(FeedsIntent.Init(1,5)),// 初始化新聞loadMoreFlow(),// 加載更多新聞reportFlow()// 舉報(bào)新聞)}// 將上拉加載更多轉(zhuǎn)換成數(shù)據(jù)流privatefunloadMoreFlow()=callbackFlow{recyclerView.setOnLoadMoreListener{trySend(FeedsIntent.More(111L,2))}awaitClose{recyclerView.removeOnLoadMoreListener(null)}}// 將舉報(bào)新聞轉(zhuǎn)換成數(shù)據(jù)流privatefunreportFlow()=callbackFlow{reportView.setOnClickListener{valnews=newsAdapter.dataList[i]as?News? ? ? ? ? ? news?.id?.let{trySend(FeedsIntent.Report(it))}}awaitClose{reportView.setOnClickListener(null)}}overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(contentView)// 訂閱意圖流intents// Intent 流入 ViewModel.onEach(newsViewModel::send).launchIn(lifecycleScope)// 訂閱狀態(tài)流newsViewModel.newState// State 流出 ViewModel,并繪制界面.collectIn(this){showNews(it)}}}classNewsViewModel(privatevalnewsRepo:NewsRepo):ViewModel(){// 用于接收意圖的 SharedFlowprivateval_feedsIntent=MutableSharedFlow<FeedsIntent>()// 意圖被變換為狀態(tài)valnewState=_feedsIntent.map{}// 偽代碼菠发,省略了 將 Intent 變換為 State 的細(xì)節(jié)// 將意圖發(fā)送到流funsend(intent:FeedsIntent){viewModelScope.launch{_feedsIntent.emit(intent)}}}
界面可以發(fā)出的所有意圖都被組織到一個(gè)流中王滤,并且羅列在一起。intents流可以作為理解業(yè)務(wù)邏輯的入口滓鸠。同時(shí) ViewModel 提供了一個(gè) State 流雁乡,供界面訂閱。
2. Intent -> PartialChange
classNewsViewModel(privatevalnewsRepo:NewsRepo):ViewModel(){privateval_feedsIntent=MutableSharedFlow<FeedsIntent>()// 供界面觀察的唯一狀態(tài)valnewState=_feedsIntent.toPartialChangeFlow().flowOn(Dispatchers.IO).stateIn(viewModelScope,SharingStarted.Eagerly,NewsState.initial))}
各種 Intent 轉(zhuǎn)換為 PartialChange 的邏輯被封裝在toPartialChangeFlow()中:
// NewsViewModel.kt// 將 Intent 流變換為 PartialChange 流privatefunFlow<FeedsIntent>.toPartialChangeFlow():Flow<FeedsPartialChange>=merge(// 過(guò)濾出初始化新聞意圖并將其變換為對(duì)應(yīng)的 PartialChangefilterIsInstance<FeedsIntent.Init>().flatMapConcat{it.toPartialChangeFlow()},// 過(guò)濾出上拉加載更多意圖并將其變換為對(duì)應(yīng)的 PartialChangefilterIsInstance<FeedsIntent.More>().flatMapConcat{it.toPartialChangeFlow()},// 過(guò)濾出舉報(bào)新聞意圖并將其變換為對(duì)應(yīng)的 PartialChangefilterIsInstance<FeedsIntent.Report>().flatMapConcat{it.toPartialChangeFlow()},)
toPartialChangeFlow() 被定義為擴(kuò)展方法糜俗。
filterIsInstance() 用于過(guò)濾出Flow<FeedsIntent>中的子類型并分類討論踱稍,因?yàn)槊糠N Intent 變換為 PartialChange 的方式有所不同曲饱。
最后用 merge 進(jìn)行合流,它會(huì)將每個(gè) Flow 中的數(shù)據(jù)合起來(lái)并發(fā)地轉(zhuǎn)發(fā)到一個(gè)新的流上珠月。merge + filterIsInstance的組合相當(dāng)于流中的 if-else扩淀。
其中的 toPartialChangeFlow() 是各種意圖的擴(kuò)展方法:
// NewsViewModel.ktprivatefunFeedsIntent.Init.toPartialChangeFlow()=flowOf(// 本地?cái)?shù)據(jù)庫(kù)新聞newsRepo.localNewsOneShotFlow,// 網(wǎng)絡(luò)新聞newsRepo.remoteNewsFlow(this.type.toString(),this.count.toString()))// 并發(fā)合流.flattenMerge().transformWhile{emit(it.news)!it.abort}// 將新聞數(shù)據(jù)變換為成功或失敗的 PartialChange.map{news->if(news.isEmpty())Init.Fail("no news")elseInit.Success(news)}// 發(fā)射展示 Loading 的 PartialChange.onStart{emit(Init.Loading)}
該擴(kuò)展方法描述了如何將 FeedsIntent.Init 變換為對(duì)應(yīng)的 PartialChange。同樣地啤挎,F(xiàn)eedsIntent.More 和 FeedsIntent.Report 的變換邏輯如下:
// NewsViewModel.ktprivatefunFeedsIntent.More.toPartialChangeFlow()=newsRepo.remoteNewsFlow("news","10").map{news->if(it.news.isEmpty())More.Fail("no more news")elseMore.Success(it.news)}.onStart{emit(More.Loading)}.catch{emit(More.Fail("load more failed by xxx"))}privatefunFeedsIntent.Report.toPartialChangeFlow()=newsRepo.reportNews(id).map{if(it>=0L)Report.Success(it)elseReport.Fail}.catch{emit((Report.Fail))}
3. PartialChange -(Reduce)-> State
經(jīng)過(guò) toPartialChangeFlow() 的變換驻谆,現(xiàn)在流中流動(dòng)的數(shù)據(jù)是各種類型的 PartialChange。接下來(lái)就要將其變換為 State:
// NewsViewModel.ktvalnewState=_feedsIntent.toPartialChangeFlow()// 將 PartialChange 變換為 State.scan(NewsState.initial){oldState,partialChange->partialChange.reduce(oldState)}.flowOn(Dispatchers.IO).stateIn(viewModelScope,SharingStarted.Eagerly,NewsState.initial))
使用scan()進(jìn)行變換:
// 從 Flow<T> 變換為 Flow<R>publicfun<T,R>Flow<T>.scan(initial:R,// 初始值operation:suspend(accumulator:R,value:T)->R// 累加算法):Flow<R>=runningFold(initial,operation)publicfun<T,R>Flow<T>.runningFold(initial:R,operation:suspend(accumulator:R,value:T)->R):Flow<R>=flow{// 累加器varaccumulator:R=initialemit(accumulator)collect{value->// 進(jìn)行累加accumulator=operation(accumulator,value)// 向下游發(fā)射累加值emit(accumulator)}}
從 scan() 的簽名看庆聘,是將一個(gè)流變換為另一個(gè)流旺韭,看似和 map() 相似。但它的變換算法是帶累加的掏觉。用 lambda 表達(dá)為(accumulator: R, value: T) -> R。
這不正好就是上面提到的 Reduce 嗎值漫!即基于老狀態(tài)和新 PartialChange 生成新狀態(tài)澳腹。
MVVM 和 MVI 復(fù)雜度比拼
就新聞流這個(gè)場(chǎng)景,用圖來(lái)對(duì)比下 MVVM 和 MVI 復(fù)雜度的區(qū)別杨何。
這張圖表達(dá)了三種復(fù)雜度:
View 發(fā)起請(qǐng)求的復(fù)雜度:ViewModel 的各種方法調(diào)用會(huì)散落在界面不同地方酱塔。即界面向 ViewModel 發(fā)起請(qǐng)求沒有統(tǒng)一入口。
View 觀察數(shù)據(jù)的復(fù)雜度:界面需要觀察多個(gè) ViewModel 提供的數(shù)據(jù)危虱,這導(dǎo)致界面狀態(tài)的一致性難以維護(hù)羊娃。
ViewModel 內(nèi)部請(qǐng)求和數(shù)據(jù)關(guān)系的復(fù)雜度:數(shù)據(jù)被定義為 ViewModel 的成員變量。成員變量是增加復(fù)雜度的利器埃跷,因?yàn)樗梢员蝗魏纬蓡T方法訪問蕊玷。也就是說(shuō),新增業(yè)務(wù)對(duì)成員變量的修改可能影響老業(yè)務(wù)的界面展示弥雹。同理垃帅,當(dāng)界面展示出錯(cuò)時(shí),也很難一下子定位到是哪個(gè)請(qǐng)求造成的剪勿。
再來(lái)看一下讓人耳目一新的 MVI 吧:
完美化解上述三個(gè)沒有必要的復(fù)雜度贸诚。
總之,用上 MVI 后厕吉,新需求不再破壞老邏輯酱固,出 bug 了能更快速定位到問題。
敬請(qǐng)期待
還有一個(gè)問題有待解決头朱,那就是 MVI 框架下运悲,刷新界面時(shí)持久性狀態(tài) State 和 一次性事件 Event 的區(qū)別對(duì)待。
在 MVVM 中髓窜,因?yàn)?LiveData 的粘性扇苞,導(dǎo)致一次性事件被界面多次消費(fèi)欺殿。對(duì)此有多種解決方案。詳情可點(diǎn)擊LiveData 面試題庫(kù)鳖敷、解答脖苏、源碼分析
但 MVI 的解題思路略有不同,限于篇幅原因定踱,只能下回分析棍潘,歡迎持續(xù)關(guān)注~
總結(jié)
MVI 框架中用單向數(shù)據(jù)流來(lái)理解界面刷新。整個(gè)數(shù)據(jù)流中包含的數(shù)據(jù)依次如下:Intent崖媚,PartialChange亦歉,State
數(shù)據(jù)流的起點(diǎn)是界面發(fā)出的意圖(Intent),一個(gè)意圖會(huì)產(chǎn)生若干結(jié)果畅哑,它們稱為 PartialChange肴楷,一個(gè) PartialChange 對(duì)應(yīng)一個(gè) State 實(shí)例。
數(shù)據(jù)流的終點(diǎn)是界面對(duì) State 的觀察而進(jìn)行的一次渲染荠呐。
MVI 就是用“響應(yīng)式編程”的方式將單向數(shù)據(jù)流中的若干 Intent 轉(zhuǎn)換成唯一 State赛蔫。
MVI 強(qiáng)調(diào)的單向數(shù)據(jù)流表現(xiàn)在兩個(gè)層面:
View 和 ViewModel 交互過(guò)程中的單向數(shù)據(jù)流:?jiǎn)蝹€(gè)Intent流流入 ViewModel,單個(gè)State流流出 ViewModel泥张。
ViewModel 內(nèi)部數(shù)據(jù)變換的單向數(shù)據(jù)流:Intent 變換為多個(gè) PartialChange呵恢,一個(gè) PartialChange 對(duì)應(yīng)一個(gè) State。
Talk is cheap, show me the code
完整代碼可以從這個(gè)地址克隆媚创。