談一談在兩個商業項目中使用MVI架構后的感悟

作者:leobertlan

前言

當時項目采用MVP分層設計,組員的代碼風格差異也較大,代碼中類職責賦予與封裝風格各成一套,隨著業務急速膨脹,代碼越發混亂。試圖用 MVI架構 + 單向流 形成 掣肘 帶來一致風格。 但這種做法不夠以人為本,最終采用 “在MVP的基礎上進行了適當改造+設計約定的方式” 解決了問題,并未將MVI投入到商業項目中,于是 放棄了紙上談兵

在半年前終于有機會在商業項目中進行實踐,同諸位談一談使用后的 個人感悟 ,并藉此講透MVI等架構。

所有內容將按照以下要點展開:

  • 從架構的理念出發 – 簡單列明各種 MVX 的理念MVX:指代 MVC、MVP、MVVM、MVI
  • 擁抱復雜的同時實現簡化 – 通過對比理解單向數據流動所解決的痛點、設計Intent的原因等問題
  • 單一可信數據源,不可僵化信奉
  • 要想優雅,需要工具 – 借助聲明式、響應式編程工具,構建屏蔽命令式編程中的細節,同樣是聚焦和簡化
  • 狀態和事件分家,絕不是吃飽了撐的 – 為什么要裂變出狀態和事件,如何界定

內容會很長,我會酌情再寫一些 ,結合實例和代碼演示內容。

兩個項目的基本情況

相比于之前的巨型項目,這兩個項目的業務量均不大,一個是基于藍牙和局域網的操控類APP,下午簡稱APP-A,一個是內部使用的工具,分析公司各個產品的日志,簡稱APP-B。

雖然他們的業務深度要比一般的APP要深,但在 本質上一致 ,畢竟同類型業務量再多也僅僅是重復運用一套模式 ,并不影響本質。

和諸多項目的本質一致,均符合如下圖所示的邏輯分層,并在人機交互過程中執行業務邏輯:

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

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

從架構的理念出發

謹記,實際情況中,MVI、MVVM這些架構均先由Web應用領域提出,用于解決瀏覽器Web應用研發中的問題。

在后續的應用領域發展過程中,存在共性問題,便引入了這些設計,并結合自身特點進行了拓展。

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

圖片出自電影一代宗師

MVI的理念

MVI 脫胎于 Model View Intent

  • Intent:驅動model發生改變的意圖,以UI中的事件最為常見;
  • Model:業務模型,包含數據和邏輯,是對應 客觀實體程序建模
  • View:表現層的視圖,以UI方式呈現Model的狀態(以及事件),接受用戶輸入,轉換為UI事件

官方的這幅圖很好的呈現了三者之間的驅動關系:

這張圖非常簡單,它摒棄了驅動方式的細節,只體現了角色與驅動關系。

注意,只要設計中滿足 角色和驅動關系 符合上圖,就是MVI架構設計,并不限制 驅動方式的實現細節

經典的MVI驅動細節要比上圖復雜很多,下文再聊。

從軟件設計的原則出發:職責分離并封裝 的目的是 解耦可獨立變化復用

顯然,區別于 MVVMMVPMVC,角色上的差別在于 ViewModel、Presenter、Controller、Intent四者,而它們又是View和Model之間的紐帶。除此之外,V和M亦稍有不同。

MVC、MVP

MVC、MVP 中,C和P的職責體現為 控制、調度

MVP中 VM 完全解耦可獨立變化,MVC中 M 直接操作 V 耦合高,在web應用中,C 需要直接操作DOM。

MVVM

MVVM中,提倡 數據驅動數據源 被剝離到 VM 中,在 雙向綁定框架 的加持下,View層的輸入反映為數據的變化,數據的變化驅動視圖內容。

顯然,VM的職責限于維護數據狀態,如有必要,驅動View層消費數據狀態, 不必再關注如何操作視圖。

一般來說,雙向綁定框架已經引入觀察者模式實現,可響應式驅動,VM一般沒有必要關心 響應式驅動和下游觀察者生命周期問題

簡單思考之后會發現MVVM的問題,它的側重點在于 利用雙向綁定讓開發者專注于數據狀態的維護,從操作視圖更新中得以解放,它難以解決 無天然狀態 問題,例如:按鈕點擊這類事件。

