談一談在兩個商業(yè)項目中使用MVI架構(gòu)后的感悟

前言

MVI并非新興事物踪蹬,在2020年時亦曾有通過撰寫一篇文章與諸位讀者探討一二的念頭篷牌。

當時項目采用MVP分層設(shè)計,組員的代碼風格差異也較大,代碼中類職責賦予與封裝風格各成一套控硼,隨著業(yè)務(wù)急速膨脹泽论,代碼越發(fā)混亂。試圖用 MVI架構(gòu) + 單向流 形成 掣肘 帶來一致風格卡乾。 但這種做法不夠以人為本翼悴,最終采用 "在MVP的基礎(chǔ)上進行了適當改造+設(shè)計約定的方式" 解決了問題,并未將MVI投入到商業(yè)項目中幔妨,于是 放棄了紙上談兵鹦赎。

在半年前終于有機會在商業(yè)項目中進行實踐,同諸位談一談使用后的 個人感悟 误堡,并藉此講透MVI等架構(gòu)古话。

所有內(nèi)容將按照以下要點展開:

  • 從架構(gòu)的理念出發(fā) -- 簡單列明各種 MVX 的理念MVX:指代 MVC锁施、MVP陪踩、MVVM、MVI
  • 擁抱復雜的同時實現(xiàn)簡化 -- 通過對比理解單向數(shù)據(jù)流動所解決的痛點悉抵、設(shè)計Intent的原因等問題
  • 單一可信數(shù)據(jù)源膊毁,不可僵化信奉
  • 要想優(yōu)雅,需要工具 -- 借助聲明式基跑、響應(yīng)式編程工具婚温,構(gòu)建屏蔽命令式編程中的細節(jié)媳否,同樣是聚焦和簡化
  • 狀態(tài)和事件分家栅螟,絕不是吃飽了撐的 -- 為什么要裂變出狀態(tài)和事件,如何界定

內(nèi)容會很長篱竭,我會酌情再寫一些 力图,結(jié)合實例和代碼演示內(nèi)容。

兩個項目的基本情況

相比于之前的巨型項目掺逼,這兩個項目的業(yè)務(wù)量均不大吃媒,一個是基于藍牙和局域網(wǎng)的操控類APP,下午簡稱APP-A吕喘,一個是內(nèi)部使用的工具赘那,分析公司各個產(chǎn)品的日志,簡稱APP-B氯质。

雖然他們的業(yè)務(wù)深度要比一般的APP要深募舟,但在 本質(zhì)上一致 ,畢竟同類型業(yè)務(wù)量再多也僅僅是重復運用一套模式 闻察,并不影響本質(zhì)拱礁。

和諸多項目的本質(zhì)一致琢锋,均符合如下圖所示的邏輯分層,并在人機交互過程中執(zhí)行業(yè)務(wù)邏輯:

  • APP-A 是Android項目呢灶,圖方便純kotlin
  • APP-B 是 Compose-Desktop項目吴超,不得不kotlin

過于絮叨了,我們進入正文鸯乃。

從架構(gòu)的理念出發(fā)

謹記鲸阻,實際情況中,MVI飒责、MVVM這些架構(gòu)均先由Web應(yīng)用領(lǐng)域提出,用于解決瀏覽器Web應(yīng)用研發(fā)中的問題仆潮。

在后續(xù)的應(yīng)用領(lǐng)域發(fā)展過程中宏蛉,存在共性問題,便引入了這些設(shè)計性置,并結(jié)合自身特點進行了拓展拾并。

接下來我們聊一聊理念,不比武功鹏浅。

圖片出自電影一代宗師

MVI的理念

MVI 脫胎于 Model View Intent

  • Intent:驅(qū)動model發(fā)生改變的意圖嗅义,以UI中的事件最為常見;
  • Model:業(yè)務(wù)模型隐砸,包含數(shù)據(jù)和邏輯之碗,是對應(yīng) 客觀實體程序建模
  • View:表現(xiàn)層的視圖季希,以UI方式呈現(xiàn)Model的狀態(tài)(以及事件)褪那,接受用戶輸入,轉(zhuǎn)換為UI事件

