為何選擇 Flux
設(shè)計上遇到的問題
最初在接觸 Flux 時就有一種驚艷的感覺,長久以來在設(shè)計上所出現(xiàn)的困擾似乎出現(xiàn)了曙光。在 Flux 還沒有出現(xiàn)之前,MVx 系列 (MVC、MVP、MVVM) 的 Design Pattern 就一直引領(lǐng)風(fēng)潮。這類型的 Design Pattern 成功地解決了特定的問題,但卻也形成了某些尾大不掉的隱憂。在畫面不多、顯示信息單純的應(yīng)用程序中問題不容易顯現(xiàn),但隨著程序復(fù)雜度的升高,設(shè)計上所隱含的矛盾也不住地增強。
MVx 系列的設(shè)計在概念上是一個畫面對應(yīng)一種數(shù)據(jù)類型,畫面專責(zé)顯示與處理該類型的數(shù)據(jù)。很直覺、也很有效地把功能區(qū)分成一組、一組的單元。水能載舟亦能覆舟,正所謂成也蕭何、敗也蕭何。就是因為每一組 MVx 太過獨立、區(qū)隔性太強,當(dāng)出現(xiàn)整合式畫面的需求時,會造成在設(shè)計上進(jìn)退兩難的抉擇。
假設(shè)程序中有一個畫面叫 Dashboard,需要整合客戶、訂單、存貨的數(shù)據(jù)。試問,這時是要設(shè)計一個新的 Model 納入所有數(shù)據(jù)?還是打破規(guī)則讓一個 View 對應(yīng)多個 Model?
有人也許會問:這是問題嗎?
如果只是期望程序能夠運行,那的確算不上是個問題。但是如果要考慮到源代碼的可維護性,就必須要維持在設(shè)計上的一致性,這點在程序愈復(fù)雜的情況下愈顯重要。否則就不需要搞什么 Design Pattern,就隨性而為、讓一切都?xì)w于渾沌就好了。
再舉另一個例子,假設(shè)要開發(fā)的是線上購物的訂單畫面,下單時要提供客戶數(shù)據(jù)、訂單數(shù)據(jù)、刷卡數(shù)據(jù)。依據(jù)之前的原則,所有的數(shù)據(jù)都會被設(shè)計納在一個單一的 Model 內(nèi)。當(dāng)某一天高層突然下指令要把購物流程改成 Wizard 的方式,每個步驟各自獨立成一個畫面。試問在這樣的情況下,開發(fā)新畫面時是讓 Model 拆解成多個?還是維持原本的樣子?
如果要維持原本的樣子,由于每一組的 MVx 都是獨立的,如何傳遞 Model?誰要負(fù)責(zé)控制傳遞的順序?又該如何保留 Model 的狀態(tài)?好吧!那就拆開...
拆開之后,問題似乎解決了,但此時高層又說了,這個程序要跨平臺,所以二種類型的畫面都要有...
Flux 所提供的效果
Flux 的架構(gòu)則是打破這層膠著的狀態(tài),在其單向數(shù)據(jù)流的原則之下,View 只要管顯示數(shù)據(jù),不管數(shù)據(jù)的來源是一個還是多個。而被通知數(shù)據(jù)有異動時,也是依循相同的方式來獲取數(shù)據(jù),刷新畫面。至于要如何異動數(shù)據(jù)與 View 無關(guān),只要把異動的信息傳出去,接著就像戰(zhàn)機上的飛彈一樣可以射后不理。
在這樣的設(shè)計之下,以之前 Dashboard 的例子,不管是單一的畫面負(fù)責(zé)顯示所有的數(shù)據(jù),還是畫面上分割成許多不同的組件來分別顯示特定的數(shù)據(jù),都不會有設(shè)計上的違和感。而另一個例子同樣也適用,無關(guān)后端的數(shù)據(jù)規(guī)劃方式,View 只要專注在選墿合適的數(shù)據(jù)來源、考量如何顯示數(shù)據(jù)上即可。
Flux 只能用在有 UI 的情境之下?不盡然,并不是只有人才會輸入或需要取得回應(yīng)。在有明確的邊界之狀況下,像是網(wǎng)絡(luò)或是因設(shè)計的考量所形成邏輯上的 Layer,這種可以用來把數(shù)據(jù)供給端及接收端做有效的分離,以便進(jìn)行分工、測試等等作業(yè)的架構(gòu),都可以考慮套用 Flux 的概念。
如何實現(xiàn)
俗話說得好,知易行難。了解 Flux 的運作過程是一回事,但要把這些過程落實到設(shè)計之中、形成源代碼又是另外一回事。Facebook 并沒有為 Java 的開發(fā)環(huán)境開發(fā)一套符合 Flux 的庫,而 Java 的環(huán)境相較于 JavaScript 又更加地多元化,加大了使用上的不確定性。為了避免在開發(fā)上每次都要反覆進(jìn)行類似的工作,于是就依據(jù)過去的工作經(jīng)驗,利用抽象化的手法及自動生成的概念,實現(xiàn)了一個 Framework,讓想要在 Java 的項目中使用 Flux 的人可以輕易的上手。
接下來會針對這個 Framework 做個簡單的說明。
取得 Binary
最新版本的 Jar 檔可以在 Github 的 Release 頁面中下載。
配置
如果是使用 Gradle 來建構(gòu)程序,則所下載到的檔案可以送到 build.gradle 配置引用的文件夾下。如果是 Android 的項目,則是放到 libs 的文件夾下即可。
在項目中有使用 fluxjava-rx 時,應(yīng)該也會需要在 build.gradle 中增加以下的內(nèi)容:
dependencies {
...
compile "io.reactivex:rxjava:1.2.+”
}
在使用之前
在 Github 的 Repository 中,F(xiàn)luxJava 庫源代碼放在 fluxjava 的文件夾下,并且在 demo-eventbus 文件夾下搭配一個示范用的 Android 項目。這個示范的項目是一個只有單一 Activity 的簡易 Todo 應(yīng)用程序。在這個 App 中可以展示以下的功能:
- 顯示 Todo 清單
- 在不同使用者間切換 Todo 清單
- 新增 Todo
- 關(guān)閉/重啟 Todo

