注意:
閱讀這遍文章需要具備以下知識:
- 瞭解 Flutter,最好寫過 demo
- 瞭解 fish_redux 的基本概念(此文基本沒有概念性的東西)
一、 fish_redux 構建專案順序與專案目錄
- 專案構建步驟:
- 配置路由
- 使用 Page 構建頁面,這裡面可以配置 state、effect、reducer 等要素
- 定義全域性 state
- 定義 effect 、 middleware 、reducer 用於實現副作用、中介軟體、結果返回處理等
- 定義 view 用於繪製頁面
- 使用 dependencies 宣告頁面需要用的介面卡(adapter)、插槽(slots)
- 用 Connector(繼承自 ConnOp),將 store 的資料分配到 component
- 專案目錄:
這是我個人的專案結構,僅供參考。有些與官方的 example 不太一樣。例如官方 todoList 的 demo 中是把 connector 與 state 寫在一塊的,我這裡將 connector 單獨抽出了一個檔案。
二、關鍵 API
Route
fish_redux 有一套自己的路由,有三種 路由方式
- AppRoutes:多個 page 共享一個 store
- PageRoutes: 頁面級別的路由,每個頁面都有一個 store
- HybridRoutes:可以結合上面兩種 route 方式
AppRoutes
AppRoutes 包含了 State, Reducer, pages && connector,可以將 store 中的資料 分發到各個頁面。其組裝方式跟 Page 差不多哦。
例:
final AbstractRoutes appRoutes = AppRoutes<AppState>(
preloadedState: AppState.initState(),
pages: {
'homePage': HomePageConnector() + HomePage(),
},
reducer: buildReducer()
);
return MaterialApp(
home: appRoutes.buildPage('homePage', null),
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<Object>(builder: (BuildContext context) {
return appRoutes.buildPage(settings.name, settings.arguments);
});
},
);
/// 想要使用 AppRoutes 發起一個 action,需要將它儲存起來,使用它的 store
/// 像下面這樣
appRoutes.store.dispatch(AppActionCreator.someEvent());
複製程式碼
- 使用 AppRoutes 建立了一個路由 -> appRoutes
- 使用 preloadedState 屬性初始化 state
- 裡面定義了 homePage 頁面,HomePageConnector 中將某些 state 分配給 homePage。
PageRoutes
PageRoutes 使用起來就比較簡單,因為其不需要定義與分配 state,每個頁面的 store 在其自身的 Page 裡定義。
例:
final AbstractRoutes pageRoutes = PageRoutes(
pages: <String, Page<Object, dynamic>>{
'homePage': HomePage(),
},
);
return MaterialApp(
home: pageRoutes.buildPage('homePage', null),
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<Object>(builder: (BuildContext context) {
return pageRoutes.buildPage(settings.name, settings.arguments);
});
},
);
複製程式碼
HybridRoutes
HybridRoutes 接受一個 route 陣列,陣列裡既可以接受 AppRoutes 也可以接受 PageRoutes
例:
final AbstractRoutes hybridRoutes = HybridRoutes(routes: <AbstractRoutes>[
appRoutes,
pageRoutes
]);
複製程式碼
狀態管理(redux)
Store
這個store是我自己的叫法,類比於 redux 的 store,定義全域性的 state.
1、 定義 store 資料
使用 class 來宣告全域性 state,在 宣告 page 的時候注入
例:
class PageState implements Cloneable <PageState> {
String someState;
@override
PageState clone() {
return PageState()
..someState = someState
}
}
複製程式碼
- 上面有個 clone 方法,用來獲取 state,在 reducer 中會用到用來 merge state。
- 需要注意的是,如果沒有特殊場景需要,在 clone 中要把所有的 state 寫全,不然在使用 clone 方法進行 merge 資料的時候,會丟掉裡面沒寫上的資料。
- 使用 Connect 給元件分配 state
類比於 redux 的 connect
class HeaderConnector extends ConnOp<PageState, HeaderState> {
@override
HeaderState get(PageState state) {
final HeaderState headerState = HeaderState(
someState: state.someState,
);
return headerState;
}
@override
void set(PageState state, HeaderState headerState) {
state.someState = headerState.someState;
}
}
複製程式碼
- 上面例子將 store 中的 someState 分配給了 HeaderState,作為 header 元件的 state。
- 裡面的 set、get 方法會在更新 state 的時候自動呼叫。
- 這裡的 HeaderConnector 會在 Page 建立的時候 dependencies 的 slots 中使用,作用是分配 store。
Action
類比於 redux 的 action,當發起 action 的時候,根據 action 名字匹配 reducer
例:
enum HomeAction {
setSomeState,
}
class HomeActionCreator {
static Action setSomeState(String someState) {
return Action(HomeAction.setSomeState, payload: someState);
}
}
複製程式碼
官方推薦的做法是像上面一樣用兩個類管理 action
- HomeAction 是一個列舉類,作為 action 的集合
- HomeActionCreator 是一個 ActionCreator 類,在這裡面可以約束 payload 型別,在 dispatch 的時候掉用 這個類中的函式並傳入 playload。
reducer
類比 redux 的 reducer,在這裡處理髮起的 action,並更新 state
例:
Reducer<PageState> buildHomeReducer() {
return asReducer({
HomeAction.setSomeState: setSomeStateHandle,
});
}
PageState setSomeStateHandle(PageState state, Action action) {
final newSomeState = action.payload;
return state.clone()..someState = newSomeState;
}
複製程式碼
- buildHomeReducer 中對 action 與 reducer 進行了對映, 當 dispatch 名為 setSomeState 的 action 時,呼叫下面的 setSomeStateHandle 方法
- 上面的 setSomeStateHandle 使用了 PageState 的 clone 函式複製了一份新的資料,修改之後並 return
- 需要注意的是,與 redux 一樣。fish_redux 的 state 也是 immutable 的,並且直接修改不生效,只能進行替換。
Middleware(中介軟體)
類比 redux 中的 Middleware,在 dispatch 時呼叫
例:
import 'package:fish_redux/fish_redux.dart';
Middleware<T> logMiddleware1<T>({
String tag = 'redux',
String Function(T) monitor,
}) {
return ({Dispatch dispatch, Get<T> getState}) {
return (Dispatch next) {
print('1');
return next;
};
};
}
複製程式碼
- 上面是一個最簡單的中介軟體,在 dispatch 的時候會列印 1, 當
return next
後會執行下一個中介軟體。 - 中介軟體的執行順序是正序的。
Component
每一個 Component 都是一個元件,它對檢視(view),修改資料(reducer), 非修改資料操作(effect)這三個邏輯進行了剝離。
Component = View + Effect(可選) + Reducer(可選) + Dependencies(可選)
例:
class HeaderComponent extends Component<HeaderState> {
HeaderComponent()
: super(
view: buildView,
effect: buildEffect(),
reducer: buildReducer(),
dependencies: Dependencies<HeaderState>(
adapter: SomeAdapter(),
slots: <String, Dependent<HeaderState>>{
'avatar': AvatarConnector() + AvaterComponent()
}),
);
}
複製程式碼
- 這是一個 header 元件,裡面使用了 view、effect、reducer、dependencies 四個配置專案,下面挨個介紹
dependencies
dependencies 是一個表達元件之間依賴關係的結構。它包含兩個屬性 adapter 和 slots。
- adapter: 元件依賴的具體介面卡(用來構建高效能的 ListView)。
- slots:元件依賴的插槽。
- 上面的例子中,其中 slots 裡有個 ‘avatar’,這是說明 HeaderComponent 頁依賴 avatar 元件,其元件名叫 AvaterComponent。 AvaterComponent 的 state 在 AvatarConnector 裡被分配。
- adapter 和 所依賴的元件 會在
view
中被ViewService.buildComponent
呼叫使用
adapter
adapter 是對 listView 的封裝優化。適用於長列表的渲染。其有三種實現方式。
- DynamicFlowAdapter 接受陣列型別的資料驅動
- StaticFlowAdapter 接受map型別的資料驅動
- CustomAdapter 自定義 adapter
1. DynamicFlowAdapter:
class SomeDynamicAdapter extends DynamicFlowAdapter<PageState> {
SomeDynamicAdapter()
: super(
pool: <String, Component<Object>>{
'task': Task(),
},
connector: Connector(),
// reducer: buildReducer(),
// filter: bindReducerFilter(),
// effect: bindEffect(),
// higherEffect: bindHeightEffect(),
// onError: onError(),
);
}
class Connector extends ConnOp<PageState, List<ItemBean>> {
@override
List<ItemBean> get(PageState state) {
return state.taskList
.map<ItemBean>((TaskState data) => ItemBean('task', data))
.toList(growable: true);
}
}
複製程式碼
- 這個檔案有兩部分,一個是 adapter 主體類,一個是 adapter 的資料來源 --
Connect
類 - adapter 這裡只用到了 pool 與 connector 屬性。其還有 reducer、filter、effect 等其他屬性,這裡不列舉。
- pool 屬性用來說明 adapter 所依賴的子元件。這裡用到了一個名叫 'task' 的元件,會在下面的 Connector 中用來作為列表的 itemView。
- 使用
ItemBean
方法將data
傳進task
元件
2. StaticFlowAdapter
staticFlowAdapter 接收 map 型別的資料
例:
class SomeStaticAdapter extends StaticFlowAdapter<PageState> {
SomeStaticAdapter()
: super(
slots: [
SomeComponent().asDependent(SomeComponenConnectt()),
TaskAdapter().asDependent(TaskAdapterConnect()),
],
);
}
複製程式碼
- StaticFlowAdapter 沒有 pool 屬性,但是有 slots,其他的屬性與 DynamicFlowAdapter 相同。
- slots 接受一個陣列,裡面每一個元素都是與 connect 連線好的 component 或 adapter。
3. CustomFlowAdapter
CustomFlowAdapter 接受的屬性與 Component 一樣, 唯一的不同是 Adapter 的檢視部分返回的是一個 ListAdapter
例:
import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';
class CommentAdapter extends Adapter<PageState> {
CommentAdapter()
: super(
adapter: buildCommentAdapter,
);
}
ListAdapter buildCommentAdapter(PageState state, Dispatch dispatch, ViewService service) {
final List builders = Collections.compact([
_buildCommentHeader(state, dispatch, service),
]);
if (state.commentList.length > 0) {
builders.addAll(_buildCommentList(state, dispatch, service));
} else {
builders.addAll(_buildEmpty(state, dispatch, service));
}
return ListAdapter(
(BuildContext buildContext, int index) => builders[index](buildContext, index),
builders.length,
);
}
複製程式碼
- 上面示例是一個 CustomFlowAdapter,其 adapter 是一個方法 buildCommentAdapter,這個方法返回了一個 ListAdapter。
- buildCommentAdapter 里根據資料生成一個檢視列表。
- 例如示例中如果
commentList.length > 0
就在 ListAdapter 中加入一個 評論列表,如果沒有評論就加入一個空檢視的 widget。
view
view 是一個輸出 Widget 的上下文無關的函式。其負責檢視層的構建,由 state 驅動。
例:
Widget buildView(HeaderState state, Dispatch dispatch, ViewService viewService) {
final ListAdapter useAdapter = viewService.buildAdapter();
return FlatButton(
child: Container(
child: ListView(
children: <Widget>[
viewService.buildComponent('avatar'),
ListView.builder(
itemBuilder: useAdapter.itemBuilder,
itemCount: useAdapter.itemCount)
],
),
),
onPressed: () {
dispatch(HeaderActionCreator.profileOpen(state.profileIsOpen));
},
);
}
複製程式碼
- 這是一個 view,其內容是一個按鈕,點選發起一個 action
- 按鈕的 child 是他依賴的 avatar 元件,由 ViewService.buildComponent 呼叫。
- onPressed 的時候呼叫 dispatch 事件,在對應的 reducer 中更新 state。
引數:
- state:傳進來的 state
- dispatch:發起 action
- viewService:呼叫 dependencies 所宣告依賴的 adapter 或 slots
- viewService.buildAdapter: 呼叫依賴的 adapter
- viewService.buildComponent(slotsName): 呼叫依賴的 slots
effect
Effect 是一個處理所有副作用的函式。我把它分為兩種,一種是對生命週期的回撥,一種是對非處理資料事件的回撥。這裡面不做任何資料的處理,如果需要處理資料的話就發起一個 action 到 reducer 裡處理。
例:
Effect<PageState> buildEffect() {
return combineEffects(<Object, Effect<PageState>>{
Lifecycle.initState: _initStateEffect,
'onClick': _onClickEffect,
});
}
void _initStateEffect(Action action, Context context)async {
final response = await fetch(
'GET',
'https://xxx.api.xxx',
params: {
'size': 10,
'page': 1,
}
);
context.dispatch(HomeActionCreator.onSetTaskDataAction(response));
}
複製程式碼
- 上面的 Effect 做了兩件事,一個是監聽 initState 生命週期;一個是處理 onClick action
- 在 initState 的時候,發起了一個請求並且同步得到 response,但是 state 是不能在這裡進行處理的,所以
context.dispatch
了一個 action 去 reducer 中處理。
Page
用來構建頁面,每個頁面都有一個 Page 並且有一個 store。在這裡初始化 store,配置 Middleware,對 Redux 做 AOP 管理。它繼承於 Component
例:
// page, 繼承自 Component,PageState 是 store 裡定義的 state
class HomePage extends Page<PageState, Map<String, dynamic>> {
HomePage()
: super(
view: buildView,
initState: initState,
effect: buildEffect(),
reducer: buildHomeReducer(),
dependencies: Dependencies<PageState>(
adapter: SomeAdapter(),
slots: <String, Dependent<PageState>>{
'header': HeaderConnector() + HeaderComponent(),
}
),
middleware: <Middleware<PageState>>[ // 中介軟體
logMiddleware1(tag: 'HomePage'),
logMiddleware2(tag: 'HomePage'),
],
);
}
複製程式碼
- middleware 是一個陣列,用來註冊中介軟體,示例中註冊了兩個中介軟體,在 dispatch 的時候,他們會按序執行
其他配置
除了上面那些 Component 還有其他幾個比較簡單或不常用的配置,如:OnError 、HigherEffect 等,如果以後有空了再補充。