MVI

在MVI中,結合業務背景將UI事件等內容轉換為 Intent ,驅動Model層業務,Model層的業務結果反映為 視圖狀態 + 事件

因此View層和Model層之間已經解耦,并可以吸收MVVM中的優點采用如下設計:

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

下圖為經典的MVI原理示意圖:

在上文中,我們已經討論了各個角色的職責,下面逐步展開討論角色具備的特性和細節知識。

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

沒有絕對的最好的設計,只有最合適的設計。

再好的架構,都需要遵循其理念并結合項目因地制宜地進行調整,以獲得最佳使用效果。所以請讀者諸君務必在閱讀時,結合自身項目的情況仔細思考以下問題:

  • 引入新框架所解決的痛點、衍生的問題、是否需要進行框架調整?
  • 框架中的角色功能,為什么出現,又有怎樣的局限?

單向數據流動

MVI擁抱了結構復雜,但能夠靈活應對業務編碼時的各種情況,按部就班即可。

從MVI原理圖中,可以清晰的看到 “數據” 的流動方向。 起始于 Intent,經過分類和選擇性消費后產生 Result,對應的reducer函數計算后,得到最新的 State (以及裂變出必要的 Event,圖中未體現) ,驅動視圖。

注意:

  • 單向 是指 單一方向
  • 此處的 數據 是廣義的、寬泛的。
  • 僅描述數據流的 變化方向 ,與數據流的數量無關,但一般 形成有效工作 均需要兩條數據流(上行數據流和下行數據流)

即驅動數據流變化的方向是唯一的,在英文中的術語為:Unidirectional Data Flow 簡稱 UDF

MVC、MVP中的痛點

前文我們提到,在MVC和MVP中,著眼于 控制、調度 ,并不強調 數據流 的概念。

View和Model間之間的交互,一般有兩種編碼風格:雙向的API調用、單向的API調用+回調:

注意:以下兩圖并未體現Controller和Presenter細節,僅表意,從View層出發的API調用和回到View層的UI更新

雙向API調用如上圖。

單向API調用+回調更新UI如上圖。

顯而易見,這兩種方式無法繼續抽象,需根據實際業務進行命令式編碼。當UI復雜時,難以寫出清晰、易讀的代碼,維護難度激增。

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

前文我們已經提到:MVVM中通過綁定框架,將UI事件轉化為數據變化,驅動業務;業務結果表現為數據變化,驅動UI更新。

顯而易見,維護樸素的數據要比直接維護復雜的UI要簡單

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

  • Model層業務結果使其變化,并期望它驅動UI更新
  • View層發生事件,反饋數據變化,并期望它驅動Model層邏輯

因此,框架需要考慮標識數據變化來源、或者其他手段消除方向性所帶來的問題。

并且MVVM難以靈活決定的 “何時調用Model層邏輯”,即大多數業務中,都需要結合多個屬性的變化形成組合條件來驅動Model層邏輯。

本篇并不重點討論MVVM,故不再展開MVVM解決循環更新的方案,以及衍生的問題。

盡管如此,MVVM中的數據綁定依舊解決了View層更新繁雜的問題。

用Intent靈活決定何時調用Model

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

僅保留數據驅動UI的部分,并增加Intent用以驅動Model層業務

在于 MVC/MVP 以及 MVVM 對比后不難得出結論:

  • MVC/MVP中,View層通過調用C/P層API的方式最終調用到Model層業務,方式質樸、無難度。但業務量規模增大后接口方法數也會增多,導致C/P層尾大不掉,難以重用。
  • MVVM中,VM層總是需要利用 技巧 進行模型概念轉換,以滿足業務響應滿足實際需求,需要很深厚的設計經驗才能寫出非常優秀的代碼,這并不友好。

作者按:我個人認為一個友好的設計,不應當劍走偏鋒,而應當大巧不工,能夠以力破法,達成 “使用者只需要吃透理論就可以解決各類問題” 的目標。

而MVI在架構角色中設計了Intent的角色:

  • 它包含了業務調用的意圖和數據
  • 從設計上可滿足 調用實現 的分離
  • 架構模型中以Intent流的形式出現,下游對其的 篩選轉換消費 等行為可遵循 FP范式 (即函數式編程范式、Functional Programming Patterns) ,邏輯的復用粒度為方法級,復用度更高更靈活
  • 解決了MVVM中的方向性問題、MVC/MVP 中的靈活度問題等