官方的這幅圖很好的呈現(xiàn)了三者之間的驅(qū)動關(guān)系:

這張圖非常簡單式塌,它摒棄了驅(qū)動方式的細節(jié)博敬,只體現(xiàn)了角色與驅(qū)動關(guān)系。

注意峰尝,只要設(shè)計中滿足 角色和驅(qū)動關(guān)系 符合上圖偏窝,就是MVI架構(gòu)設(shè)計,并不限制 驅(qū)動方式的實現(xiàn)細節(jié)

經(jīng)典的MVI驅(qū)動細節(jié)要比上圖復雜很多武学,下文再聊祭往。

從軟件設(shè)計的原則出發(fā):職責分離并封裝 的目的是 解耦可獨立變化火窒、復用链沼。

顯然,區(qū)別于 MVVM 沛鸵、 MVP 括勺、 MVC缆八,角色上的差別在于 ViewModel、Presenter疾捍、Controller奈辰、Intent四者,而它們又是View和Model之間的紐帶乱豆。除此之外奖恰,V和M亦稍有不同。

MVC宛裕、MVP

MVC瑟啃、MVP 中,C和P的職責體現(xiàn)為 控制揩尸、調(diào)度蛹屿。

MVP中 VM 完全解耦可獨立變化,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的職責限于維護數(shù)據(jù)狀態(tài)谊囚,如有必要,驅(qū)動View層消費數(shù)據(jù)狀態(tài)执赡, 不必再關(guān)注如何操作視圖镰踏。

一般來說,雙向綁定框架已經(jīng)引入觀察者模式實現(xiàn)沙合,可響應(yīng)式驅(qū)動奠伪,VM一般沒有必要關(guān)心 響應(yīng)式驅(qū)動和下游觀察者生命周期問題

簡單思考之后會發(fā)現(xiàn)MVVM的問題,它的側(cè)重點在于 利用雙向綁定讓開發(fā)者專注于數(shù)據(jù)狀態(tài)的維護首懈,從操作視圖更新中得以解放绊率,它難以解決 無天然狀態(tài) 問題,例如:按鈕點擊這類事件究履。

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)點采用如下設(shè)計:

  • 將雙向綁定退化為單向綁定,View層消費UI狀態(tài)流和事件流欲芹,這也意味著UI狀態(tài)的職責精簡卿啡,它不再承載View層的用戶輸入等事件
  • 將UI狀態(tài)獨立,Model層僅產(chǎn)生 UI狀態(tài)的局部變化事件

下圖為經(jīng)典的MVI原理示意圖:

在上文中菱父,我們已經(jīng)討論了各個角色的職責颈娜,下面逐步展開討論角色具備的特性和細節(jié)知識。

在此之前浙宜,還請謹記:合適的才是最好的

沒有絕對的最好的設(shè)計官辽,只有最合適的設(shè)計。

再好的架構(gòu)粟瞬,都需要遵循其理念并結(jié)合項目因地制宜地進行調(diào)整同仆,以獲得最佳使用效果。所以請讀者諸君務(wù)必在閱讀時亩钟,結(jié)合自身項目的情況仔細思考以下問題:

  • 引入新框架所解決的痛點乓梨、衍生的問題鳖轰、是否需要進行框架調(diào)整清酥?
  • 框架中的角色功能,為什么出現(xiàn)蕴侣,又有怎樣的局限焰轻?

單向數(shù)據(jù)流動

MVI擁抱了結(jié)構(gòu)復雜,但能夠靈活應(yīng)對業(yè)務(wù)編碼時的各種情況昆雀,按部就班即可辱志。