在這個示范項目中使用 greenrobot 的 EventBus 來協(xié)助 Dispatcher 和 Store 發(fā)送信息。
如果想要與 RxJava 搭配使用,可以看一下 fluxjava-rx 文件夾,里面有 FluxJava 為 RxJava 所開發(fā)的 Addon。同時,有一個與之配對的示范項目在 demo-rx 文件夾下,是由 demo-eventbus 復(fù)制過來修改的。在這個示范項目中,原本的 EventBus 以 fluxjava-rx 所提供的 RxBus 取代。而基于 RxJava 1.x 庫的 RxBus 所提供的功能和 EventBus 的功能相同。
如何使用
準(zhǔn)備工作
Bus
Dispatcher 和 Store 會呼叫 Bus 來傳送信息。Bus 必須要實現(xiàn) IFluxBus 的介面,實現(xiàn)時可以使用任何的 Bus 方案,像是:Otto、EventBus,或是自行開發(fā)的方案。如果有同時引用fluxjava-rx,則可以直接使用 RxBus 來提供傳送信息的功能。Action
Dispatcher 使用 Action 來通知 Store 要進(jìn)行的工作。在 Action 中有二個屬性,一個是 Type、一個是 Data。Type 用來讓 Store 識別要對數(shù)據(jù)進(jìn)行的動作,Data 則是該動作的附屬信息。以示范的項目來說,當(dāng)一個新的 Todo 從介面上被傳進(jìn)來,則新 Todo 的內(nèi)容會被放在 Data 欄位中。ActionHelper
ActionHelper 協(xié)助 ActionCreator 決定產(chǎn)生何種 Action,并且協(xié)助 ActionCreator 將目前傳進(jìn)來的數(shù)據(jù)格式轉(zhuǎn)成可被處理的格式。Store
Store 負(fù)責(zé)截收由 Dispatcher 所送出的 Action,并根據(jù) Action 上的信息進(jìn)行對應(yīng)的數(shù)據(jù)處理。當(dāng)數(shù)據(jù)處理完成,Store 會再送出一個數(shù)據(jù)異動的事件,讓事件的接收者可用以反應(yīng)新的數(shù)據(jù)狀態(tài)。StoreMap
StoreMap 是一個一對一的對照表,在 Framework 中使用這一個對照表來產(chǎn)生需要的 Store Instance。假設(shè) Action 和 Store 的關(guān)系是一對一的,則 Action 的型別可以用來做為 Store 型別的鍵值,或是很單純地使用一個數(shù)值來做為鍵值。像是在示范的項目中可以看到有二個常數(shù)在Constants.java中,分別是 DATA_USER 及 DATA_TODO,這二個常數(shù)各自會對應(yīng)到一個 Store 的型別。因此,與 TodoAction 配對的 TodoStore 就會被產(chǎn)生來負(fù)責(zé)處理與 Todo 相關(guān)的數(shù)據(jù)要求,而 User 也是套用一樣的邏輯。
初始化程序
在 FluxJava 中,F(xiàn)luxContext 是用來做為整個程序開始的進(jìn)入點。FluxContext 被設(shè)計成 Singleton,負(fù)責(zé)整合 Framework 中相關(guān)的組件,并且管理特定組件的 Instance。
FluxContext 的 Instance 可以由其內(nèi)含的 Builder 來建立,示范的源代碼如下:
FluxContext.getBuilder()
.setBus(new Bus())
.setActionHelper(new ActionHelper())
.setStoreMap(storeMap)
.build();
開始發(fā)送要求
在取得使用者透過 UI 組件所輸入的數(shù)據(jù)后,接下來可以利用 ActionCreator 來推送 Action,ActionCreator 的 Instance 可經(jīng)由 FluxContext 來取得。Framework 預(yù)設(shè)所提供的 ActionCreator 只有一項功能 sendRequest,呼叫的源代碼要傳入 Id 及使用者輸入的數(shù)據(jù)。其中,Id 是用來決定要產(chǎn)生的 Action 型別。使用者輸入的數(shù)據(jù)可以在呼叫 sendRequest 后,經(jīng)由 ActionHelper 轉(zhuǎn)成 Store 所需的格式。
以下為示范的源代碼:
Todo todo = new Todo();
FluxContext.getInstance()
.getActionCreator()
.sendRequestAsync(TODO_ADD, todo);
sendRequest 有提供二種版本的實現(xiàn),同步和非同步。非同步的版本會先建立一個新的 Thread 之后,在新的 Thread 中執(zhí)行。如果需要特別管控 Thread 的使用或是想要使用 Thread Pool,則可以呼叫同步的版本來達(dá)到目的。
進(jìn)行數(shù)據(jù)處理
要進(jìn)行數(shù)據(jù)處理需在 Store 中攔截指定的 Action,攔截的方法會依據(jù)所使用的 Bus 方案而不同。以示范項目的例子來說,要在 Store 中新增一個搭配特定 Annotation 的方法。相關(guān)的程序范例如下:
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onAction(final TodoAction inAction) {
switch (inAction.getType()) {
case TODO_LOAD:
...
super.emitChange(new ListChangeEvent());
break;
case TODO_ADD:
...
super.emitChange(new ListChangeEvent());
break;
case TODO_CLOSE:
...
super.emitChange(new ItemChangeEvent(i));
break;
}
}
如果是使用 fluxjava-rx,則 Store 可以繼承自 RxStore,此時只要改寫 RxStore 中的 onAction 方法即可。相關(guān)的程序范例如下:
@Override
protected <TAction extends IFluxAction> void onAction(final TAction inAction) {
final TodoAction action = (TodoAction)inAction;
switch (action.getType()) {
case TODO_LOAD:
...
super.emitChange(new ListChangeEvent());
break;
case TODO_ADD:
...
super.emitChange(new ListChangeEvent());
break;
case TODO_CLOSE:
...
super.emitChange(new ItemChangeEvent(i));
break;
}
}
反應(yīng)數(shù)據(jù)異動
跟 Store 一樣,UI 組件要依據(jù)使用的 Bus 方案來接收由 Store 所發(fā)出的數(shù)據(jù)異動事件。在 EventBus 的例子中:
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(final TodoStore.ListChangeEvent inEvent) {
super.notifyDataSetChanged();
}
在 RxBus 的例子中:
todoStore.toObservable(TodoStore.ListChangeEvent.class)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
new Action1<TodoStore.ListChangeEvent>() {
@Override
public void call(final TodoStore.ListChangeEvent inEvent) {
TodoAdapter.super.notifyDataSetChanged();
}
});