單一可信數據源

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

  • 單一
  • 可信
  • 數據源

在MVI背景下,數據源 指的是視圖對應的數據實體,它代表視圖的內容狀態。

可信指從數據源中獲取的數據是 最新的完整的可靠的,否則是不可信的,我們沒有理由在編碼中使用不可信的數據源

單一是指這樣的數據源僅一個。

在經典設計中,其內涵如下圖:

  • 按照視圖的 所有的 內容狀態,定義一個不可變的 ViewState
  • 按照業務初始化 ViewState 實例
  • Model業務生成驅動 ViewState變化的Result
  • 計算出新狀態,Reduce(Pre-ViewState,Result) -> New-ViewState
  • 更新數據源
  • View層消費ViewState

借助于數據綁定框架,可以很方便地解決視圖更新的問題。

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

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

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

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

何為單一

從機器執行程序的原理上看,我們無法實現 多個內容一致的數據源任意時刻 滿足 最新的可靠的

將視圖視為一個整體,規定它只擁有 一個 可信的數據源。在此基礎上看局部的視圖,它們也順其自然地僅擁有一個可信的數據源。

反過來看,當任意的局部視圖僅具有一個可信數據源時,整體視圖也僅有一個邏輯上的可信數據源。

據此,我們可以對 經典MVI實現 進行一定程度的改造,將ViewState進行局部分解,使得UI綁定部分的業務邏輯更 清晰、干凈

請注意,復雜度不會憑空消失,我們為了讓 “UI綁定的業務邏輯更清晰、干凈”、“更新UI的計算量更少”,將復雜度轉移到了ViewState的拆分。拆分后,將具有 多個視圖部件的單一可信數據源,注意,為了不引起額外的麻煩、并且便于維護擴展,建議遵守以下條件:

  • 基于業務需求,組合數據源形成新數據源
  • 不在數據源的邏輯范圍之外進行數據源組合操作

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

考慮到RxJava的廣泛度依舊高于Kotlin-Coroutine+flow,數據流的實現采用RxJava

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

經典實現

在經典MVI實現中,需要先定義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,創建ViewState流,忽略掉其初始化和其他部分

class VM {val viewState = BehaviorSubject.create<ViewState>()//ignore
}

并定義View層,忽略掉其他部分,簡單起見暫時不使用數據綁定框架

class View {private val vm = VM()lateinit var imgRealNameVerified: ImageViewlateinit var cbHasFollowAuthor: CheckBoxlateinit var someButton: Buttonfun onCreate() {//ignore view initializevm.viewState.subscribe {render(it)}}private fun render(state: ViewState) {imgRealNameVerified.isVisible = state.realNameVerifiedcbHasFollowAuthor.isChecked = state.hasFollowAuthorsomeButton.isVisible = state.realNameVerified && state.hasFollowAuthor//ignore other}
}

在JS中,JSON并不能附加邏輯,基本等價于Java中的POJO,故在數據源外部處理簡單邏輯的情況較為常見。而在Java、Kotlin中可以進行適當的優化,適當封裝,使得代碼更加干凈便于維護:

data class ViewState(//ignore
) {fun isSomeFuncEnabled():Boolean = realNameVerified && hasFollowAuthor
}class View {//ignoreprivate fun render(state: ViewState) {//...someButton.isVisible = state.isSomeFuncEnabled()}
}

拆分實現

依舊先定義邏輯上完整的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,子模塊數據流均已定義,故而無需再定義全ViewState的流

class VM(val userId: Int) {val viewState = ComposedViewState(userId)//ignore
}

編寫View層的UI綁定,同樣簡單起見,不使用數據綁定框架

class View {private val vm = VM(1)lateinit var imgRealNameVerified: ImageViewlateinit var cbHasFollowAuthor: CheckBoxlateinit var someButton: Buttonfun onCreate() {//ignore view initializebindViewStateWithUI()}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}
}

例子較為簡單,在實際項目中,如果遇到復雜頁面,則可以分塊進行處理。

注意:實際情況中,并沒有必要將每一個子數據源拆分到一個View級別的控件,那樣過于啰嗦,例子因非常簡單而無法豐滿起來。 e.g. 針對每一塊視圖區,例如作者區域,定義子ViewState類,創建其數據流即可。

作者按:務必評估,在一次Model業務產生的Result中,會引起數據流下游的更新次數。 為避免產生不可預期的問題,可通過類似以下方式,使下游響應次數表現和經典實現的情況一致。

額外定義PartialChange流或者功能等價的流,它用于標識 reduce 計算的開始和結束,可以將此期間的數據流的變化延遲到最后發送終態

更加推薦定義功能上等價的流

class ComposedViewState(/*unique id of current login user*/val userId: Int,
) {internal val changes = BehaviorSubject.create<PartialChange>()//ignoreval 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函數添加邊界:

PartialChange是Model產生的Result的表現物,封裝了ViewState的reduce函數邏輯,即如何從 Pre-ViewState 生成 新 ViewState

sealed class PartialChange {open fun reduce(state: ComposedViewState) {}/*** 同步標記,從頭開始到真實PartialChange之間,流的狀態生效* */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)}}
}

