作者:leobertlan
前言
當(dāng)時項(xiàng)目采用MVP分層設(shè)計(jì),組員的代碼風(fēng)格差異也較大,代碼中類職責(zé)賦予與封裝風(fēng)格各成一套,隨著業(yè)務(wù)急速膨脹,代碼越發(fā)混亂。試圖用 MVI架構(gòu)
+ 單向流
形成 掣肘
帶來一致風(fēng)格。 但這種做法不夠以人為本,最終采用 “在MVP的基礎(chǔ)上進(jìn)行了適當(dāng)改造+設(shè)計(jì)約定的方式” 解決了問題,并未將MVI投入到商業(yè)項(xiàng)目中,于是 放棄了紙上談兵。
在半年前終于有機(jī)會在商業(yè)項(xiàng)目中進(jìn)行實(shí)踐,同諸位談一談使用后的 個人感悟 ,并藉此講透MVI等架構(gòu)。
所有內(nèi)容將按照以下要點(diǎn)展開:
- 從架構(gòu)的理念出發(fā) – 簡單列明各種
MVX
的理念 , MVX:指代 MVC、MVP、MVVM、MVI - 擁抱復(fù)雜的同時實(shí)現(xiàn)簡化 – 通過對比理解單向數(shù)據(jù)流動所解決的痛點(diǎn)、設(shè)計(jì)Intent的原因等問題
- 單一可信數(shù)據(jù)源,不可僵化信奉
- 要想優(yōu)雅,需要工具 – 借助聲明式、響應(yīng)式編程工具,構(gòu)建
流
,屏蔽命令式編程中的細(xì)節(jié),同樣是聚焦和簡化 - 狀態(tài)和事件分家,絕不是吃飽了撐的 – 為什么要裂變出狀態(tài)和事件,如何界定
內(nèi)容會很長,我會酌情再寫一些 解
,結(jié)合實(shí)例和代碼演示內(nèi)容。
兩個項(xiàng)目的基本情況
相比于之前的巨型項(xiàng)目,這兩個項(xiàng)目的業(yè)務(wù)量均不大,一個是基于藍(lán)牙和局域網(wǎng)的操控類APP,下午簡稱APP-A,一個是內(nèi)部使用的工具,分析公司各個產(chǎn)品的日志,簡稱APP-B。
雖然他們的業(yè)務(wù)深度要比一般的APP要深,但在 本質(zhì)上一致 ,畢竟同類型業(yè)務(wù)量再多也僅僅是重復(fù)運(yùn)用一套模式 ,并不影響本質(zhì)。
和諸多項(xiàng)目的本質(zhì)一致,均符合如下圖所示的邏輯分層,并在人機(jī)交互過程中執(zhí)行業(yè)務(wù)邏輯:
- APP-A 是Android項(xiàng)目,圖方便純kotlin
- APP-B 是 Compose-Desktop項(xiàng)目,不得不kotlin
過于絮叨了,我們進(jìn)入正文。
從架構(gòu)的理念出發(fā)
謹(jǐn)記,實(shí)際情況中,MVI、MVVM這些架構(gòu)均先由Web應(yīng)用領(lǐng)域提出,用于解決瀏覽器Web應(yīng)用研發(fā)中的問題。
在后續(xù)的應(yīng)用領(lǐng)域發(fā)展過程中,存在共性問題,便引入了這些設(shè)計(jì),并結(jié)合自身特點(diǎn)進(jìn)行了拓展。
接下來我們聊一聊理念,不比武功。
圖片出自電影一代宗師
MVI的理念
MVI
脫胎于 Model View Intent
- Intent:驅(qū)動model發(fā)生改變的意圖,以UI中的事件最為常見;
- Model:業(yè)務(wù)模型,包含數(shù)據(jù)和邏輯,是對應(yīng)
客觀實(shí)體
的程序建模
; - View:表現(xiàn)層的視圖,以UI方式呈現(xiàn)Model的狀態(tài)(以及事件),接受用戶輸入,轉(zhuǎn)換為UI事件
官方的這幅圖很好的呈現(xiàn)了三者之間的驅(qū)動關(guān)系:
這張圖非常簡單,它摒棄了驅(qū)動方式的細(xì)節(jié),只體現(xiàn)了角色與驅(qū)動關(guān)系。
注意,只要設(shè)計(jì)中滿足 角色和驅(qū)動關(guān)系
符合上圖,就是MVI架構(gòu)設(shè)計(jì),并不限制 驅(qū)動方式的實(shí)現(xiàn)細(xì)節(jié)
經(jīng)典的MVI驅(qū)動細(xì)節(jié)要比上圖復(fù)雜很多,下文再聊。
從軟件設(shè)計(jì)的原則出發(fā):職責(zé)分離并封裝
的目的是 解耦
、 可獨(dú)立變化
、復(fù)用
。
顯然,區(qū)別于 MVVM
、 MVP
、 MVC
,角色上的差別在于 ViewModel、Presenter、Controller、Intent四者,而它們又是View和Model之間的紐帶。除此之外,V和M亦稍有不同。
MVC、MVP
MVC、MVP 中,C和P的職責(zé)體現(xiàn)為 控制、調(diào)度
。
MVP中 V
和 M
完全解耦可獨(dú)立變化,MVC中 M
直接操作 V
耦合高,在web應(yīng)用中,C
需要直接操作DOM。
MVVM
MVVM中,提倡 數(shù)據(jù)驅(qū)動
, 數(shù)據(jù)源
被剝離到 VM
中,在 雙向綁定框架
的加持下,View層的輸入反映為數(shù)據(jù)的變化,數(shù)據(jù)的變化驅(qū)動視圖內(nèi)容。
顯然,VM的職責(zé)限于維護(hù)數(shù)據(jù)狀態(tài),如有必要,驅(qū)動View層消費(fèi)數(shù)據(jù)狀態(tài), 不必再關(guān)注如何操作視圖。
一般來說,雙向綁定框架已經(jīng)引入觀察者模式實(shí)現(xiàn),可響應(yīng)式驅(qū)動,VM一般沒有必要關(guān)心 響應(yīng)式驅(qū)動和下游觀察者生命周期問題
簡單思考之后會發(fā)現(xiàn)MVVM的問題,它的側(cè)重點(diǎn)在于 利用雙向綁定讓開發(fā)者專注于數(shù)據(jù)狀態(tài)的維護(hù),從操作視圖更新中得以解放
,它難以解決 無天然狀態(tài)
問題,例如:按鈕點(diǎn)擊這類事件。
MVI
在MVI中,結(jié)合業(yè)務(wù)背景將UI事件等內(nèi)容轉(zhuǎn)換為 Intent
,驅(qū)動Model層業(yè)務(wù),Model層的業(yè)務(wù)結(jié)果反映為 視圖狀態(tài)
+ 事件
。
因此View層和Model層之間已經(jīng)解耦,并可以吸收MVVM中的優(yōu)點(diǎn)采用如下設(shè)計(jì):
- 將雙向綁定退化為單向綁定,View層消費(fèi)UI狀態(tài)流和事件流,這也意味著UI狀態(tài)的職責(zé)精簡,它不再承載View層的用戶輸入等事件
- 將UI狀態(tài)獨(dú)立,Model層僅產(chǎn)生
UI狀態(tài)的局部變化
和事件
下圖為經(jīng)典的MVI原理示意圖:
在上文中,我們已經(jīng)討論了各個角色的職責(zé),下面逐步展開討論角色具備的特性和細(xì)節(jié)知識。
在此之前,還請謹(jǐn)記:合適的才是最好的
沒有絕對的最好的設(shè)計(jì),只有最合適的設(shè)計(jì)。
再好的架構(gòu),都需要遵循其理念并結(jié)合項(xiàng)目因地制宜地進(jìn)行調(diào)整,以獲得最佳使用效果。所以請讀者諸君務(wù)必在閱讀時,結(jié)合自身項(xiàng)目的情況仔細(xì)思考以下問題:
- 引入新框架所解決的痛點(diǎn)、衍生的問題、是否需要進(jìn)行框架調(diào)整?
- 框架中的角色功能,為什么出現(xiàn),又有怎樣的局限?
單向數(shù)據(jù)流動
MVI擁抱了結(jié)構(gòu)復(fù)雜,但能夠靈活應(yīng)對業(yè)務(wù)編碼時的各種情況,按部就班即可。
從MVI原理圖中,可以清晰的看到 “數(shù)據(jù)” 的流動方向。 起始于 Intent
,經(jīng)過分類和選擇性消費(fèi)后產(chǎn)生 Result
,對應(yīng)的reducer函數(shù)計(jì)算后,得到最新的 State
(以及裂變出必要的 Event
,圖中未體現(xiàn)) ,驅(qū)動視圖。
注意:
-
單向
是指 單一方向 - 此處的
數(shù)據(jù)
是廣義的、寬泛的。 - 僅描述數(shù)據(jù)流的 變化方向 ,與數(shù)據(jù)流的數(shù)量無關(guān),但一般 形成有效工作 均需要兩條數(shù)據(jù)流(上行數(shù)據(jù)流和下行數(shù)據(jù)流)
即驅(qū)動數(shù)據(jù)流變化的方向是唯一的,在英文中的術(shù)語為:Unidirectional Data Flow
簡稱 UDF
。
MVC、MVP中的痛點(diǎn)
前文我們提到,在MVC和MVP中,著眼于 控制、調(diào)度 ,并不強(qiáng)調(diào) 數(shù)據(jù)流
的概念。
View和Model間之間的交互,一般有兩種編碼風(fēng)格:雙向的API調(diào)用、單向的API調(diào)用+回調(diào):
注意:以下兩圖并未體現(xiàn)Controller和Presenter細(xì)節(jié),僅表意,從View層出發(fā)的API調(diào)用和回到View層的UI更新
雙向API調(diào)用如上圖。
單向API調(diào)用+回調(diào)更新UI如上圖。
顯而易見,這兩種方式無法繼續(xù)抽象,需根據(jù)實(shí)際業(yè)務(wù)進(jìn)行命令式編碼。當(dāng)UI復(fù)雜時,難以寫出清晰、易讀的代碼,維護(hù)難度激增。
MVVM解決UI更新代碼混亂問題
前文我們已經(jīng)提到:MVVM中通過綁定框架,將UI事件轉(zhuǎn)化為數(shù)據(jù)變化,驅(qū)動業(yè)務(wù);業(yè)務(wù)結(jié)果表現(xiàn)為數(shù)據(jù)變化,驅(qū)動UI更新。
顯而易見,維護(hù)樸素的數(shù)據(jù)要比直接維護(hù)復(fù)雜的UI要簡單。
但問題也同時產(chǎn)生,data1的變化有兩個可能的原因:
- Model層業(yè)務(wù)結(jié)果使其變化,并期望它驅(qū)動UI更新
- View層發(fā)生事件,反饋數(shù)據(jù)變化,并期望它驅(qū)動Model層邏輯
因此,框架需要考慮標(biāo)識數(shù)據(jù)變化來源、或者其他手段消除方向性所帶來的問題。
并且MVVM難以靈活決定的 “何時調(diào)用Model層邏輯”,即大多數(shù)業(yè)務(wù)中,都需要結(jié)合多個屬性的變化形成組合條件來驅(qū)動Model層邏輯。
本篇并不重點(diǎn)討論MVVM,故不再展開MVVM解決循環(huán)更新的方案,以及衍生的問題。
盡管如此,MVVM中的數(shù)據(jù)綁定依舊解決了View層更新繁雜的問題。
用Intent靈活決定何時調(diào)用Model
既然數(shù)據(jù)驅(qū)動UI有極大的益處,且View層事件驅(qū)動ViewModel的數(shù)據(jù)變化有很多弊端 (需要建立很高的復(fù)雜度) ,那自然需要 趨利避害
僅保留數(shù)據(jù)驅(qū)動UI的部分,并增加Intent用以驅(qū)動Model層業(yè)務(wù)
在于 MVC/MVP
以及 MVVM
對比后不難得出結(jié)論:
- MVC/MVP中,View層通過調(diào)用C/P層API的方式最終調(diào)用到Model層業(yè)務(wù),方式質(zhì)樸、無難度。但業(yè)務(wù)量規(guī)模增大后接口方法數(shù)也會增多,導(dǎo)致C/P層尾大不掉,難以重用。
- MVVM中,VM層總是需要利用
技巧
進(jìn)行模型概念轉(zhuǎn)換,以滿足業(yè)務(wù)響應(yīng)滿足實(shí)際需求,需要很深厚的設(shè)計(jì)經(jīng)驗(yàn)才能寫出非常優(yōu)秀的代碼,這并不友好。
作者按:我個人認(rèn)為一個友好的設(shè)計(jì),不應(yīng)當(dāng)劍走偏鋒,而應(yīng)當(dāng)大巧不工,能夠以力破法,達(dá)成 “使用者只需要吃透理論就可以解決各類問題” 的目標(biāo)。
而MVI在架構(gòu)角色中設(shè)計(jì)了Intent的角色:
- 它包含了業(yè)務(wù)調(diào)用的意圖和數(shù)據(jù)
- 從設(shè)計(jì)上可滿足
調(diào)用
與實(shí)現(xiàn)
的分離 - 架構(gòu)模型中以Intent流的形式出現(xiàn),下游對其的
篩選
、轉(zhuǎn)換
、消費(fèi)
等行為可遵循FP范式
(即函數(shù)式編程范式、Functional Programming Patterns) ,邏輯的復(fù)用粒度為方法級,復(fù)用度更高更靈活 - 解決了MVVM中的方向性問題、MVC/MVP 中的靈活度問題等
單一可信數(shù)據(jù)源
我猜測讀者諸君都曾聽過這個詞,將 單一可信數(shù)據(jù)源
拆解一下:
- 單一
- 可信
- 數(shù)據(jù)源
在MVI背景下,數(shù)據(jù)源
指的是視圖對應(yīng)的數(shù)據(jù)實(shí)體,它代表視圖的內(nèi)容狀態(tài)。
可信指從數(shù)據(jù)源中獲取的數(shù)據(jù)是 最新的
、完整的
、可靠的
,否則是不可信的,我們沒有理由在編碼中使用不可信的數(shù)據(jù)源。
單一是指這樣的數(shù)據(jù)源僅一個。
在經(jīng)典設(shè)計(jì)中,其內(nèi)涵如下圖:
- 按照視圖的 所有的 內(nèi)容狀態(tài),定義一個不可變的
ViewState
- 按照業(yè)務(wù)初始化 ViewState 實(shí)例
- Model業(yè)務(wù)生成驅(qū)動 ViewState變化的Result
- 計(jì)算出新狀態(tài),Reduce(Pre-ViewState,Result) -> New-ViewState
- 更新數(shù)據(jù)源
- View層消費(fèi)ViewState
借助于數(shù)據(jù)綁定框架,可以很方便地解決視圖更新的問題。
想象一下,此時頁面UI非常復(fù)雜……
如果僵化的信奉這樣的 單一
,情況會如何呢?
- 復(fù)雜(大量屬性)的ViewState
- 復(fù)雜的UI更新計(jì)算,e.g. 100個屬性變了2個,依然需要計(jì)算98個屬性未變或者全量強(qiáng)制更新
在 APP-A和APP-B中,我分別使用了 DataBinding和Compose,但均無法避免該問題。
何為單一
從機(jī)器執(zhí)行程序的原理上看,我們無法實(shí)現(xiàn) 多個內(nèi)容一致的數(shù)據(jù)源 在 任意時刻 滿足 最新的
、可靠的
。
將視圖視為一個整體,規(guī)定它只擁有 一個 可信的數(shù)據(jù)源。在此基礎(chǔ)上看局部的視圖,它們也順其自然地僅擁有一個可信的數(shù)據(jù)源。
反過來看,當(dāng)任意的局部視圖僅具有一個可信數(shù)據(jù)源時,整體視圖也僅有一個邏輯上的可信數(shù)據(jù)源。
據(jù)此,我們可以對 經(jīng)典MVI實(shí)現(xiàn)
進(jìn)行一定程度的改造,將ViewState進(jìn)行局部分解,使得UI綁定部分的業(yè)務(wù)邏輯更 清晰、干凈。
請注意,復(fù)雜度不會憑空消失,我們?yōu)榱俗?“UI綁定的業(yè)務(wù)邏輯更清晰、干凈”、“更新UI的計(jì)算量更少”,將復(fù)雜度轉(zhuǎn)移到了ViewState的拆分。拆分后,將具有 多個視圖部件的單一可信數(shù)據(jù)源,注意,為了不引起額外的麻煩、并且便于維護(hù)擴(kuò)展,建議遵守以下條件:
- 基于業(yè)務(wù)需求,組合數(shù)據(jù)源形成新數(shù)據(jù)源
- 不在數(shù)據(jù)源的邏輯范圍之外進(jìn)行數(shù)據(jù)源組合操作
舉個虛擬的例子:用戶需要實(shí)名認(rèn)證 且 關(guān)注博主 ,才在界面上顯示某功能按鈕。下面使用代碼分別演示。
考慮到RxJava的廣泛度依舊高于Kotlin-Coroutine+flow,數(shù)據(jù)流的實(shí)現(xiàn)采用RxJava
注意,考慮到讀者可能會編寫demo做UDF局部的驗(yàn)證,下文中的代碼以示例目的為主,兼顧編寫場景冒煙的方便性,流的類型不一定是構(gòu)建完整UDF的最佳選擇。
經(jīng)典實(shí)現(xiàn)
在經(jīng)典MVI實(shí)現(xiàn)中,需要先定義ViewState
data class ViewState(
/*unique id of current login user*/
val userId: Int,
/*true if the current login user has complete real-name verified*/
val realNameVerified: Boolean,
/*true if the current login user has followed the author*/
val hasFollowAuthor: Boolean
) {
}
并定義ViewModel,創(chuàng)建ViewState流,忽略掉其初始化和其他部分
class VM {
val viewState = BehaviorSubject.create<ViewState>()
//ignore
}
并定義View層,忽略掉其他部分,簡單起見暫時不使用數(shù)據(jù)綁定框架
class View {
private val vm = VM()
lateinit var imgRealNameVerified: ImageView
lateinit var cbHasFollowAuthor: CheckBox
lateinit var someButton: Button
fun onCreate() {
//ignore view initialize
vm.viewState.subscribe {
render(it)
}
}
private fun render(state: ViewState) {
imgRealNameVerified.isVisible = state.realNameVerified
cbHasFollowAuthor.isChecked = state.hasFollowAuthor
someButton.isVisible = state.realNameVerified && state.hasFollowAuthor
//ignore other
}
}
在JS中,JSON并不能附加邏輯,基本等價于Java中的POJO,故在數(shù)據(jù)源外部處理簡單邏輯的情況較為常見。而在Java、Kotlin中可以進(jìn)行適當(dāng)?shù)膬?yōu)化,適當(dāng)封裝,使得代碼更加干凈便于維護(hù):
data class ViewState(
//ignore
) {
fun isSomeFuncEnabled():Boolean = realNameVerified && hasFollowAuthor
}
class View {
//ignore
private fun render(state: ViewState) {
//...
someButton.isVisible = state.isSomeFuncEnabled()
}
}
拆分實(shí)現(xiàn)
依舊先定義邏輯上完整的ViewState:
class ComposedViewState(
/*unique id of current login user*/
val userId: Int,
) {
/**
* real-name-verified observable subject,feed true if the current login user has complete real-name verified
* */
val realNameVerified = BehaviorSubject.create<Boolean>()
/**
* follow-author observable subject, feed true if the current login user has followed the author
* */
val hasFollowAuthor = BehaviorSubject.create<Boolean>()
val someFuncEnabled = BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }
}
定義ViewModel,子模塊數(shù)據(jù)流均已定義,故而無需再定義全ViewState的流
class VM(val userId: Int) {
val viewState = ComposedViewState(userId)
//ignore
}
編寫View層的UI綁定,同樣簡單起見,不使用數(shù)據(jù)綁定框架
class View {
private val vm = VM(1)
lateinit var imgRealNameVerified: ImageView
lateinit var cbHasFollowAuthor: CheckBox
lateinit var someButton: Button
fun onCreate() {
//ignore view initialize
bindViewStateWithUI()
}
private fun bindViewStateWithUI() {
vm.viewState.realNameVerified.subscribe {
renderSection1(it)
}
vm.viewState.hasFollowAuthor.subscribe {
renderSection2(it)
}
vm.viewState.someFuncEnabled.subscribe {
renderSection3(it)
}
//...
}
private fun renderSection1(foo:Boolean) {
imgRealNameVerified.isVisible = foo
}
private fun renderSection2(foo:Boolean) {
cbHasFollowAuthor.isChecked = foo
}
private fun renderSection3(foo:Boolean) {
someButton.isVisible = foo
}
}
例子較為簡單,在實(shí)際項(xiàng)目中,如果遇到復(fù)雜頁面,則可以分塊進(jìn)行處理。
注意:實(shí)際情況中,并沒有必要將每一個子數(shù)據(jù)源拆分到一個View級別的控件,那樣過于啰嗦,例子因非常簡單而無法豐滿起來。 e.g. 針對每一塊視圖區(qū),例如作者區(qū)域,定義子ViewState類,創(chuàng)建其數(shù)據(jù)流即可。
作者按:務(wù)必評估,在一次Model業(yè)務(wù)產(chǎn)生的Result中,會引起數(shù)據(jù)流下游的更新次數(shù)。 為避免產(chǎn)生不可預(yù)期的問題,可通過類似以下方式,使下游響應(yīng)次數(shù)表現(xiàn)和經(jīng)典實(shí)現(xiàn)的情況一致。
額外定義PartialChange流或者功能等價的流,它用于標(biāo)識 reduce
計(jì)算的開始和結(jié)束,可以將此期間的數(shù)據(jù)流的變化延遲到最后發(fā)送終態(tài)
更加推薦定義功能上等價的流
class ComposedViewState(
/*unique id of current login user*/
val userId: Int,
) {
internal val changes = BehaviorSubject.create<PartialChange>()
//ignore
val someFuncEnabled =
BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }.sync(PartialChange.Tag, changes)
}
inline fun <reified T, S> Observable<T>.sync(tag: S, sync: BehaviorSubject<S>): Observable<T> {
return BehaviorSubject.combineLatest(this, sync) { source, syncItem ->
if (syncItem == tag) {
syncItem
} else {
source
}
}.filter { it is T }.cast(T::class.java)
}
修改PartialChange,為reduce函數(shù)添加邊界:
PartialChange是Model產(chǎn)生的Result的表現(xiàn)物,封裝了ViewState的reduce函數(shù)邏輯,即如何從 Pre-ViewState 生成 新 ViewState
sealed class PartialChange {
open fun reduce(state: ComposedViewState) {
}
/**
* 同步標(biāo)記,從頭開始到真實(shí)PartialChange之間,流的狀態(tài)生效
* */
object Tag : PartialChange()
object None : PartialChange()
class Foo(val a: Boolean, val b: Boolean) : PartialChange() {
override fun reduce(state: ComposedViewState) {
state.changes.onNext(Tag)
state.realNameVerified.onNext(a)
state.hasFollowAuthor.onNext(b)
state.changes.onNext(this)
}
}
}
要想優(yōu)雅,需要工具
采用響應(yīng)式流,避免命令式編碼
想來這一點(diǎn)已不需要多做解釋。
在Android中,存在 LiveData
組件,它通過簡單的方式封裝了可觀測的數(shù)據(jù),但實(shí)現(xiàn)方式簡單也限制了它的功能 不夠強(qiáng)大 。因此,建議使用 RxJava
或者 Kotlin-Coroutine & flow
構(gòu)建數(shù)據(jù)流。
本節(jié)便不再展開。
采用數(shù)據(jù)綁定框架
采用 jetpack-compose
或者 DataBinding
均可以移除枯燥的UI命令式邏輯,在APP-A中我使用了DataBinding,在APP-B中我使用了Compose。
在 ViewState的代碼很棒時,均可以獲得優(yōu)秀的編程體驗(yàn),從啰嗦的UI中解放出來。
作者的個人觀點(diǎn):
關(guān)于Compose。Compose依舊屬于較新的事物,在商業(yè)項(xiàng)目中使用存在學(xué)習(xí)門檻和造輪工作。在目標(biāo)用戶具有較高容忍度的情況下,已然可以進(jìn)行嘗試。
關(guān)于DataBinding。一個近乎毀譽(yù)參半的工具,關(guān)于它的批判,大多集中于:xml中實(shí)現(xiàn)的邏輯難以閱讀、維護(hù),這實(shí)際上是對DataBinding設(shè)計(jì)的誤解而帶來的錯誤使用。
DataBinding本身具有生成VM層的功能,但這一功能并不足夠強(qiáng)大,且沒有完善的使用指導(dǎo),而在官方Demo中過度宣傳了它,導(dǎo)致大家認(rèn)為DataBinding就該這樣使用。
僅使用基礎(chǔ)的數(shù)據(jù)綁定功能、和Resource或者Context有關(guān)的功能(例如字符串模板)、組件生命周期綁定等,適度自定義綁定。
何為狀態(tài)、何為事件。最后的一公里
首先區(qū)別于上文提到的UI事件,這里的狀態(tài)和事件均產(chǎn)生于數(shù)據(jù)流的末段,而UI事件處于數(shù)據(jù)流的首段。
UI事件屬于:A possible action that the user can perform that is monitored by an application or the operating system (event listener). When an event occurs an event handler is called which performs a specific task
在展開之前,先用一張圖回顧總結(jié)上文中對于 單向數(shù)據(jù)流
& 單一可信數(shù)據(jù)源
的知識
在 單向數(shù)據(jù)流動 章節(jié)中,提到了MVI的UDF設(shè)計(jì):
- 系統(tǒng)捕獲的UI事件、其他偵聽事件(例如熄屏、應(yīng)用生命周期事件),生成Intent,壓入Intent流中
- ViewModel層中篩選、轉(zhuǎn)換、處理Intent,實(shí)際是使用Model層業(yè)務(wù),產(chǎn)生業(yè)務(wù)結(jié)果,即PartialChange
- PartialChange經(jīng)過Reducer計(jì)算處理得到最新的ViewState,壓入ViewState流
- View層(廣義的表現(xiàn)層)響應(yīng)并呈現(xiàn)最新的ViewState
在 單一可信數(shù)據(jù)源 章節(jié)中,提到View層應(yīng)當(dāng)采用 單一可信數(shù)據(jù)源
在這張圖中,我們僅體現(xiàn)了 狀態(tài)
即 ViewState。
關(guān)于GUI程序的認(rèn)知
在展開前,先聊點(diǎn)理念上的內(nèi)容。請讀者諸君思考下自己對于GUI程序的認(rèn)知。
作者的理解:
程序狹義上是計(jì)算機(jī)能識別和執(zhí)行的一組指令集,編程工作是在程序世界對
客觀實(shí)體
、業(yè)務(wù)邏輯
進(jìn)行 建模和邏輯表達(dá)。而GUI程序擁有
用戶圖形界面
, 除了結(jié)合硬件接收用戶交互輸入外,可以將程序世界中的模型
以用戶圖形界面
等方式表現(xiàn)給用戶。表現(xiàn)出來的內(nèi)容代表著客觀實(shí)體
其本質(zhì)目的在于:通過 描述特征屬性 、 描述變化過程 等方式讓用戶感知并理解
客觀實(shí)體
而除了通過 程序語言描述 、 程序世界模擬展現(xiàn) 外,同樣可以通過 自然語言描述 達(dá)到目的,這也是產(chǎn)品經(jīng)理的工作。
當(dāng)然,產(chǎn)品經(jīng)理往往需要借助一些工具來提升自己的自然語言表達(dá)能力,但無奈的是能用數(shù)學(xué)公式和邏輯推演表達(dá)需求的產(chǎn)品經(jīng)理太少見了。
寫這段只是為了引入 他山之石
。
First-Order logic
在數(shù)學(xué)、哲學(xué)、語言學(xué)、計(jì)算機(jī)科學(xué)中,有一個概念 First-Order logic
,無論是產(chǎn)品需求還是計(jì)算機(jī)程序,都可以建立FOL表達(dá)。
當(dāng)然,本篇不討論FOL,那是一個很龐大且偏離主題的事情。我僅僅是想借用其中的概念。
FOL表達(dá) Event或者State時:
- Event 體現(xiàn)的是特定的變化
- State 體現(xiàn)的是客觀實(shí)體在任意時刻都適用的一組情況,即一段時間內(nèi)無變化的條件或者特征
不難理解,變化是瞬時的,連續(xù)的變化是可分的。
但在人機(jī)交互中,瞬時意義很小,我們的目的在于讓用戶感知。
例如:“好友向你發(fā)送了一條消息的場景中”,消息抵達(dá)就是Event,它背后潛藏著 “消息數(shù)的變化”、“最新消息內(nèi)容的變化” 等。 在常見的設(shè)計(jì)中:
- 應(yīng)用需要彈出一個氣泡通知用戶這一事件
- 應(yīng)用需要更新消息數(shù),消息列表內(nèi)容等,以呈現(xiàn)出最新的State
而為了讓用戶感知到,氣泡呈現(xiàn)時長并不是瞬時的,但在產(chǎn)品交互設(shè)計(jì)中依舊將其定義為事件。
分離狀態(tài)和事件,不是吃飽撐得
看山是山、看水是水
此時此刻,答案已經(jīng)很明顯。
在通用的產(chǎn)品設(shè)計(jì)中,狀態(tài)和事件有不同的意義,如果程序中不分離出兩者,則必然是自找麻煩,這是公然挑釁 面向?qū)ο缶幊?/code> 的行為。如果不明確定義不同的Class,則勢必導(dǎo)致代碼混亂不堪,畢竟這是違背編程原則的事情。
在大多MVVM設(shè)計(jì)中,狀態(tài)和事件未分家,導(dǎo)致bug叢生,這一點(diǎn)便不再展開。
如何區(qū)分Event和State
State是一段時間內(nèi)無變化的條件或者特征,它天然的 契合 了位于表現(xiàn)層的主體內(nèi)容所對應(yīng)的 數(shù)據(jù)模型特征。
Event是特定的變化,它在表現(xiàn)層體現(xiàn),但與State的生命周期不一致,且并無一一對應(yīng)的關(guān)系。
基于經(jīng)驗(yàn)主義,我們可以機(jī)械地、籠統(tǒng)地認(rèn)為:頁面主體靜態(tài)內(nèi)容所需要的數(shù)據(jù)屬于State范疇,氣泡提醒等短暫的物體所需要的數(shù)據(jù)屬于Event范疇。
從邏輯推演的角度出發(fā),進(jìn)行 等價邏輯推斷 和 條件限定下的邏輯推斷 ,一定序列的Event可以模型轉(zhuǎn)換為State。
事件粘性導(dǎo)致重復(fù)?只是框架設(shè)計(jì)的bug
看山不是山,看水不是水
前面提到,State是一段時間內(nèi)無變化的條件或者特征,所以在程序設(shè)計(jì)中State具有粘性的特征。
如果Event也設(shè)計(jì)出這樣的粘性特征并造成重復(fù)消費(fèi),明顯是違背需求的,無疑是框架設(shè)計(jì)的Bug。此問題在各大論壇中很常見。
注意,我們無法脫離實(shí)際需求去二元化的討論事件本身該不該有粘性特征,只能結(jié)合實(shí)際討論框架功能是否存在bug
如果要實(shí)現(xiàn)以力破法,在框架設(shè)計(jì)層面上 Event體系的設(shè)計(jì)要比State體系要復(fù)雜 。因?yàn)閺慕换ピO(shè)計(jì)上:
- State 只需要考慮呈現(xiàn)的準(zhǔn)確性和及時性,除去美觀、可理解性等等
- Event 需要考慮準(zhǔn)確性、優(yōu)先級、及時性、按條件丟棄等等,除去美觀、可理解性等等
舉個例子:網(wǎng)絡(luò)連接問題導(dǎo)致的Web-API調(diào)用失敗需要使用Toast提示網(wǎng)絡(luò)連接失敗
不難想象:
- 可能一瞬間的斷開網(wǎng)絡(luò)連接,會導(dǎo)致多個連接均返回失敗
- 可能連接問題未修復(fù),10秒前請求失敗,當(dāng)前請求又失敗了
難道連續(xù)彈出嗎?難道和上一次Event一致就不消費(fèi)嗎?…
或許您會使用一些 劍走偏鋒的技巧
來解決問題,但技巧總是建立在特定條件下生效的,一旦條件發(fā)生變化,就會帶來煩惱,您很難控制上游的PM和交互設(shè)計(jì)師。
所以在框架層面需要針對產(chǎn)品、交互設(shè)計(jì)的泛化理念,設(shè)計(jì)準(zhǔn)確的、靈活的Event體系。
準(zhǔn)確的、靈活的Event體系
看山還是山,看水還是水
回到FOL中,為了更加準(zhǔn)確的表達(dá)Event和State的含義,還需要一些額外的參數(shù),例如:參與者
、地點(diǎn)
、時間
等。
想通這一點(diǎn)會發(fā)現(xiàn),產(chǎn)品中定義的Event事件、及其消費(fèi)邏輯均含有隱藏屬性,例如:
- 發(fā)生時間
- 客觀有效期
- 判斷有效的條件(如呈現(xiàn)的條件)
- 判斷失效的條件 ,用于實(shí)現(xiàn)提前失效
產(chǎn)品經(jīng)理和交互設(shè)計(jì)師一般會使用 “響應(yīng)時間”、“優(yōu)先級” 等詞描述它們,但一般不嚴(yán)謹(jǐn)、不成體系,帶來期望不一致的問題
反觀State流,它代表了界面主體內(nèi)容在時間軸上的完整變化,任意一個時間點(diǎn)均可以得出界面內(nèi)容所對應(yīng)的條件和特征。一旦State流中出現(xiàn)一個新的狀態(tài),它均被及時的、準(zhǔn)確的在表現(xiàn)層予以體現(xiàn)。
不難理解,一個State的生命周期為 從init或者reducer計(jì)算生成開始
至 reducer計(jì)算出新State、宿主生命期結(jié)束為止
,在State流中已然暗含:
- State之間無生命周期重疊
- 所有State的生命周期相加可填滿時間軸
前文提到Event是瞬時的,所以Event本身并沒有實(shí)質(zhì)意義上的生命周期,為了方便表述,我們將 “Event從生成到在表現(xiàn)層不可觀測的階段” 定義為Event生命周期
而Event流 不同于 State流 ,因?yàn)镋vent的生命周期情況更加復(fù)雜:
- Event可能存在生命周期重疊
- 所有Event的生命周期相加可能無法覆蓋完整的時間軸
需要額外設(shè)計(jì)實(shí)現(xiàn) 。實(shí)現(xiàn)這一點(diǎn)后,從Event流中分流(以及裂變+組合)出的 子流 將和State流 性質(zhì)一致。
此刻,您會發(fā)現(xiàn),根據(jù)不同類型的事件交互控件所對應(yīng)的交互特征,又將Event流結(jié)合條件流衍生出各個State流。完整的數(shù)據(jù)流細(xì)節(jié)如下:
文章來源:http://www.zghlxwxcb.cn/news/detail-643764.html
Android 學(xué)習(xí)筆錄
Android 性能優(yōu)化篇:https://qr18.cn/FVlo89
Android 車載篇:https://qr18.cn/F05ZCM
Android 逆向安全學(xué)習(xí)筆記:https://qr18.cn/CQ5TcL
Android Framework底層原理篇:https://qr18.cn/AQpN4J
Android 音視頻篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(內(nèi)含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源碼解析筆記:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知識體:https://qr18.cn/CyxarU
Android 核心筆記:https://qr21.cn/CaZQLo
Android 往年面試題錦:https://qr18.cn/CKV8OZ
2023年最新Android 面試題集:https://qr18.cn/CgxrRy
Android 車載開發(fā)崗位面試習(xí)題:https://qr18.cn/FTlyCJ
音視頻面試題錦:https://qr18.cn/AcV6Ap
文章來源地址http://www.zghlxwxcb.cn/news/detail-643764.html
到了這里,關(guān)于談一談在兩個商業(yè)項(xiàng)目中使用MVI架構(gòu)后的感悟的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!