Flutter狀態管理學習手冊(二)——Redux

Windin發表於2019-04-04

上一篇講到了一個簡單的狀態管理架構—— ScopedModel , 當然,這種簡單的架構會用在商業專案中的概率比較小,本篇則講述另一個架構: Redux ,一個優雅且實用的狀態管理框架。本篇 Demo 地址:github.com/windinwork/…

一、Redux 的準備工作

Redux 的概念源於 React,對於不是從事前端工作或者沒有接觸過 React 的人要理解 Redux 會比較繁複。對於不瞭解 Redux 的小夥伴,這裡有兩篇很不錯的文章介紹了 Redux 的概念和相關知識:

Redux 入門教程(一):基本用法

Redux 入門教程(二):中介軟體與非同步操作

二、Redux的概念

學習後 Redux 可以瞭解到,Redux 主要由涉及下面幾種概念:

  • Store,是儲存資料的地方。整個應用只能有一個 Store 。Store 有十分重要的方法 dispatch(action) 來傳送 Action。
  • State,是某個時間點的資料快照, 一個 State 對應一個 View。只要 State 相同,View 就相同。
  • Action,是 View 發出的通知,通過 Reducer 使 State 發生變化。
  • Reducer,是一個純函式,接受 Action 和當前 State 作為引數,返回一個新的 State。
  • Middleware,中介軟體,它的操作在發出 Action 和執行 Reducer 這兩步之間發生,用於增加額外功能,如處理非同步操作或者列印日誌功能等。

Redux 的工作流程如圖所示,先使用者發出 Action ,Store自動給 Middleware 進行處理,再傳遞給 Reducer , Reducer 會返回新的 State ,通過 Store 觸發重新渲染 View。這裡的 View 在 Flutter 中以 Widget 的形式存在。

Flutter狀態管理學習手冊(二)——Redux

以上這部分是 Redux 的內容,比較容易理解。

三、Flutter中Redux的使用

在 Flutter 中,我們除了引入 redux 第三方庫之外,還要引入 flutter_redux 第三方庫 ,並且,為了對非同步操作有更好的支援,還要引入 redux_thunk 庫作為 Middleware ,對 thunk action 有更好的支援。

下面來了解 flutter_redux 中的概念。

  1. StoreProvider ,是一個基礎 Widget ,一般在應用的入口處作為父佈局使得,用於傳遞 Store 。

  2. StoreConnector ,一個可以獲取 Store 的 Widget ,作用是響應 Store 發出的狀態改變事件來重建 UI。

  • StoreConnector 中有兩個標記為@required 的引數,一個是 converter , converter 用於將 store 中的 state 轉化為 viewModel,另一個是 builder,builder 的作用是將 viewModel 進一步轉化為 UI 佈局。

  • StoreConnector 有一個值得一提的函式:onInit,在這個函式中可以執行初始化操作。

  1. 對於非同步操作,我們引入了 redux_thunk 庫,redux_thunk 庫是一箇中介軟體,它會處理 thunkAction。thunkAction 是隻包含一個 Store 型別引數的函式。

通過 StoreProvider 和 StoreConnector 就可以在 Flutter 實現 Redux 的功能了,接下來是具體實踐。

四、Redux 的實踐

這裡以常見的獲取列表選擇列表為例子。一個頁面用於展示選中項和跳轉到列表,一個頁面用於顯示列表。

Flutter狀態管理學習手冊(二)——Redux

1. 引入 Redux 的第三方庫

  redux: ^3.0.0
  flutter_redux: ^0.5.3
  redux_thunk: ^0.2.1
複製程式碼

2. 建立 Store

Store 全域性只有一個,這裡封裝一個建立 Store 的方法。建立Store時,傳入 Reducer ,初始化的 State 和 Middleware 。

Store<AppState> createStore() {
  return Store(appReducer, initialState: AppState.initial(), middleware: [
    // 引入 thunk action 的中介軟體
    thunkMiddleware
  ]);
}
複製程式碼

3. 建立 State

State 狀態,需要建立一個 AppState,作為整個應用的 state,除此之外,根據不同的頁面可以有不同的 State,比如有一個列表頁面 ListPage ,就可以有一個列表狀態 ListState ,並且 ListState 會放在 AppState 中作為成員變數進行管理。

// app state
class AppState {
  ListState listState;

  AppState(this.listState);
}
複製程式碼
// list state
class ListState {
  bool _init = false; // 列表初始化標誌
  List<String> _list = []; // 列表資料
  String _selected = '未選中'; // 選中的列表項
  
  ListState(this._init, this._list, this._selected);
}
複製程式碼

4. 建立 Action

Action 是 Widget 在使用者操作後發出的通知。對於 ListPage 頁面,建立一個 list_action.dart 檔案,用於存在可發出的 Action 。在示例中,需要用到兩個 Action 。一個是載入完成列表資料後發出的 Action ,用於重新整理列表,一個是選中列表項時發出的 Action 。

/**
 * 載入完成列表資料
 */
class FetchListAction {
  List<String> list;

  FetchListAction(this.list);
}

/**
 * 選擇列表
 */
class SelectItemAction {
  String selected;

  SelectItemAction(this.selected);
}
複製程式碼

5. 建立 Reducer

Reducer 是一個帶 State 和 Action 兩個引數的純函式,在這裡,只需要關心根據傳入的 Action 和舊的 State,返回一個新的 State。