要想優雅,需要工具

采用響應式流,避免命令式編碼

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

在Android中,存在 LiveData 組件,它通過簡單的方式封裝了可觀測的數據,但實現方式簡單也限制了它的功能 不夠強大 。因此,建議使用 RxJava 或者 Kotlin-Coroutine & flow 構建數據流。

本節便不再展開。

采用數據綁定框架

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

在 ViewState的代碼很棒時,均可以獲得優秀的編程體驗,從啰嗦的UI中解放出來。

作者的個人觀點:

關于Compose。Compose依舊屬于較新的事物,在商業項目中使用存在學習門檻和造輪工作。在目標用戶具有較高容忍度的情況下,已然可以進行嘗試。

關于DataBinding。一個近乎毀譽參半的工具,關于它的批判,大多集中于:xml中實現的邏輯難以閱讀、維護,這實際上是對DataBinding設計的誤解而帶來的錯誤使用。

DataBinding本身具有生成VM層的功能,但這一功能并不足夠強大,且沒有完善的使用指導,而在官方Demo中過度宣傳了它,導致大家認為DataBinding就該這樣使用。

僅使用基礎的數據綁定功能、和Resource或者Context有關的功能(例如字符串模板)、組件生命周期綁定等,適度自定義綁定。

何為狀態、何為事件。最后的一公里

首先區別于上文提到的UI事件,這里的狀態和事件均產生于數據流的末段,而UI事件處于數據流的首段。

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

在展開之前,先用一張圖回顧總結上文中對于 單向數據流 & 單一可信數據源 的知識

單向數據流動 章節中,提到了MVI的UDF設計:

  • 系統捕獲的UI事件、其他偵聽事件(例如熄屏、應用生命周期事件),生成Intent,壓入Intent流中
  • ViewModel層中篩選、轉換、處理Intent,實際是使用Model層業務,產生業務結果,即PartialChange
  • PartialChange經過Reducer計算處理得到最新的ViewState,壓入ViewState流
  • View層(廣義的表現層)響應并呈現最新的ViewState

單一可信數據源 章節中,提到View層應當采用 單一可信數據源

在這張圖中,我們僅體現了 狀態 即 ViewState。

關于GUI程序的認知

在展開前,先聊點理念上的內容。請讀者諸君思考下自己對于GUI程序的認知。

作者的理解:

程序狹義上是計算機能識別和執行的一組指令集,編程工作是在程序世界對 客觀實體業務邏輯 進行 建模和邏輯表達。

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

表現出來的內容代表著客觀實體

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

而除了通過 程序語言描述程序世界模擬展現 外,同樣可以通過 自然語言描述 達到目的,這也是產品經理的工作。

當然,產品經理往往需要借助一些工具來提升自己的自然語言表達能力,但無奈的是能用數學公式和邏輯推演表達需求的產品經理太少見了。

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

First-Order logic

在數學、哲學、語言學、計算機科學中,有一個概念 First-Order logic,無論是產品需求還是計算機程序,都可以建立FOL表達

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

FOL表達 Event或者State時:

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

不難理解,變化是瞬時的,連續的變化是可分的。

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