從MVI原理圖中,可以清晰的看到 "數(shù)據(jù)" 的流動方向狞膘。 起始于 Intent揩懒,經(jīng)過分類和選擇性消費后產(chǎn)生 Result,對應(yīng)的reducer函數(shù)計算后挽封,得到最新的 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中的痛點

前文我們提到聋呢,在MVC和MVP中,著眼于 控制遭笋、調(diào)度 坝冕,并不強調(diào) 數(shù)據(jù)流 的概念。

View和Model間之間的交互瓦呼,一般有兩種編碼風格:雙向的API調(diào)用喂窟、單向的API調(diào)用+回調(diào):

注意:以下兩圖并未體現(xiàn)Controller和Presenter細節(jié),僅表意央串,從View層出發(fā)的API調(diào)用和回到View層的UI更新

雙向API調(diào)用如上圖磨澡。

單向API調(diào)用+回調(diào)更新UI如上圖。

顯而易見质和,這兩種方式無法繼續(xù)抽象稳摄,需根據(jù)實際業(yè)務(wù)進行命令式編碼。當UI復雜時饲宿,難以寫出清晰厦酬、易讀的代碼,維護難度激增瘫想。

MVVM解決UI更新代碼混亂問題

前文我們已經(jīng)提到:MVVM中通過綁定框架仗阅,將UI事件轉(zhuǎn)化為數(shù)據(jù)變化,驅(qū)動業(yè)務(wù)国夜;業(yè)務(wù)結(jié)果表現(xiàn)為數(shù)據(jù)變化减噪,驅(qū)動UI更新。

顯而易見车吹,維護樸素的數(shù)據(jù)要比直接維護復雜的UI要簡單筹裕。

但問題也同時產(chǎn)生,data1的變化有兩個可能的原因:

  • Model層業(yè)務(wù)結(jié)果使其變化窄驹,并期望它驅(qū)動UI更新
  • View層發(fā)生事件朝卒,反饋數(shù)據(jù)變化,并期望它驅(qū)動Model層邏輯

因此乐埠,框架需要考慮標識數(shù)據(jù)變化來源抗斤、或者其他手段消除方向性所帶來的問題。

并且MVVM難以靈活決定的 "何時調(diào)用Model層邏輯"饮戳,即大多數(shù)業(yè)務(wù)中豪治,都需要結(jié)合多個屬性的變化形成組合條件來驅(qū)動Model層邏輯。

本篇并不重點討論MVVM扯罐,故不再展開MVVM解決循環(huán)更新的方案负拟,以及衍生的問題。

盡管如此歹河,MVVM中的數(shù)據(jù)綁定依舊解決了View層更新繁雜的問題掩浙。

用Intent靈活決定何時調(diào)用Model

既然數(shù)據(jù)驅(qū)動UI有極大的益處花吟,且View層事件驅(qū)動ViewModel的數(shù)據(jù)變化有很多弊端 (需要建立很高的復雜度) ,那自然需要 趨利避害

僅保留數(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ù)也會增多,導致C/P層尾大不掉拭抬,難以重用部默。
  • MVVM中,VM層總是需要利用 技巧 進行模型概念轉(zhuǎn)換造虎,以滿足業(yè)務(wù)響應(yīng)滿足實際需求傅蹂,需要很深厚的設(shè)計經(jīng)驗才能寫出非常優(yōu)秀的代碼,這并不友好算凿。

作者按:我個人認為一個友好的設(shè)計份蝴,不應(yīng)當劍走偏鋒,而應(yīng)當大巧不工氓轰,能夠以力破法婚夫,達成 "使用者只需要吃透理論就可以解決各類問題" 的目標。

