實踐移動端的Flux架構

歡迎Follow我的GitHub, 關注我的簡書. 其余參考Android目錄.

架構

本文的合集已經編著成書,高級Android開發強化實戰,歡迎各位讀友的建議和指導。在京東即可購買:https://item.jd.com/12385680.html

Android

任何架構最終目的都是讓程序更加有序, 功能便于擴展, Bug容易追蹤.

Facebook使用Flux架構來構建客戶端的Web應用. Flux架構并不是為移動端設計的, 但是我們仍然可以采用這個思想在Android端使用. Flux是數據驅動型架構, 在以數據為核心的場景中使用非常合適, 不過Facebook好像把Flux架構應用于所有產品, 無論是前端還是移動端. 最新Facebook開發的ReactNative中, 就是使用Flux架構為核心, 也是開源的, 可以閱讀RN的代碼了解所有內容.

Flux架構, 顧名思義表示, 是以數據流為基礎.

本文源碼的Github下載地址


基本架構模型如圖:

架構

模型主要分為四個模塊:
(1) View: 視圖, 根據用戶交互(Use Interaction)的內容創建響應事件, 調用活動創建器(ActionCreator)的方法, 發送響應用戶操作的事件.
(2) ActionCreator: 活動創建器, 把活動的類型和數據發送給調度器(Dispatcher), 根據類型和數據創建活動(Action). 也可以進行網絡操作(WebApi), 完成后, 再發送數據.
(3) Dispatcher: 調度器, 把接收到的活動, 通過總線(EventBus), 把活動發送出去, 由存儲器(Store)根據活動的類型和數據, 修改數據模型.
(4) Store: 存儲器, 維護特定的數據狀態, 接收總線分發的活動, 根據活動類型執行不同的業務邏輯. 在完成時, 發出數據修改(ChangeEvent)的事件, 視圖監聽這一事件, 更新顯示內容.

Talk is cheap, show you the code.
只有通過代碼才能真正的了解架構的意義, 我寫了一個基于Flux架構的ToDoList.
頁面發送各種增刪改查的事件, 通過Flux模型, 修改本地存儲的數據, 再反饋回頁面.

TodoList

1. 視圖(View)

視圖(View): 視圖是一個Activity, 負責響應用戶交互(Use Interaction)的事件, 發送到給活動創建器(ActionCreator); 同時接收修改數據(ChangeEvent)事件, 更新頁面.

public class MainActivity extends AppCompatActivity {

    // ...

    // 添加數據
    @OnClick(R.id.main_add) void addItem() {
        addTodo(); // 添加TodoItem
        resetMainInput(); // 重置輸入框
    }

    // 選中數據
    @OnClick(R.id.main_checkbox) void checkItem() {
        checkAll(); // 所有Item項改變選中狀態
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); // 設置Layout
        ButterKnife.bind(this); // 綁定ButterKnife

        initDependencies(); // 創建Flux的核心管理類

        // 設置RecyclerView
        mMainList.setLayoutManager(new LinearLayoutManager(this));
        mListAdapter = new RecyclerAdapter(sActionsCreator);
        mMainList.setAdapter(mListAdapter);
    }

    // 初始化: Dispatcher調度器, Action事件, Store狀態
    private void initDependencies() {
        sDispatcher = Dispatcher.getInstance(new Bus());
        sActionsCreator = ActionsCreator.getInstance(sDispatcher);
        sTodoStore = TodoStore.getInstance(sDispatcher);
    }

    @Override
    protected void onResume() {
        super.onResume();

        // 把訂閱接口注冊到EventBus
        sDispatcher.register(this);
        sDispatcher.register(sTodoStore);
    }

    @Override
    protected void onPause() {
        super.onPause();

        // 解除訂閱接口
        sDispatcher.unregister(this);
        sDispatcher.unregister(sTodoStore);
    }

    // 改變改變所有狀態(ActionsCreator)
    private void checkAll() {
        sActionsCreator.toggleCompleteAll();
    }

    // 清理選中的項(ActionsCreator)
    private void clearCompleted() {
        sActionsCreator.destroyCompleted();
    }

    // ...

    // 接收事件的改變
    @Subscribe
    public void onTodoStoreChange(TodoStore.TodoStoreChangeEvent event) {
        updateUI();
    }

    // 更新UI, 核心方法
    private void updateUI() {
        // 設置適配器數據, 每次更新TodoStore的狀態
        mListAdapter.setItems(sTodoStore.getTodos());
        ...
    }
}

代碼比較長, 只截取了一部分. 其中View的核心部分:
(1) 活動創建器(ActionsCreator), 負責響應用戶交互事件, 如checkAll()等.
(2) 接收狀態修改(onTodoStoreChange), 負責接收存儲器(Store)修改完成的數據, 并更新頁面(updateUI).


2. 活動創建器(ActionsCreator)

活動創建器(ActionsCreator): 主要負責根據響應事件類型數據, 發送相應的活動到調度器(Dispatch). 也可以發送網絡請求(WebApi), 獲取異步數據, 完成后再發送.