例如:“好友向你發送了一條消息的場景中”,消息抵達就是Event,它背后潛藏著 “消息數的變化”、“最新消息內容的變化” 等。 在常見的設計中:

  • 應用需要彈出一個氣泡通知用戶這一事件
  • 應用需要更新消息數,消息列表內容等,以呈現出最新的State

而為了讓用戶感知到,氣泡呈現時長并不是瞬時的,但在產品交互設計中依舊將其定義為事件。

分離狀態和事件,不是吃飽撐得

看山是山、看水是水

此時此刻,答案已經很明顯。

在通用的產品設計中,狀態和事件有不同的意義,如果程序中不分離出兩者,則必然是自找麻煩,這是公然挑釁 面向對象編程 的行為。如果不明確定義不同的Class,則勢必導致代碼混亂不堪,畢竟這是違背編程原則的事情。

在大多MVVM設計中,狀態和事件未分家,導致bug叢生,這一點便不再展開。

如何區分Event和State

State是一段時間內無變化的條件或者特征,它天然的 契合 了位于表現層的主體內容所對應的 數據模型特征

Event是特定的變化,它在表現層體現,但與State的生命周期不一致,且并無一一對應的關系。

基于經驗主義,我們可以機械地、籠統地認為:頁面主體靜態內容所需要的數據屬于State范疇,氣泡提醒等短暫的物體所需要的數據屬于Event范疇。

從邏輯推演的角度出發,進行 等價邏輯推斷條件限定下的邏輯推斷 ,一定序列的Event可以模型轉換為State。

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

看山不是山,看水不是水

前面提到,State是一段時間內無變化的條件或者特征,所以在程序設計中State具有粘性的特征。

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

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

如果要實現以力破法,在框架設計層面上 Event體系的設計要比State體系要復雜 。因為從交互設計上:

  • State 只需要考慮呈現的準確性和及時性,除去美觀、可理解性等等
  • Event 需要考慮準確性、優先級、及時性、按條件丟棄等等,除去美觀、可理解性等等

舉個例子:網絡連接問題導致的Web-API調用失敗需要使用Toast提示網絡連接失敗

不難想象:

  • 可能一瞬間的斷開網絡連接,會導致多個連接均返回失敗
  • 可能連接問題未修復,10秒前請求失敗,當前請求又失敗了

難道連續彈出嗎?難道和上一次Event一致就不消費嗎?…

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

所以在框架層面需要針對產品、交互設計的泛化理念,設計準確的、靈活的Event體系。

準確的、靈活的Event體系

看山還是山,看水還是水

回到FOL中,為了更加準確的表達Event和State的含義,還需要一些額外的參數,例如:參與者地點時間 等。

想通這一點會發現,產品中定義的Event事件、及其消費邏輯均含有隱藏屬性,例如:

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

產品經理和交互設計師一般會使用 “響應時間”、“優先級” 等詞描述它們,但一般不嚴謹、不成體系,帶來期望不一致的問題

反觀State流,它代表了界面主體內容在時間軸上的完整變化,任意一個時間點均可以得出界面內容所對應的條件和特征。一旦State流中出現一個新的狀態,它均被及時的、準確的在表現層予以體現。

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

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

前文提到Event是瞬時的,所以Event本身并沒有實質意義上的生命周期,為了方便表述,我們將 “Event從生成到在表現層不可觀測的階段” 定義為Event生命周期

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

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

需要額外設計實現 。實現這一點后,從Event流中分流(以及裂變+組合)出的 子流 將和State流 性質一致

此刻,您會發現,根據不同類型的事件交互控件所對應的交互特征,又將Event流結合條件流衍生出各個State流。完整的數據流細節如下:

Android 學習筆錄