而MVI在架構(gòu)角色中設(shè)計了Intent的角色:

  • 它包含了業(yè)務(wù)調(diào)用的意圖和數(shù)據(jù)
  • 從設(shè)計上可滿足 調(diào)用實現(xiàn) 的分離
  • 架構(gòu)模型中以Intent流的形式出現(xiàn)戒努,下游對其的 篩選 请敦、轉(zhuǎn)換 镐躲、 消費 等行為可遵循 FP范式 (即函數(shù)式編程范式储玫、Functional Programming Patterns) ,邏輯的復用粒度為方法級萤皂,復用度更高更靈活
  • 解決了MVVM中的方向性問題撒穷、MVC/MVP 中的靈活度問題等

單一可信數(shù)據(jù)源

我猜測讀者諸君都曾聽過這個詞,將 單一可信數(shù)據(jù)源 拆解一下:

  • 單一
  • 可信
  • 數(shù)據(jù)源

在MVI背景下裆熙,數(shù)據(jù)源 指的是視圖對應(yīng)的數(shù)據(jù)實體端礼,它代表視圖的內(nèi)容狀態(tài)。

可信指從數(shù)據(jù)源中獲取的數(shù)據(jù)是 最新的入录、完整的蛤奥、可靠的,否則是不可信的僚稿,我們沒有理由在編碼中使用不可信的數(shù)據(jù)源凡桥。

單一是指這樣的數(shù)據(jù)源僅一個。

在經(jīng)典設(shè)計中蚀同,其內(nèi)涵如下圖:

  • 按照視圖的 所有的 內(nèi)容狀態(tài)拜秧,定義一個不可變的 ViewState
  • 按照業(yè)務(wù)初始化 ViewState 實例
  • Model業(yè)務(wù)生成驅(qū)動 ViewState變化的Result
  • 計算出新狀態(tài)禀倔,Reduce(Pre-ViewState,Result) -> New-ViewState
  • 更新數(shù)據(jù)源
  • View層消費ViewState

借助于數(shù)據(jù)綁定框架刃榨,可以很方便地解決視圖更新的問題。

想象一下迟蜜,此時頁面UI非常復雜……

如果僵化的信奉這樣的 單一 ,情況會如何呢啡省?

  • 復雜(大量屬性)的ViewState
  • 復雜的UI更新計算娜睛,e.g. 100個屬性變了2個,依然需要計算98個屬性未變或者全量強制更新

在 APP-A和APP-B中卦睹,我分別使用了 DataBinding和Compose微姊,但均無法避免該問題。

何為單一

從機器執(zhí)行程序的原理上看分预,我們無法實現(xiàn) 多個內(nèi)容一致的數(shù)據(jù)源任意時刻 滿足 最新的兢交、可靠的

將視圖視為一個整體笼痹,規(guī)定它只擁有 一個 可信的數(shù)據(jù)源配喳。在此基礎(chǔ)上看局部的視圖,它們也順其自然地僅擁有一個可信的數(shù)據(jù)源凳干。

反過來看晴裹,當任意的局部視圖僅具有一個可信數(shù)據(jù)源時,整體視圖也僅有一個邏輯上的可信數(shù)據(jù)源救赐。

據(jù)此涧团,我們可以對 經(jīng)典MVI實現(xiàn) 進行一定程度的改造,將ViewState進行局部分解经磅,使得UI綁定部分的業(yè)務(wù)邏輯更 清晰泌绣、干凈

請注意预厌,復雜度不會憑空消失阿迈,我們?yōu)榱俗?"UI綁定的業(yè)務(wù)邏輯更清晰、干凈"轧叽、"更新UI的計算量更少"苗沧,將復雜度轉(zhuǎn)移到了ViewState的拆分。拆分后炭晒,將具有 多個視圖部件的單一可信數(shù)據(jù)源待逞,注意,為了不引起額外的麻煩网严、并且便于維護擴展识樱,建議遵守以下條件:

  • 基于業(yè)務(wù)需求,組合數(shù)據(jù)源形成新數(shù)據(jù)源
  • 不在數(shù)據(jù)源的邏輯范圍之外進行數(shù)據(jù)源組合操作