public class ActionsCreator {
    private static ActionsCreator sInstance;
    private final Dispatcher mDispatcher;

    private ActionsCreator(Dispatcher dispatcher) {
        mDispatcher = dispatcher;
    }

    public static ActionsCreator getInstance(Dispatcher dispatcher) {
        if (sInstance == null) {
            sInstance = new ActionsCreator(dispatcher);
        }
        return sInstance;
    }

    public void create(String text) {
        mDispatcher.dispatch(TodoActions.TODO_CREATE, TodoActions.KEY_TEXT, text);
    }

    public void destroy(long id) {
        mDispatcher.dispatch(TodoActions.TODO_DESTROY, TodoActions.KEY_ID, id);
    }

    // ...
}

活動在調度器(Dispatcher)中創建.
活動創造器(ActionsCreator), 第一個參數是類型, 其余參數是數據, 即配對的Key-Value.


3. 調度器(Dispatcher)

調度器(Dispatcher), 是事件分發的中心, 使用事件總線(EventBus), 發送活動到存儲器(Store). 存儲器根據活動的類型和數據, 進行處理.

public class Dispatcher {

    private final Bus mBus;
    private static Dispatcher sInstance;

    private Dispatcher(Bus bus) {
        mBus = bus;
    }

    public static Dispatcher getInstance(Bus bus) {
        if (sInstance == null) {
            sInstance = new Dispatcher(bus);
        }
        return sInstance;
    }

    public void register(final Object cls) {
        mBus.register(cls);
    }

    public void unregister(final Object cls) {
        mBus.unregister(cls);
    }

    private void post(final Object event) {
        mBus.post(event);
    }

    // 每個狀態改變都需要發送事件, 由View相應, 做出更改
    public void emitChange(Store.StoreChangeEvent o) {
        post(o);
    }

    /**
     * 調度核心函數
     *
     * @param type 調度類型
     * @param data 數據(Key, Value)
     */
    public void dispatch(String type, Object... data) {
        if (type == null || type.isEmpty()) { // 數據空
            throw new IllegalArgumentException("Type must not be empty");
        }
        if (data.length % 2 != 0) { // 非Key-Value
            throw new IllegalArgumentException("Data must be a valid list of key,value pairs");
        }

        Action.Builder actionBuilder = Action.type(type);

        int i = 0;
        while (i < data.length) {
            String key = (String) data[i++];
            Object value = data[i++];
            actionBuilder.bundle(key, value); // 放置鍵值
        }

        // 發送到EventBus
        post(actionBuilder.build());
    }

}

調度器使用事件總線(EventBus)分發數據, 有兩個核心部分:
(1) dispatch(): 把類型和數據組成活動(Action), 發送至事件總線(EventBus), 由存儲器(Store)負責處理.
(2) emitChange(): 把存儲器(Store)處理完的狀態和數據, 發送修改通知至事件總線(EventBus), 提示視圖(View)進行更新頁面(UpdateUI).


4. 存儲器(Store)

存儲器(Store): 負責存儲數據和狀態, 接收事件總線(EventBus)上的修改通知, 根據類型, 修改數據和狀態. 也可以使用數據庫和本地存儲.

public class TodoStore extends Store {
    private static TodoStore sInstance; // 單例
    private final List<Todo> mTodos; // 數據
    private Todo lastDeleted; // 狀態: 最近一次刪除數據

    private TodoStore(Dispatcher dispatcher) {
        super(dispatcher);
        mTodos = new ArrayList<>();
    }

    public static TodoStore getInstance(Dispatcher dispatcher) {
        if (sInstance == null) {
            sInstance = new TodoStore(dispatcher);
        }
        return sInstance;
    }

    @Override
    @Subscribe
    public void onAction(Action action) {
        long id;
        switch (action.getType()) {
            case TodoActions.TODO_CREATE:
                String text = ((String) action.getData().get(TodoActions.KEY_TEXT));
                create(text);
                emitStoreChange(); // 發生改變事件
                break;

            case TodoActions.TODO_DESTROY:
                id = ((long) action.getData().get(TodoActions.KEY_ID));
                destroy(id);
                emitStoreChange();
                break;

            case TodoActions.TODO_UNDO_DESTROY:
                undoDestroy();
                emitStoreChange();
                break;
            // ...
        }
    }

    private void destroyCompleted() {
        Iterator<Todo> iter = mTodos.iterator();
        while (iter.hasNext()) {
            Todo todo = iter.next();
            if (todo.isComplete()) {
                iter.remove();
            }
        }
    }

    // ...

    @Override
    public StoreChangeEvent changeEvent() {
        return new TodoStoreChangeEvent();
    }
}

存儲器(Store)的核心是onAction()方法, 獲得事件總線(EventBus)的通知, 修改數據, 完成后調用emitStoreChange()方法, 通知視圖(View)進行數據更新(UpdateUI).


整套的循環邏輯都已經完成, 清晰可見, 這就是架構的好處吧.

Flux: View -> Action -> Dispatcher -> Store -> View.

想更多的了解Flux架構, Facebook參考.

OK, that's all! Enjoy it!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容