和 State 一樣,reducer 也分為應用的 app_reducer 和列表頁面的 list_reducer

Reducer是純函式。在示例中通過 app_reducer 返回一個新的 AppState ,通過 list_reducer 分別對 FetchListActionSelectItemAction 進行處理返回一個新的 ListState

AppState appReducer(AppState state, dynamic action) {
  return AppState(listReducer(state.listState, action));
}
複製程式碼
ListState listReducer(ListState pre, dynamic action) {
  if (action is FetchListAction) {
    return ListState(true, action.list, pre.selected);
  }
  if (action is SelectItemAction) {
    return ListState(pre.isInit, pre.list, action.selected);
  }
  return pre;
}
複製程式碼

6. 使用 Thunk Action

在建立 Store 時引入了 thunkMiddleware ,使得專案支援 thunk action。

專案中需要一個載入列表的非同步操作,可通過 thunk action 實現具體操作,注意 ThunkAction 是隻有一個 store 引數的函式。

ThunkAction<AppState> fetchList = (Store<AppState> store) async {
  // 模擬網路請求
  await Future.delayed(Duration(milliseconds: 3000));
  var list = [
    "1. Redux State Management",
    "2. Redux State Management",
    "3. Redux State Management",
    "4. Redux State Management",
    "5. Redux State Management",
    "6. Redux State Management",
    "7. Redux State Management",
    "8. Redux State Management",
    "9. Redux State Management",
    "10. Redux State Management"
  ];

  store.dispatch(FetchListAction(list));
};
複製程式碼

7. UI 佈局

通過以上程式碼,我們做好了 Redux 的準備工作。接下來便可以佈局頁面。

① 在 main.dart 中,要做的工作是建立 Store,和使用 StoreProider 作為根佈局。

    // 建立Store
    var store = createStore();
    // 使用StoreProvider作為根佈局
    return StoreProvider<AppState>(
        store: store,
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: ShowPage(),
        ));
  }
複製程式碼

show_page.dart ,用於顯示選中的列表項和提供跳轉到 ListPage 的按鈕入口。

show_page.dart 中,選中的列表項的資料來自於 Store,所以這裡使用 StoreConnector 來根據資料構建 UI 介面。

StoreConnector<AppState, String>(
  converter: (store) => store.state.listState.selected,
  builder: (context, selected) {
    return Text(selected);
  },
),
複製程式碼

從上面可見,StoreConnector 需要兩個泛型,一個是我們建立的 AppState,另一個是 ViewModel,這裡我們直接將 String 作為 ViewModel。

StoreConnector 要定義兩個函式,一個是 converter,從 Store 中拿出選中的列表項資料 store.state.listState.selected,另一個是 builder,將 converter 返回的 selected 進一步轉化為介面:Text(selected)。

這就是 StoreConnector 的用法。

③ list_page.dart

list_page.dart 中的內容比較重點,裡面實現了載入列表和點選列表的功能。

list_page.dart 中一開始要顯示載入中的介面,等待資料成功載入後顯示列表介面。這裡也是用 StoreConnector 構建 UI。StoreConnector 將 AppState 轉換為 ListState,通過 ListState 判斷當前顯示 loading 介面還是列表介面。

另外,在 StoreConnector 的 onInit 函式中執行載入列表的操作。

由於載入列表是一個非同步操作,所以要用到之前定義的 fetchList 的 thunk action,這裡通過 store.dispatch(fetchList) 來執行。

StoreConnector<AppState, ListState>(
    // 初始化時載入列表
    onInit: (store) {
  if (!store.state.listState.isInit) {
    store.dispatch(fetchList);
  }
  // 將store的state轉化為viewModel
}, converter: (store) {
  return store.state.listState;
  // 通過viewModel更新介面
}, builder: (contxet, state) {
  // 根據狀態顯示介面
  if (!state.isInit) {
    // 顯示loading介面
    return buildLoad();
  } else {
    // 顯示列表介面
    var list = state.list;
    return buildList(list);
  }
})
複製程式碼

另外,點選列表時也要發出一個選中列表專案的 Action ,即之前定義的 SelectItemAction

var store = StoreProvider.of<AppState>(context);
store.dispatch(SelectItemAction(list[index]));
// 返回到上一級頁面
Navigator.pop(context);
複製程式碼

store.dispatch(action) 發出的 Action 經過 Middleware 和 Reducer 的處理後,轉變成 State,StoreConnector 就會自動地根據 State 重建 UI。

這樣,一個使用 Redux 架構的應用就基本成型了。完整程式碼可以參考github.com/windinwork/…

五、總結

在 Redux 中,資料總是"單向流動"的,保證了流程的清晰。相對於 ScopedModel 的架構, Redux 無疑更加規範,更易於開發和維護。只是, Redux 會相對地增加複雜性,所以,簡單小型的專案可以不需要去考慮引進 Redux 這種架構,但對於大型的 Flutter 專案來說, Redux 就十分有用的架構。

六、思考

由於 Redux 只有一個應用只有一個 Store ,所以應用需要對整個 AppState 進行維護,在大型客戶端程式碼迭代過程中,會和元件化和單獨模組編譯執行這種需求相矛盾,這是否會阻礙 Redux 成為大型專案採用的主要架構的因素呢?

參考目錄

Flutter Redux Thunk, an example finally.

Introduction to Redux in Flutter

官網推薦 Sample:inKino

相關文章