2億用戶背后的Flutter應(yīng)用框架Fish Redux
背景
在使用 Flutter 開發(fā)過程中,我們遇到了業(yè)務(wù)代碼耦合嚴重,代碼可維護性糟糕,如入泥濘。對于閑魚這樣的負責(zé)業(yè)務(wù)場景,我們需要一個統(tǒng)一的應(yīng)用框架來擺脫當(dāng)下的開發(fā)困境,而這也是 Flutter 領(lǐng)域空缺的一塊。
Fish Redux 是為解決上面問題上層應(yīng)用框架,它是一個基于 Redux 數(shù)據(jù)管理的組裝式 flutter 應(yīng)用框架, 特別適用于構(gòu)建中大型的復(fù)雜應(yīng)用。
它的一大特點是配置式組裝, 一方面將一個大的頁面,對視圖和數(shù)據(jù)層層拆解為互相獨立的 Component|Adapter,上層負責(zé)組裝,下層負責(zé)實現(xiàn),另一方面將 Component|Adapter 拆分為 View,Reducer,Effect 等相互獨立的上下文無關(guān)函數(shù)。所以它會非常干凈,易編寫、易維護、易協(xié)作。
Fish Redux 的靈感主要來自于 Redux、React、Elm、Dva 這樣的優(yōu)秀框架,而 Fish Redux 站在巨人的肩膀上,將集中,分治,復(fù)用,隔離做的更進一步。
分層架構(gòu)圖
架構(gòu)圖,主體自底而上,分三層,每一層用來解決不通層面的問題和矛盾,下面依次來展開。
Redux
- Redux 是來自前端社區(qū)的一個數(shù)據(jù)管理框架, 對 Native 開發(fā)同學(xué)來說可能會有一點陌生,我們做一個簡單的介紹。
Redux 做什么的?
- Redux 是一個用來做可預(yù)測易調(diào)試的數(shù)據(jù)管理的框架。所有對數(shù)據(jù)的增刪改查等操作都由 Redux 來集中負責(zé)。
Redux 是怎么設(shè)計和實現(xiàn)的?
- Redux 是一個函數(shù)式的數(shù)據(jù)管理的框架。
傳統(tǒng) OOP 做數(shù)據(jù)管理,往往是定義一些 Bean,每一個 Bean 對外暴露一些 Public-API 用來操作內(nèi)部數(shù)據(jù)(充血模型)。 函數(shù)式的做法是更上一個抽象的緯度,對數(shù)據(jù)的定義是一些 Struct(貧血模型),而操作數(shù)據(jù)的方法都統(tǒng)一到具有相同函數(shù)簽名 (T, Action) => T 的 Reducer 中。 FP:Struct(貧血模型) + Reducer = OOP:Bean(充血模型) 同時 Redux 加上了 FP 中常用的 Middleware(AOP) 模式和 Subscribe 機制,給框架帶了極高的靈活性和擴展性。 貧血模型、充血模型 參考: [https://en.wikipedia.org/wiki/Plain_old_Java_object](https://en.wikipedia.org/wiki/Plain_old_Java_object)
Redux 的缺點
- Redux 核心僅僅關(guān)心數(shù)據(jù)管理,不關(guān)心具體什么場景來使用它,這是它的優(yōu)點同時也是它的缺點。
- 在我們實際使用 Redux 中面臨兩個具體問題
- Redux 的集中和 Component 的分治之間的矛盾。
- Redux 的 Reducer 需要一層層手動組裝,帶來的繁瑣性和易錯性。
Fish Redux 的改良
Fish Redux 通過 Redux 做集中化的可觀察的數(shù)據(jù)管理。然不僅于此,對于傳統(tǒng) Redux 在使用層面上的缺點,在面向端側(cè) flutter 頁面緯度開發(fā)的場景中,我們通過更好更高的抽象,做了改良。
一個組件需要定義一個數(shù)據(jù)(Struct)和一個 Reducer。同時組件之間存在著父依賴子的關(guān)系。通過這層依賴關(guān)系,
我們解決了【集中】和【分治】之間的矛盾,同時對 Reducer 的手動層層 Combine 變成由框架自動完成,大大簡化了使用 Redux 的困難。
我們得到了理想的集中的效果和分治的代碼。
對社區(qū)標(biāo)準(zhǔn)的 follow
- State、Action、Reducer、Store、Middleware 以上概念和社區(qū)的 ReduxJS 是完全一致的。我們將原汁原味地保留所有的 Redux 的優(yōu)勢。
- 如果想對 Redux 有更近一步的理解,請參考 https://github.com/reduxjs/redux
Component
組件是對局部的展示和功能的封裝。 基于 Redux 的原則,我們對功能細分為修改數(shù)據(jù)的功能(Reducer)和非修改數(shù)據(jù)的功能(副作用 Effect)。
于是我們得到了,View、 Effect、Reducer 三部分,稱之為組件的三要素,分別負責(zé)了組件的展示、非修改數(shù)據(jù)的行為、修改數(shù)據(jù)的行為。
這是一種面向當(dāng)下,也面向未來的拆分。在面向當(dāng)下的 Redux 看來,是數(shù)據(jù)管理和其他。在面向未來的 UI-Automation 看來是 UI 表達和其他。
UI 的表達對程序員而言即將進入黑盒時代,研發(fā)工程師們會把更多的精力放在非修改數(shù)據(jù)的行為、修改數(shù)據(jù)的行為上。
組件是對視圖的分治,也是對數(shù)據(jù)的分治。通過逐層分治,我們將復(fù)雜的頁面和數(shù)據(jù)切分為相互獨立的小模塊。這將利于團隊內(nèi)的協(xié)作開發(fā)。
關(guān)于 View
View 僅僅是一個函數(shù)簽名: (T,Dispatch,ViewService) => Widget
它主要包含三方面的信息
- 視圖是完全由數(shù)據(jù)驅(qū)動。
- 視圖產(chǎn)生的事件/回調(diào),通過 Dispatch 發(fā)出“意圖”,不做具體的實現(xiàn)。
- 需要用到的組件依賴等,通過 ViewService 標(biāo)準(zhǔn)化調(diào)用。
- 比如一個典型的符合 View 簽名的函數(shù)
關(guān)于 Effect
Effect 是對非修改數(shù)據(jù)行為的標(biāo)準(zhǔn)定義,它是一個函數(shù)簽名: (Context, Action) => Object
它主要包含四方面的信息
- 接收來自 View 的“意圖”,也包括對應(yīng)的生命周期的回調(diào),然后做出具體的執(zhí)行。
- 它的處理可能是一個異步函數(shù),數(shù)據(jù)可能在過程中被修改,所以我們不崇尚持有數(shù)據(jù),而通過上下文來獲取新數(shù)據(jù)。
- 它不修改數(shù)據(jù), 如果修要,應(yīng)該發(fā)一個 Action 到 Reducer 里去處理。
- 它的返回值僅限于 bool or Future, 對應(yīng)支持同步函數(shù)和協(xié)程的處理流程。
比如:良好的協(xié)程的支持
關(guān)于 Reducer
Reducer 是一個完全符合 Redux 規(guī)范的函數(shù)簽名:(T,Action) => T
一些符合簽名的 Reducer
同時我們以顯式配置的方式來完成大組件所依賴的小組件、適配器的注冊,這份依賴配置稱之為 Dependencies。
所以有這樣的公式 Component = View + Effect(可選) + Reducer(可選) + Dependencies(可選)。
一個典型的組裝
通過 Component 的抽象,我們得到了完整的分治,多緯度的復(fù)用,更好的解耦。
Adapter
Adapter 也是對局部的展示和功能的封裝。它為 ListView 高性能場景而生,它是 Component 實現(xiàn)上的一種變化。
- 它的目標(biāo)是解決 Component 模型在 flutter-ListView 的場景下的 3 個問題
- 1)將一個"Big-Cell"放在 Component 里,無法享受 ListView 代碼的性能優(yōu)化。
- 2)Component 無法區(qū)分 appear|disappear 和 init|dispose 。
- 3)Effect 的生命周期和 View 的耦合,在 ListView 的場景下不符合直觀的預(yù)期。
概括的講,我們想要一個邏輯上的 ScrollView,性能上的 ListView ,這樣的一種局部展示和功能封裝的抽象。 做出這樣獨立一層的抽象是, 我們看實際的效果, 我們對頁面不使用框架,使用框架 Component,使用框架 Component+Adapter 的性能基線對比
- Reducer is long-lived, Effect is medium-lived, View is short-lived.
- 我們通過不斷的測試做對比,以某 android 機為例:
- 使用框架前 我們的詳情頁面的 FPS,基線在 52FPS。
- 使用框架, 僅使用 Component 抽象下,F(xiàn)PS 下降到 40, 遭遇“Big-Cell”的陷阱。
- 使用框架,同時使用 Adapter 抽象后,F(xiàn)PS 提升到 53,回到基線以上,有小幅度的提升。
Directory
推薦的目錄結(jié)構(gòu)會是這樣
sample_page -- action.dart -- page.dart -- view.dart -- effect.dart -- reducer.dart -- state.dart components sample_component -- action.dart -- component.dart -- view.dart -- effect.dart -- reducer.dart -- state.dart
上層負責(zé)組裝,下層負責(zé)實現(xiàn), 同時會有一個插件提供, 便于我們快速填寫。
以閑魚的詳情場景為例的組裝:
組件和組件之間,組件和容器之間都完全的獨立。
Communication Mechanism
- 組件|適配器內(nèi)通信
- 組件|適配器間內(nèi)通信
 簡單的描述:采用的是帶有一段優(yōu)先處理的廣播, self-first-broadcast。 發(fā)出的 Action,自己優(yōu)先處理,否則廣播給其他組件和 Redux 處理。 最終我們通過一個簡單而直觀的 dispatch 完成了組件內(nèi),組件間(父到子,子到父,兄弟間等)的所有的通信訴求。