Android 性能優化篇:https://qr18.cn/FVlo89
Android 車載篇:https://qr18.cn/F05ZCM
Android 逆向安全學習筆記:https://qr18.cn/CQ5TcL
Android Framework底層原理篇:https://qr18.cn/AQpN4J
Android 音視頻篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(內含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 車載開發崗位面試習題:https://qr18.cn/FTlyCJ
音視頻面試題錦:https://qr18.cn/AcV6Ap

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/37896.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/37896.shtml
英文地址,請注明出處:http://en.pswp.cn/news/37896.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

linux系列基本介紹

雖然我們常說Linux操作系統&#xff0c;這種叫法是不正確的&#xff0c;嚴格意義上講&#xff0c;Linux并不是操作系統&#xff0c;而是屬于操作系統的一個內核&#xff0c;inux內核提供了操作系統的核心功能&#xff0c;如進程管理、內存管理、文件系統等。 Linux有很多不同的…

LeetCode 熱題 100 JavaScript--33. 搜索旋轉排序數組

整數數組 nums 按升序排列&#xff0c;數組中的值 互不相同 。 在傳遞給函數之前&#xff0c;nums 在預先未知的某個下標 k&#xff08;0 < k < nums.length&#xff09;上進行了 旋轉&#xff0c;使數組變為 [nums[k], nums[k1], …, nums[n-1], nums[0], nums[1], …,…

yolov5 轉換為rknn模型在3588上運行

為了把yolov5在rk3588上跑起來&#xff0c;在網上搜羅了一圈,踩了一些坑。由于瑞芯微的文檔有升級&#xff0c;導致和網絡的文章有出入&#xff0c;所以做個記錄。 rknn-toolkit 轉換文檔&#xff1a; 瑞芯微的轉換文檔在 rknn-toolkit/example/pytorch/yolov5/REAME.md 里 …

LangChain入門:構建LLM驅動的應用程序的初學者指南

LangChain & DemoGPT 一、介紹 你有沒有想過如何使用大型語言模型&#xff08;LLM&#xff09;構建強大的應用程序&#xff1f;或者&#xff0c;也許您正在尋找一種簡化的方式來開發這些應用程序&#xff1f;那么你來對地方了&#xff01;本指南將向您介紹LangChain&#x…

【Sklearn】基于邏輯回歸算法的數據分類預測(Excel可直接替換數據)

【Sklearn】基于邏輯回歸算法的數據分類預測(Excel可直接替換數據) 1.模型原理2.模型參數3.文件結構4.Excel數據5.下載地址6.完整代碼7.運行結果1.模型原理 邏輯回歸是一種用于二分類問題的統計學習方法,盡管名字中含有“回歸”,但實際上是一種分類算法。它的基本原理是通…

網絡基礎--ARP協議介紹

1、ARP作用 ARP&#xff08; Address Resolution Protocol&#xff0c;地址解析協議&#xff09;是將 IP 地址解析為以太網 MAC 地址&#xff08;或稱物理地址&#xff09;的協議。在局域網中&#xff0c;當主機或其它網絡設備有數據要發送給另一個主機或設備時&#xff0c;它必…

Java鷹眼軌跡服務 輕騎小程序 運動健康與社交案例

Java地圖專題課 基本API BMapGLLib 地圖找房案例 MongoDB 百度地圖鷹眼軌跡服務 鷹眼軌跡服務概述 鷹眼是一套軌跡管理服務&#xff0c;提供各端SDK和API供開發者便捷接入&#xff0c;追蹤所管理的車輛/人員等運動物體。 基于鷹眼提供的接口和云端服務&#xff0c;開發者可以迅…

前后端分離------后端創建筆記(05)用戶列表查詢接口(下)

本文章轉載于【SpringBootVue】全網最簡單但實用的前后端分離項目實戰筆記 - 前端_大菜007的博客-CSDN博客 僅用于學習和討論&#xff0c;如有侵權請聯系 源碼&#xff1a;https://gitee.com/green_vegetables/x-admin-project.git 素材&#xff1a;https://pan.baidu.com/s/…

Java通過文件流和文件地址下載文件

通過文件流下載文件 如何使用 MultipartFile 進行文件上傳、下載到本地&#xff0c;并返回保存路徑呢&#xff1a; import org.springframework.web.multipart.MultipartFile;import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOExcep…

Redis_緩存2_緩存刪除和淘汰策略

14.5 緩存數據的刪除和替換 14.5.1 過期數據 可以使用ttl查看key的狀態。已過期的數據&#xff0c;redis并未馬上刪除。優先去執行讀寫數據操作&#xff0c;刪除操作延后執行。 14.5.2 刪除策略 redis中每一個value對應一個內存地址&#xff0c;在expires&#xff0c;一個內…

BC117 小樂樂走臺階(附完整代碼)

描述 小樂樂上課需要走n階臺階&#xff0c;因為他腿比較長&#xff0c;所以每次可以選擇走一階或者走兩階&#xff0c;那么他一共有多少種走法&#xff1f; 輸入描述 輸入包含一個整數n (1 ≤ n ≤ 30) 輸出描述 輸出一個整數&#xff0c;即小樂樂可以走的方法數。 思路&a…

分享個試卷去筆跡什么軟件,幾個步驟輕松擦除

試卷擦去筆跡是一項非常關鍵的技能&#xff0c;它可以幫助你更好地管理你的筆記和文件。不管是小伙伴們想重新測試試卷或者是將試卷輸出為電子版&#xff0c;都可以實現的。在這篇文章中&#xff0c;我將分享一些方法和軟件&#xff0c;幫助你更好地進行試卷擦除。有需要的小伙…

個人博客系統測試報告

文章目錄 一、功能測試1.編寫測試用例2.總結測試后發現的BUG 二、UI自動化測試0.搭建測試環境1. 創建公共類2.注冊頁面UI自動化測試用例編寫3.登錄頁面UI自動化測試用例編寫4.用戶博客列表頁面自動化測試5. 修改個信息頁面6. 文章編輯頁面7. 設置密保問題發現bug 8. 所有用戶文…

Stable Diffusion +EbSynth應用實踐和經驗分享

Ebsynth應用 1.安裝ffmpeg 2.安裝pip install transparent-background,下載模型https://www.mediafire.com/file/gjvux7ys4to9b4v/latest.pth/file 放到C:\Users\自己的用戶名.transparent-background\加一個ckpt_base.pth文件 3.秋葉安裝ebsynth插件,重啟webui 填寫項目基本…

Redis 持久化及集群架構

Redis 持久化及集群架構 本篇技術博文將深入探討 Redis 持久化機制的原理、配置和使用方式。我們將介紹兩種常用的持久化方式&#xff1a;RDB 持久化和 AOF 持久化。您將了解到它們的工作原理、優缺點以及如何根據需求選擇合適的持久化方式。 通過深入學習 Redis 持久化及集群…

Rest 優雅的url請求處理風格及注意事項

&#x1f600;前言 本篇博文是關于Rest 風格請求的應用和注意事項&#xff0c;希望能夠幫助到您&#x1f60a; &#x1f3e0;個人主頁&#xff1a;晨犀主頁 &#x1f9d1;個人簡介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以幫助到大家&#xff0c;您…

應急響應-Webshell

文章目錄 一、Webshell概述什么是WebshellWebshell分類基于編程語言基于文件大小/提供的功能多少 Webshell 檢測方法 二、常規處置方法三、技術指南1、初步預判2、 Webshell排查3、Web日志分析&#xff08;查找攻擊路徑及失陷原因&#xff09;4、系統排查4.1 Windows4.2 Linux …

CSS中的position屬性有哪些值,并分別描述它們的作用。

聚沙成塔每天進步一點點 ? 專欄簡介? static? relative? absolute? fixed? sticky? 寫在最后 ? 專欄簡介 前端入門之旅&#xff1a;探索Web開發的奇妙世界 記得點擊上方或者右側鏈接訂閱本專欄哦 幾何帶你啟航前端之旅 歡迎來到前端入門之旅&#xff01;這個專欄是為那…

通達OA SQL注入漏洞【CVE-2023-4166】

通達OA SQL注入漏洞【CVE-2023-4166】 一、產品簡介二、漏洞概述三、影響范圍四、復現環境POC小龍POC檢測工具: 五、修復建議 免責聲明&#xff1a;請勿利用文章內的相關技術從事非法測試&#xff0c;由于傳播、利用此文所提供的信息或者工具而造成的任何直接或者間接的后果及損…

C/C++ 標準模版庫STL(持續更新版)

標準模版庫STL <algorithm> 算法庫 max, min 用于找出一組值中的最大值和最小值 swap 用于交換兩個變量的值 sort 用于對一個范圍內的元素進行排序 lower_bound, upper_bound 用于在已排序的容器中查找元素的下界和上界 unique(a,an)-a 用于在一個范圍內刪除相鄰重…