Flutter Redux 食用總結
效果
Flutter App本質上是一個單頁面應用,需要我們自己維護State,Model,Route。隨著業務的增加,這些工作會變得很複雜,也不可預測,復現一個bug會很困難,跨元件傳遞資料也很難。Redux思想繼承於Flux,通過合理的約定讓業務分層解耦,資料的變動可以預測,可以重現。Redux有三個原則:
1.單一的資料來源(App統一的Store)
2.狀態State是隻讀的(資料不能直接修改,只能用過約定的Action觸發,Reduce修改)
3.資料改動須是純函式(這些純函式叫Reducer,定義瞭如何修改Store,由Action觸發)
原理
Redux(3.0.0)是作者用Dart把JS 的redux庫實現了,它定義了Store,Action,Reduce,Middleware以及它們之間的行為關係。
flutter_redux(0.5.2)作為工具類橋接Redux和Flutter,它提供了StoreProvider,StoreBuilder,StoreConnector這些元件,使我們在flutter中使用redux變的很簡便。
流程圖

Action
Action定義一種行為,可以攜帶資訊,發往Store。換言之Store發生改變須由Action觸發。Live Template快捷鍵ac,建立一套Api Aciton:
class xxxRequestAction extends VoidAction {} class xxxSuccessAction extends ActionType { finalpayload; xxxSuccessAction({this.payload}) : super(payload: payload); } class xxxFailureAction extends ActionType { final RequestFailureInfo errorInfo; xxxFailureAction({this.errorInfo}) : super(payload: errorInfo); } 複製程式碼
API
App功能最小粒度依賴是API,一般我們前後端會約定一套Rest介面定義。這裡APP端是用一個靜態方法封裝實現的,裡面定義了Path,Request,Success,Failure三個Action的響應回撥。
static fetchxxx() { final access = StoreContainer.access; final apiFuture = Services.rest.get( '/zpartner_api/${access.path}/${access.businessGroupUid}/xxxx/'); Services.asyncRequest( apiFuture, xxxRequestAction(), (json) => xxxSuccessAction(payload: xxxInfo.fromJson(json)), (errorInfo) => xxxFailureAction(errorInfo: errorInfo)); } 複製程式碼
Reduce&state
State是Store的一個節點,定義了這個節點的資料申明,Reduce每一次響應Action都會建立一個新的State去替換原來Store裡的那個節點State。Reduce和State基本上是一對一的,所以把他們放在一個檔案裡。Live Template快捷鍵rd,建立一套Reduce&State:
@immutable class xxxState { final bool isLoading; xxxState({this.isLoading}); xxxState copyWith({bool isLoading}) { return xxxState(isLoading: isLoading ?? this.isLoading); } xxxState.initialState() : isLoading = false; } class xxxReducer { xxxState reducer(xxxState state, ActionType action) { switch (action.runtimeType) { case xxxRequestAction: return state.copyWith(isLoading: ); case xxxSuccessAction: return state.copyWith(isLoading: ); case xxxFailureAction: return state.copyWith(isLoading: ); default: return state; } } } 複製程式碼
Middleware
中介軟體,插在Action觸發後還沒有到達Reduce之間執行,一般是用來做一些API非同步請求並處理。這一步是可選的,當時鑑於Dio網路庫對資料有Json處理,flutter_epic表現也還不夠穩定。所以我們沒用Middleware而是封裝了一個工具方法在API services裡直接呼叫處理API並且根據結果分發對應Action。有接入整合測試的需要,需要重點考慮是否引入它。
進階
全域性Action
App裡的登出操作是比較特殊的,它可能在不同模組被調起,而且需要做的操作是清空整個Store。我們用了一個GlobalReduce去分發Action
AppState reduxReducer(AppState state, action) => GlobalReducer().reducer(state, action); class GlobalReducer { AppState reducer(AppState state, ActionType action) { switch (action.runtimeType) { case AppRestartAction: hasToken(); return _initialReduxState(); default: return AppState( login: LoginReducer().reducer(state.login, action), ...) } } } 複製程式碼
APIFuction
前面提到我們沒有使用Middleware,而是自己封裝了一個工具Function,好處是簡單易用,缺點是沒有明確返回值不好寫測試,利弊需要權衡下的。
/// common function for network with dio /// Future<Response> apiFuture [Dio.request] /// request action /// success action /// failure action static asyncRequest( Future<Response> apiFuture, ActionType request, ActionType Function(dynamic) success, ActionType Function(RequestFailureInfo) failure, ) async { // request StoreContainer.global.dispatch(request); final requestBegin = DateTimeUtil.dateTimeNowMilli(); try { final response = await apiFuture; final requestEnd = DateTimeUtil.dateTimeNowMilli(); final requestSpend = requestEnd - requestBegin; if (requestSpend < requestMinThreshold) { await Future.delayed(Duration( milliseconds: requestMinThreshold - requestSpend)); // 請求返回太快,頁面有點卡頓,有點尷尬 todo } // success StoreContainer.global.dispatch(success(response.data)); } on DioError catch (error) { var message = ''; var code = '-1'; var url = ''; if (error.response != null) { var errorData = error.response.data; List messageList = errorData is Map<String, dynamic> ? ((errorData['message']) ?? []) : []; messageList .forEach((item) => message = message + item.toString() + ' '); code = error.response.statusCode.toString(); url = error.response.request.baseUrl + error.response.request.path; } else { message = error.message; } final model = RequestFailureInfo( errorCode: code, errorMessage: message, dateTime: DateTimeUtil.dateTimeNowIso()); // failure StoreContainer.global.dispatch(failure(model)); } } 複製程式碼
區域性重新整理
使用flutter_redux提供的StoreConnector元件時,可以設定distinct為ture,Store變化後是否重新整理檢視可以完全自己控制。原理是需要過載ViewModel的==運算子和重寫hashcode方法。這樣在Store變化時,StoreStreamListener通過比對前後兩個ViewModel是否相等來觸發是否重新builder,而這個是否相等都是我們重寫並自己控制的。
class _RestartAppViewModel { Key key; bool isLogin; _RestartAppViewModel({this.key, this.isLogin}); static _RestartAppViewModel fromStore(Store<AppState> store) => _RestartAppViewModel( key: store.state.cache.key, isLogin: store.state.cache.isLogin); @override int get hashCode => key.hashCode ^ isLogin.hashCode; @override bool operator ==(other) => identical(this, other) || other is _RestartAppViewModel && key == other.key && isLogin == other.isLogin; } StoreConnector<AppState, _RestartAppViewModel>( distinct: true, builder: (context, vm) { return App(vm.isLogin, vm.key); }, converter: (store) => _RestartAppViewModel.fromStore(store)) 複製程式碼