Refresh Mechanism
數(shù)據(jù)刷新
- 局部數(shù)據(jù)修改,自動層層觸發(fā)上層數(shù)據(jù)的淺拷貝,對上層業(yè)務(wù)代碼是透明的。
- 層層的數(shù)據(jù)的拷貝
- 一方面是對 Redux 數(shù)據(jù)修改的嚴格的 follow。
- 另一方面也是對數(shù)據(jù)驅(qū)動展示的嚴格的 follow。

視圖刷新
- 扁平化通知到所有組件,組件通過 shouldUpdate 確定自己是否需要刷新

優(yōu)點
數(shù)據(jù)的集中管理
- 通過 Redux 做集中化的可觀察的數(shù)據(jù)管理。我們將原汁原味地保留所有的 Redux 的優(yōu)勢,同時在 Reducer 的合并上,變成由框架代理自動完成,大大簡化了使用 Redux 的繁瑣度。
組件的分治管理
- 組件既是對視圖的分治,也是對數(shù)據(jù)的分治。通過逐層分治,我們將復(fù)雜的頁面和數(shù)據(jù)切分為相互獨立的小模塊。這將利于團隊內(nèi)的協(xié)作開發(fā)。
View、Reducer、Effect 隔離
- 將組件拆分成三個無狀態(tài)的互不依賴的函數(shù)。因為是無狀態(tài)的函數(shù),它更易于編寫、調(diào)試、測試、維護。同時它帶來了更多的組合、復(fù)用和創(chuàng)新的可能。
聲明式配置組裝
- 組件、適配器通過自由的聲明式配置組裝來完成。包括它的 View、Reducer、Effect 以及它所依賴的子項。
良好的擴展性
- 核心框架保持自己的核心的三層關(guān)注點,不做核心關(guān)注點以外的事情,同時對上層保持了靈活的擴展性。
- 框架甚至沒有任何的一行的打印的代碼,但我們可通過標(biāo)準(zhǔn)的 Middleware 來觀察到數(shù)據(jù)的流動,組件的變化。
- 在框架的核心三層外,也可以通過 dart 的語言特性 為 Component 或者 Adapter 添加 mixin,來靈活的組合式地增強他們的上層使用上的定制和能力。
- 框架和其他中間件的打通,諸如自動曝光、高可用等,各中間件和框架之間都是透明的,由上層自由組裝。
精小、簡單、完備
- 它非常小,僅僅包含 1000 多行代碼。
- 它使用簡單,完成幾個小的函數(shù),完成組裝,即可運行。
- 它是完備的。
Fish Redux 目前已在阿里巴巴閑魚技術(shù)團隊內(nèi)多場景,深入應(yīng)用。