舉個虛擬的例子:用戶需要實名認證 且 關(guān)注博主 ,才在界面上顯示某功能按鈕牺荠。下面使用代碼分別演示翁巍。

考慮到RxJava的廣泛度依舊高于Kotlin-Coroutine+flow,數(shù)據(jù)流的實現(xiàn)采用RxJava

注意休雌,考慮到讀者可能會編寫demo做UDF局部的驗證灶壶,下文中的代碼以示例目的為主,兼顧編寫場景冒煙的方便性杈曲,流的類型不一定是構(gòu)建完整UDF的最佳選擇驰凛。

經(jīng)典實現(xiàn)

在經(jīng)典MVI實現(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中可以進行適當?shù)膬?yōu)化卜壕,適當封裝您旁,使得代碼更加干凈便于維護:

data class ViewState(
    //ignore
) {
    fun isSomeFuncEnabled():Boolean = realNameVerified && hasFollowAuthor
}

class View {
    //ignore

    private fun render(state: ViewState) {
        //...

        someButton.isVisible = state.isSomeFuncEnabled()
    }
}

拆分實現(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ù)據(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)典實現(xiàn)的情況一致。

額外定義PartialChange流或者功能等價的流嗤锉,它用于標識 reduce 計算的開始和結(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) {

    }

    /**
     * 同步標記,從頭開始到真實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)式流,避免命令式編碼

想來這一點已不需要多做解釋触菜。

在Android中九榔,存在 LiveData 組件,它通過簡單的方式封裝了可觀測的數(shù)據(jù)涡相,但實現(xiàn)方式簡單也限制了它的功能 不夠強大 帚屉。因此,建議使用 RxJava 或者 Kotlin-Coroutine & flow 構(gòu)建數(shù)據(jù)流漾峡。

本節(jié)便不再展開攻旦。

采用數(shù)據(jù)綁定框架

采用 jetpack-compose 或者 DataBinding 均可以移除枯燥的UI命令式邏輯,在APP-A中我使用了DataBinding生逸,在APP-B中我使用了Compose牢屋。

在 ViewState的代碼很棒時,均可以獲得優(yōu)秀的編程體驗槽袄,從啰嗦的UI中解放出來烙无。

作者的個人觀點:

關(guān)于Compose。Compose依舊屬于較新的事物遍尺,在商業(yè)項目中使用存在學習門檻和造輪工作截酷。在目標用戶具有較高容忍度的情況下,已然可以進行嘗試乾戏。

關(guān)于DataBinding迂苛。一個近乎毀譽參半的工具,關(guān)于它的批判鼓择,大多集中于:xml中實現(xiàn)的邏輯難以閱讀三幻、維護,這實際上是對DataBinding設(shè)計的誤解而帶來的錯誤使用呐能。

DataBinding本身具有生成VM層的功能念搬,但這一功能并不足夠強大,且沒有完善的使用指導,而在官方Demo中過度宣傳了它朗徊,導致大家認為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è)計:

  • 系統(tǒng)捕獲的UI事件在刺、其他偵聽事件(例如熄屏、應(yīng)用生命周期事件)头镊,生成Intent蚣驼,壓入Intent流中
  • ViewModel層中篩選、轉(zhuǎn)換相艇、處理Intent颖杏,實際是使用Model層業(yè)務(wù),產(chǎn)生業(yè)務(wù)結(jié)果坛芽,即PartialChange
  • PartialChange經(jīng)過Reducer計算處理得到最新的ViewState留储,壓入ViewState流
  • View層(廣義的表現(xiàn)層)響應(yīng)并呈現(xiàn)最新的ViewState

單一可信數(shù)據(jù)源 章節(jié)中,提到View層應(yīng)當采用 單一可信數(shù)據(jù)源

在這張圖中咙轩,我們僅體現(xiàn)了 狀態(tài) 即 ViewState获讳。

關(guān)于GUI程序的認知

在展開前,先聊點理念上的內(nèi)容活喊。請讀者諸君思考下自己對于GUI程序的認知丐膝。

作者的理解:

程序狹義上是計算機能識別和執(zhí)行的一組指令集,編程工作是在程序世界對 客觀實體 钾菊、 業(yè)務(wù)邏輯 進行 建模和邏輯表達帅矗。

而GUI程序擁有 用戶圖形界面 , 除了結(jié)合硬件接收用戶交互輸入外,可以將 程序世界中的模型用戶圖形界面 等方式表現(xiàn)給用戶结缚。

表現(xiàn)出來的內(nèi)容代表著客觀實體

其本質(zhì)目的在于:通過 描述特征屬性 损晤、 描述變化過程 等方式讓用戶感知并理解 客觀實體

而除了通過 程序語言描述程序世界模擬展現(xiàn) 外红竭,同樣可以通過 自然語言描述 達到目的,這也是產(chǎn)品經(jīng)理的工作。

當然茵宪,產(chǎn)品經(jīng)理往往需要借助一些工具來提升自己的自然語言表達能力最冰,但無奈的是能用數(shù)學公式和邏輯推演表達需求的產(chǎn)品經(jīng)理太少見了。

寫這段只是為了引入 他山之石 稀火。

First-Order logic

在數(shù)學暖哨、哲學、語言學凰狞、計算機科學中篇裁,有一個概念 First-Order logic,無論是產(chǎn)品需求還是計算機程序赡若,都可以建立FOL表達达布。

當然,本篇不討論FOL逾冬,那是一個很龐大且偏離主題的事情黍聂。我僅僅是想借用其中的概念。

FOL表達 Event或者State時:

  • Event 體現(xiàn)的是特定的變化
  • State 體現(xiàn)的是客觀實體在任意時刻都適用的一組情況身腻,即一段時間內(nèi)無變化的條件或者特征

不難理解产还,變化是瞬時的,連續(xù)的變化是可分的嘀趟。

但在人機交互中脐区,瞬時意義很小,我們的目的在于讓用戶感知她按。

例如:"好友向你發(fā)送了一條消息的場景中"坡椒,消息抵達就是Event,它背后潛藏著 "消息數(shù)的變化"尤溜、"最新消息內(nèi)容的變化" 等倔叼。 在常見的設(shè)計中:

  • 應(yīng)用需要彈出一個氣泡通知用戶這一事件
  • 應(yīng)用需要更新消息數(shù),消息列表內(nèi)容等宫莱,以呈現(xiàn)出最新的State

而為了讓用戶感知到丈攒,氣泡呈現(xiàn)時長并不是瞬時的,但在產(chǎn)品交互設(shè)計中依舊將其定義為事件授霸。

分離狀態(tài)和事件巡验,不是吃飽撐得

看山是山、看水是水

此時此刻碘耳,答案已經(jīng)很明顯显设。

在通用的產(chǎn)品設(shè)計中,狀態(tài)和事件有不同的意義辛辨,如果程序中不分離出兩者捕捂,則必然是自找麻煩瑟枫,這是公然挑釁 面向?qū)ο缶幊?/code> 的行為。如果不明確定義不同的Class指攒,則勢必導致代碼混亂不堪慷妙,畢竟這是違背編程原則的事情。

在大多MVVM設(shè)計中允悦,狀態(tài)和事件未分家膝擂,導致bug叢生,這一點便不再展開隙弛。

如何區(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)驗主義办绝,我們可以機械地、籠統(tǒng)地認為:頁面主體靜態(tài)內(nèi)容所需要的數(shù)據(jù)屬于State范疇姚淆,氣泡提醒等短暫的物體所需要的數(shù)據(jù)屬于Event范疇孕蝉。

從邏輯推演的角度出發(fā),進行 等價邏輯推斷條件限定下的邏輯推斷 腌逢,一定序列的Event可以模型轉(zhuǎn)換為State降淮。

事件粘性導致重復?只是框架設(shè)計的bug

看山不是山搏讶,看水不是水

前面提到佳鳖,State是一段時間內(nèi)無變化的條件或者特征,所以在程序設(shè)計中State具有粘性的特征媒惕。

如果Event也設(shè)計出這樣的粘性特征并造成重復消費系吩,明顯是違背需求的,無疑是框架設(shè)計的Bug妒蔚。此問題在各大論壇中很常見穿挨。

注意,我們無法脫離實際需求去二元化的討論事件本身該不該有粘性特征肴盏,只能結(jié)合實際討論框架功能是否存在bug

如果要實現(xiàn)以力破法科盛,在框架設(shè)計層面上 Event體系的設(shè)計要比State體系要復雜 。因為從交互設(shè)計上:

  • State 只需要考慮呈現(xiàn)的準確性和及時性菜皂,除去美觀贞绵、可理解性等等
  • Event 需要考慮準確性、優(yōu)先級恍飘、及時性榨崩、按條件丟棄等等谴垫,除去美觀、可理解性等等

舉個例子:網(wǎng)絡(luò)連接問題導致的Web-API調(diào)用失敗需要使用Toast提示網(wǎng)絡(luò)連接失敗

不難想象:

  • 可能一瞬間的斷開網(wǎng)絡(luò)連接蜡饵,會導致多個連接均返回失敗
  • 可能連接問題未修復弹渔,10秒前請求失敗胳施,當前請求又失敗了

難道連續(xù)彈出嗎溯祸?難道和上一次Event一致就不消費嗎?...

或許您會使用一些 劍走偏鋒的技巧 來解決問題舞肆,但技巧總是建立在特定條件下生效的焦辅,一旦條件發(fā)生變化,就會帶來煩惱椿胯,您很難控制上游的PM和交互設(shè)計師筷登。

所以在框架層面需要針對產(chǎn)品、交互設(shè)計的泛化理念哩盲,設(shè)計準確的前方、靈活的Event體系。

準確的廉油、靈活的Event體系

看山還是山惠险,看水還是水

回到FOL中,為了更加準確的表達Event和State的含義抒线,還需要一些額外的參數(shù)班巩,例如:參與者地點嘶炭、時間 等抱慌。

想通這一點會發(fā)現(xiàn),產(chǎn)品中定義的Event事件眨猎、及其消費邏輯均含有隱藏屬性抑进,例如:

  • 發(fā)生時間
  • 客觀有效期
  • 判斷有效的條件(如呈現(xiàn)的條件)
  • 判斷失效的條件 ,用于實現(xiàn)提前失效

產(chǎn)品經(jīng)理和交互設(shè)計師一般會使用 "響應(yīng)時間"睡陪、"優(yōu)先級" 等詞描述它們寺渗,但一般不嚴謹、不成體系宝穗,帶來期望不一致的問題

反觀State流户秤,它代表了界面主體內(nèi)容在時間軸上的完整變化,任意一個時間點均可以得出界面內(nèi)容所對應(yīng)的條件和特征逮矛。一旦State流中出現(xiàn)一個新的狀態(tài)鸡号,它均被及時的、準確的在表現(xiàn)層予以體現(xiàn)须鼎。

不難理解鲸伴,一個State的生命周期為 從init或者reducer計算生成開始reducer計算出新State府蔗、宿主生命期結(jié)束為止,在State流中已然暗含:

  • State之間無生命周期重疊
  • 所有State的生命周期相加可填滿時間軸

前文提到Event是瞬時的汞窗,所以Event本身并沒有實質(zhì)意義上的生命周期姓赤,為了方便表述,我們將 "Event從生成到在表現(xiàn)層不可觀測的階段" 定義為Event生命周期

而Event流 不同于 State流 仲吏,因為Event的生命周期情況更加復雜:

  • Event可能存在生命周期重疊
  • 所有Event的生命周期相加可能無法覆蓋完整的時間軸

需要額外設(shè)計實現(xiàn) 不铆。實現(xiàn)這一點后,從Event流中分流(以及裂變+組合)出的 子流 將和State流 性質(zhì)一致裹唆。

此刻誓斥,您會發(fā)現(xiàn),根據(jù)不同類型的事件交互控件所對應(yīng)的交互特征许帐,又將Event流結(jié)合條件流衍生出各個State流劳坑。完整的數(shù)據(jù)流細節(jié)如下:

作者按:在圖中省略了Event分流轉(zhuǎn)變?yōu)樽覵tate流的過程,因為它需要遵循特定產(chǎn)品交互機制

結(jié)語

這篇文章成畦,從5月計劃寫距芬,到6月動筆,斷斷續(xù)續(xù)循帐,草稿寫了很長框仔,幾經(jīng)刪改依舊留有很長的篇幅,雖已竭力盡智惧浴,但任覺文字上有表意未通透之處存和,歡迎在評論區(qū)討論。

作者:leobert-lan
鏈接:https://juejin.cn/post/7135328592673636359

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末衷旅,一起剝皮案震驚了整個濱河市捐腿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌柿顶,老刑警劉巖茄袖,帶你破解...
    沈念sama閱讀 222,946評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嘁锯,居然都是意外死亡宪祥,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評論 3 399
  • 文/潘曉璐 我一進店門家乘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝗羊,“玉大人,你說我怎么就攤上這事仁锯∫遥” “怎么了?”我有些...
    開封第一講書人閱讀 169,716評論 0 364
  • 文/不壞的土叔 我叫張陵业崖,是天一觀的道長野芒。 經(jīng)常有香客問我蓄愁,道長,這世上最難降的妖魔是什么狞悲? 我笑而不...
    開封第一講書人閱讀 60,222評論 1 300
  • 正文 為了忘掉前任撮抓,我火速辦了婚禮,結(jié)果婚禮上摇锋,老公的妹妹穿的比我還像新娘丹拯。我一直安慰自己,他們只是感情好乱投,可當我...
    茶點故事閱讀 69,223評論 6 398
  • 文/花漫 我一把揭開白布咽笼。 她就那樣靜靜地躺著顷编,像睡著了一般戚炫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上媳纬,一...
    開封第一講書人閱讀 52,807評論 1 314
  • 那天双肤,我揣著相機與錄音,去河邊找鬼钮惠。 笑死茅糜,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的素挽。 我是一名探鬼主播蔑赘,決...
    沈念sama閱讀 41,235評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼预明!你這毒婦竟也來了缩赛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,189評論 0 277
  • 序言:老撾萬榮一對情侶失蹤撰糠,失蹤者是張志新(化名)和其女友劉穎酥馍,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阅酪,經(jīng)...
    沈念sama閱讀 46,712評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡旨袒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,775評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了术辐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片砚尽。...
    茶點故事閱讀 40,926評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖辉词,靈堂內(nèi)的尸體忽然破棺而出必孤,到底是詐尸還是另有隱情,我是刑警寧澤较屿,帶...
    沈念sama閱讀 36,580評論 5 351
  • 正文 年R本政府宣布隧魄,位于F島的核電站卓练,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏购啄。R本人自食惡果不足惜襟企,卻給世界環(huán)境...
    茶點故事閱讀 42,259評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望狮含。 院中可真熱鬧顽悼,春花似錦、人聲如沸几迄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽映胁。三九已至木羹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間解孙,已是汗流浹背坑填。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留弛姜,地道東北人脐瑰。 一個月前我還...
    沈念sama閱讀 49,368評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像廷臼,于是被迫代替她去往敵國和親苍在。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,930評論 2 361

推薦閱讀更多精彩內(nèi)容