好久沒更新文章了,最近趁著娃睡覺的功夫,嘗試了下 fish_redux,這邊做下記錄,安全無毒,小伙伴們可放心食用(本文基于版本 fish_redux 0.3.1)。
fish_redux 的介紹就不在這廢話了,需要的小伙伴可以直接查看 fish_redux 官方文檔,這里我們直接通過例子來踩坑。
項目的大概結(jié)構(gòu)如下所示,具體可以查看 倉庫代碼

可以看到 UI 包下充斥著許多的 action,effect,reducer,state,view,page,component,adapter 類,不要慌,接下來大概的會說明下每個類的職責。
fish_redux 的分工合作
-
action是用來定義一些操作的聲明,其內(nèi)部包含一個枚舉類XxxAction和 聲明類XxxActionCreator,枚舉類用來定義一個操作,ActionCreator用來定義一個Action,通過dispatcher發(fā)送對應(yīng)Action就可以實現(xiàn)一個操作。例如我們需要打開一個行的頁面,可以如下進行定義enum ExamAction { openNewPage, openNewPageWithParams } class ExamActionCreator { static Action onOpenNewPage(){ // Action 可以傳入一個 payload,例如我們需要攜帶參數(shù)跳轉(zhuǎn)界面,則可以通過 payload 傳遞 // 然后在 effect 或者 reducer 層通過 action.payload 獲取 return const Action(ExamAction.openNewPage); } static Action onOpenNewPageWithParams(String str){ return Action(ExamAction.openNewPageWithParams, payload: str); } } -
effect用來定義一些副作用的操作,例如網(wǎng)絡(luò)請求,頁面跳轉(zhuǎn)等,通過buildEffect方法結(jié)合Action和最終要實現(xiàn)的副作用,例如還是打開頁面的操作,可通過如下方式實現(xiàn)Effect<ExamState> buildEffect() { return combineEffects(<Object, Effect<ExamState>>{ ExamAction.openNewPage: _onOpenNewPage, }); } void _onOpenNewPage(Action action, Context<ExamState> ctx) { Navigator.of(ctx.context).pushNamed('路由地址'); } -
reducer用來定義數(shù)據(jù)發(fā)生變化的操作,比如網(wǎng)絡(luò)請求后,數(shù)據(jù)發(fā)生了變化,則把原先的數(shù)據(jù)clone一份出來,然后把新的值賦值上去,例如有個網(wǎng)絡(luò)請求,發(fā)生了數(shù)據(jù)的變化,可通過如下方式實現(xiàn)Reducer<ExamState> buildReducer() { return asReducer( <Object, Reducer<ExamState>>{ HomeAction.onDataRequest: _onDataRequest, }, ); } ExamState _onDataRequest(ExamState state, Action action) { // data 的數(shù)據(jù)通過 action 的 payload 進行傳遞,reducer 只負責數(shù)據(jù)刷新 return state.clone()..data = action.payload; } state就是當前頁面需要展示的一些數(shù)據(jù)view就是當前的UI展示效果page和component就是上述的載體,用來將數(shù)據(jù)和UI整合到一起adapter用來整合列表視圖
Show the code
這邊要實現(xiàn)的例子大概長下面的樣子,一個 Drawer 列表,實現(xiàn)主題色,語言,字體的切換功能,當然后期會增加別的功能,目前先看這部分[home 模塊],基本上涵蓋了上述所有的內(nèi)容。在寫代碼之前,可以先安裝下 FishRedux 插件,可以快速構(gòu)建類,直接在插件市場搜索即可

整體配置
void main() {
runApp(createApp());
}
Widget createApp() {
// 頁面路由配置,所有頁面需在此注冊路由名
final AbstractRoutes routes = PageRoutes(
pages: <String, Page<Object, dynamic>>{
RouteConfigs.route_name_splash_page: SplashPage(), // 起始頁
RouteConfigs.route_name_home_page: HomePage(), // home 頁
});
return MaterialApp(
title: 'FishWanAndroid',
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
localizationsDelegates: [ // 多語言配置
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
FlutterI18nDelegate()
],
supportedLocales: [Locale('en'), Locale('zh')],
home: routes.buildPage(RouteConfigs.route_name_splash_page, null), // 配置 home 頁
onGenerateRoute: (settings) {
return CupertinoPageRoute(builder: (context) {
return routes.buildPage(settings.name, settings.arguments);
});
},
);
}
Home 整體構(gòu)建
Home 頁面整體就是一個帶 Drawer,主體是一個 PageView,頂部帶一個 banner 控件,banner 的數(shù)據(jù)我們通過網(wǎng)絡(luò)進行獲取,在 Drawer 是一個點擊列表,包括圖標,文字和動作,那么我們可以創(chuàng)建一個 DrawerSettingItem 類,用了創(chuàng)建列表,頭部的用戶信息目前可以先寫死。所以我們可以先搭建 HomeState
class HomeState implements Cloneable<HomeState> {
int currentPage; // PageView 的當前項
List<HomeBannerDetail> banners; // 頭部 banner 數(shù)據(jù)
List<SettingItemState> settings; // Drawer 列表數(shù)據(jù)
@override
HomeState clone() {
return HomeState()
..currentPage = currentPage
..banners = banners
..settings = settings;
}
}
HomeState initState(Map<String, dynamic> args) {
return HomeState();
}
同樣的 HomeAction 也可以定義出來
enum HomeAction { pageChange, fetchBanner, loadSettings, openDrawer, openSearch }
class HomeActionCreator {
static Action onPageChange(int page) { // PageView 切換
return Action(HomeAction.pageChange, payload: page);
}
static Action onFetchBanner(List<HomeBannerDetail> banner) { // 更新 banner 數(shù)據(jù)
return Action(HomeAction.fetchBanner, payload: banner);
}
static Action onLoadSettings(List<SettingItemState> settings) { // 加載 setting 數(shù)據(jù)
return Action(HomeAction.loadSettings, payload: settings);
}
static Action onOpenDrawer(BuildContext context) { // 打開 drawer 頁面
return Action(HomeAction.openDrawer, payload: context);
}
static Action onOpenSearch() { // 打開搜索頁面
return const Action(HomeAction.openSearch);
}
}
構(gòu)建 banner
為了加強頁面的復(fù)用性,可以通過 component 進行模塊構(gòu)建,具體查看 banner_component 包下文件。首先定義 state,因為 banner 作為 home 下的內(nèi)容,所以其 state 不能包含 HomeState 外部的屬性,因此定義如下
class HomeBannerState implements Cloneable<HomeBannerState> {
List<HomeBannerDetail> banners; // banner 數(shù)據(jù)列表
@override
HomeBannerState clone() {
return HomeBannerState()..banners = banners;
}
}
HomeBannerState initState(Map<String, dynamic> args) {
return HomeBannerState();
}
action 只有點擊的 Action,所以也可以快速定義
enum HomeBannerAction { openBannerDetail }
class HomeBannerActionCreator {
static Action onOpenBannerDetail(String bannerUrl) {
return Action(HomeBannerAction.openBannerDetail, payload: bannerUrl);
}
}
由于不涉及到數(shù)據(jù)的改變,所以可以不需要定義 reducer,通過 effect 來處理 openBannerDetail 即可
Effect<HomeBannerState> buildEffect() {
return combineEffects(<Object, Effect<HomeBannerState>>{
// 當收到 openBannerDetail 對應(yīng)的 Action 的時候,執(zhí)行對應(yīng)的方法
HomeBannerAction.openBannerDetail: _onOpenBannerDetail,
});
}
void _onOpenBannerDetail(Action action, Context<HomeBannerState> ctx) {
// payload 中攜帶了 bannerUrl 參數(shù),用來打開對應(yīng)的網(wǎng)址
// 可查看 [HomeBannerActionCreator.onOpenBannerDetail] 方法定義
RouteConfigs.openWebDetail(ctx.context, action.payload);
}
接著就是對 view 進行定義啦
Widget buildView(HomeBannerState state, Dispatch dispatch, ViewService viewService) {
var _size = MediaQuery.of(viewService.context).size;
return Container(
height: _size.height / 5, // 設(shè)置固定高度
child: state.banners == null || state.banners.isEmpty
? SizedBox()
: Swiper( // 當有數(shù)據(jù)存在時,才顯示 banner
itemCount: state.banners.length,
transformer: DeepthPageTransformer(),
loop: true,
autoplay: true,
itemBuilder: (_, index) {
return GestureDetector(
child: FadeInImage.assetNetwork(
placeholder: ResourceConfigs.pngPlaceholder,
image: state.banners[index].imagePath ?? '',
width: _size.width,
height: _size.height / 5,
fit: BoxFit.fill,
),
onTap: () { // dispatch 對應(yīng)的 Action,當 effect 或者 reduce 收到會進行對應(yīng)處理
dispatch(HomeBannerActionCreator.onOpenBannerDetail(state.banners[index].url));
},
);
},
),
);
}
最后再回到 component,這個類插件已經(jīng)定義好了,基本上不需要做啥修改
class HomeBannerComponent extends Component<HomeBannerState> {
HomeBannerComponent()
: super(
effect: buildEffect(), // 對應(yīng) effect 的方法
reducer: buildReducer(), // 對應(yīng) reducer 的方法
view: buildView, // 對應(yīng) view 的方法
dependencies: Dependencies<HomeBannerState>(
adapter: null, // 用于展示數(shù)據(jù)列表
// 組件插槽,注冊后可通過 viewService.buildComponent 方法生成對應(yīng)組件
slots: <String, Dependent<HomeBannerState>>{},
),
);
}
這樣就定義好了一個 component,可以通過注冊 slot 方法使用該 component
使用 banner component
在上一步,我們已經(jīng)定義好了 banner component,這里就可以通過 slot 愉快的進行使用了,首先,需要定義一個 connector,connector 是用來連接兩個父子 state 的橋梁。
// connector 需要繼承 ConnOp 類,并混入 ReselectMixin,泛型分別為父級 state 和 子級 state
class HomeBannerConnector extends ConnOp<HomeState, HomeBannerState> with ReselectMixin {
@override
HomeBannerState computed(HomeState state) {
// computed 用于父級 state 向子級 state 數(shù)據(jù)的轉(zhuǎn)換
return HomeBannerState()..banners = state.banners;
}
@override
List factors(HomeState state) {
// factors 為轉(zhuǎn)換的因子,返回所有改變的因子即可
return state.banners ?? [];
}
}
在 Page 中注冊 slot
page 的結(jié)構(gòu)和 component 的結(jié)構(gòu)是一樣的,使用 component 直接在 dependencies 中注冊 slots 即可
class HomePage extends Page<HomeState, Map<String, dynamic>> {
HomePage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<HomeState>(
adapter: null,
slots: <String, Dependent<HomeState>>{
// 通過 slot 進行 component 注冊
'banner': HomeBannerConnector() + HomeBannerComponent(),
'drawer': HomeDrawerConnector() + HomeDrawerComponent(), // 定義側(cè)滑組件,方式同 banner
},
),
middleware: <Middleware<HomeState>>[],
);
}
注冊完成 slot 之后,就可以直接在 view 上使用了,使用的方法也很簡單
Widget buildView(HomeState state, Dispatch dispatch, ViewService viewService) {
var _pageChildren = <Widget>[
// page 轉(zhuǎn)換成 widget 通過 buildPage 實現(xiàn),參數(shù)表示要傳遞的參數(shù),無需傳遞則為 null 即可
// 目前 HomeArticlePage 只做簡單的 text 展示
HomeArticlePage().buildPage(null),
HomeArticlePage().buildPage(null),
HomeArticlePage().buildPage(null),
];
return Theme(
data: ThemeData(primarySwatch: state.themeColor),
child: Scaffold(
body: Column(
children: <Widget>[
// banner slot
// 通過 viewService.buildComponent('slotName') 使用,slotName 為 page 中注冊的 component key
viewService.buildComponent('banner'),
Expanded(
child: TransformerPageView(
itemCount: _pageChildren.length,
transformer: ScaleAndFadeTransformer(fade: 0.2, scale: 0.8),
onPageChanged: (index) {
// page 切換的時候把當前的 page index 值通過 action 傳遞給 state,
// state 可查看上面提到的 HomeState
dispatch(HomeActionCreator.onPageChange(index));
},
itemBuilder: (context, index) => _pageChildren[index],
),
),
],
),
// drawer slot,方式同 banner
drawer: viewService.buildComponent('drawer'),
),
);
}
更新 banner 數(shù)據(jù)
在前面的 HomeActionCreator 中,我們定義了 onFetchBanner 這個 Action,需要傳入一個 banner 列表作為參數(shù),所以更新數(shù)據(jù)可以這么進行操作
Effect<HomeState> buildEffect() {
return combineEffects(<Object, Effect<HomeState>>{
// Lifecycle 的生命周期同 StatefulWidget 對應(yīng),所以在初始化的時候處理請求 banner 數(shù)據(jù)等初始化操作
Lifecycle.initState: _onPageInit,
});
}
void _onPageInit(Action action, Context<HomeState> ctx) async {
ctx.dispatch(HomeActionCreator.onPageChange(0));
var banners = await Api().fetchHomeBanner(); // 網(wǎng)絡(luò)請求,具體的可以查看 `api.dart` 文件
ctx.dispatch(HomeActionCreator.onFetchBanner(banners)); // 通過 dispatch 發(fā)送 Action
}
一開始我們提到過,effect 只負責一些副作用的操作,reducer 負責數(shù)據(jù)的修改操作,所以在 reducer 需要做數(shù)據(jù)的刷新
Reducer<HomeState> buildReducer() {
return asReducer(
<Object, Reducer<HomeState>>{
// 當 dispatch 發(fā)送了對應(yīng)的 Action 的時候,就會調(diào)用對應(yīng)方法
HomeAction.fetchBanner: _onFetchBanner,
},
);
}
HomeState _onFetchBanner(HomeState state, Action action) {
// reducer 修改數(shù)據(jù)方式是先 clone 一份數(shù)據(jù),然后進行賦值
// 這樣就把網(wǎng)絡(luò)請求返回的數(shù)據(jù)更新到 view 層了
return state.clone()..banners = action.payload;
}
通過上述操作,就將網(wǎng)絡(luò)的 banner 數(shù)據(jù)加載到 UI 了
使用 adapter 構(gòu)建 drawer 功能列表
drawer 由一個頭部和列表構(gòu)成,頭部可以通過 component 進行構(gòu)建,方法類似上述 banner component 和 drawer component,唯一區(qū)別就是一個在 page 的 slots 注冊,一個在 component 的 slots 注冊。所以構(gòu)建 drawer 就是需要去構(gòu)建一個列表,這里就需要用到 adapter 來處理了。
在老的版本中(本文版本 0.3.1),構(gòu)建 adapter 一般通過 DynamicFlowAdapter 實現(xiàn),而且在插件中也可以發(fā)現(xiàn),但是在該版本下,DynamicFlowAdapter 已經(jīng)被標記為過時,并且官方推薦使用 SourceFlowAdapter。SourceFlowAdapter 需要指定一個 State,并且該 State 必須繼承自 AdapterSource。AdapterSource 有兩個子類,分別是可變數(shù)據(jù)源的 MutableSource 和不可變數(shù)據(jù)源的 ImmutableSource,兩者的差別因為官方也沒有給出具體的說明,本文使用 MutableSource 來處理 adapter。所以對應(yīng)的 state 定義如下
class HomeDrawerState extends MutableSource implements Cloneable<HomeDrawerState> {
List<SettingItemState> settings; // state 為列表 item component 對應(yīng)的 state
@override
HomeDrawerState clone() {
return HomeDrawerState()
..settings = settings;
}
@override
Object getItemData(int index) => settings[index]; // 對應(yīng) index 下的數(shù)據(jù)
@override
String getItemType(int index) => DrawerSettingAdapter.settingType; // 對應(yīng) index 下的數(shù)據(jù)類型
@override
int get itemCount => settings?.length ?? 0; // 數(shù)據(jù)源長度
@override
void setItemData(int index, Object data) => settings[index] = data; // 對應(yīng) index 下的數(shù)據(jù)如何修改
}
同樣,adapter 也可以如下進行定義
class DrawerSettingAdapter extends SourceFlowAdapter<HomeDrawerState> {
static const settingType = 'setting';
DrawerSettingAdapter()
: super(pool: <String, Component<Object>>{
// 不同數(shù)據(jù)類型,對應(yīng)的 component 組件,type 和 state getItemType 方法對應(yīng)
// 允許多種 type
settingType: SettingItemComponent(),
});
}
經(jīng)過上述兩部分,就定義好了 adapter 的主體部分啦,接著就是要實現(xiàn) SettingItemComponent 這個組件,只需要簡單的 ListTile 即可,ListTile 的展示內(nèi)容通過對應(yīng)的 state 來設(shè)置
/// state
class SettingItemState implements Cloneable<SettingItemState> {
DrawerSettingItem item; // 定義了 ListTile 的圖標,文字,以及點擊
SettingItemState({this.item});
@override
SettingItemState clone() {
return SettingItemState()
..item = item;
}
}
/// view
Widget buildView(SettingItemState state, Dispatch dispatch, ViewService viewService) {
return ListTile(
leading: Icon(state.item.itemIcon),
title: Text(
FlutterI18n.translate(viewService.context, state.item.itemTextKey),
style: TextStyle(
fontSize: SpValues.settingTextSize,
),
),
onTap: () => dispatch(state.item.action),
);
}
因為不涉及數(shù)據(jù)的修改,所以不需要定義 reducer,點擊實現(xiàn)通過 effect 實現(xiàn)即可,具體的代碼可查看對應(yīng)文件,這邊不貼多余代碼了.
經(jīng)過上述步驟,adapter 就定義完成了,接下來就是要使用對應(yīng)的 adapter 了,使用也非常方便,我們回到 HomeDrawerComponent 這個類,在 adapter 屬性下加上我們前面定義好的 DrawerSettingAdapter 就行了
/// component
class HomeDrawerComponent extends Component<HomeDrawerState> {
HomeDrawerComponent()
: super(
view: buildView,
dependencies: Dependencies<HomeDrawerState>(
// 給 adapter 屬性賦值的時候,需要加上 NoneConn<XxxState>
adapter: NoneConn<HomeDrawerState>() + DrawerSettingAdapter(),
slots: <String, Dependent<HomeDrawerState>>{
'header': HeaderConnector() + SettingHeaderComponent(),
},
),
);
}
/// 對應(yīng) view
Widget buildView(HomeDrawerState state, Dispatch dispatch, ViewService viewService) {
return Drawer(
child: Column(
children: <Widget>[
viewService.buildComponent('header'),
Expanded(
child: ListView.builder(
// 通過 viewService.buildAdapter 獲取列表信息
// 同樣,在 GridView 也可以使用 adapter
itemBuilder: viewService.buildAdapter().itemBuilder,
itemCount: viewService.buildAdapter().itemCount,
),
)
],
),
);
}
將列表設(shè)置到界面后,就剩下最后的數(shù)據(jù)源了,數(shù)據(jù)從哪來呢,答案當然是和 banner component 一樣,通過上層獲取,這邊不需要通過網(wǎng)絡(luò)獲取,直接在本地定義就行了,具體的獲取查看文件 home\effect.dart 下的 _loadSettingItems 方法,實現(xiàn)和獲取 banner 數(shù)據(jù)無多大差別,除了一個本地加載,一個網(wǎng)絡(luò)獲取。
fish_redux 實現(xiàn)全局狀態(tài)
fish_redux 全局狀態(tài)的實現(xiàn),我們參考 官方 demo,首先構(gòu)造一個 GlobalBaseState 抽象類(涉及到全局狀態(tài)變化的 state 都需要繼承該類),這個類定義了全局變化的狀態(tài)屬性,例如我們該例中需要實現(xiàn)全局的主題色,語言和字體的改變,那么我們就可以如下定義
abstract class GlobalBaseState {
Color get themeColor;
set themeColor(Color color);
Locale get localization;
set localization(Locale locale);
String get fontFamily;
set fontFamily(String fontFamily);
}
接著需要定義一個全局 State,繼承自 GlobalBaseState 并實現(xiàn) Cloneable
class GlobalState implements GlobalBaseState, Cloneable<GlobalState> {
@override
Color themeColor;
@override
Locale localization;
@override
String fontFamily;
@override
GlobalState clone() {
return GlobalState()
..fontFamily = fontFamily
..localization = localization
..themeColor = themeColor;
}
}
接著需要定義一個全局的 store 來存儲狀態(tài)值
class GlobalStore {
// Store 用來存儲全局狀態(tài) GlobalState,當刷新狀態(tài)值的時候,通過
// store 的 dispatch 發(fā)送相關(guān)的 action 即可做出相應(yīng)的調(diào)整
static Store<GlobalState> _globalStore;
static Store<GlobalState> get store => _globalStore ??= createStore(
GlobalState(),
buildReducer(), // reducer 用來刷新狀態(tài)值
);
}
/// action
enum GlobalAction { changeThemeColor, changeLocale, changeFontFamily }
class GlobalActionCreator {
static Action onChangeThemeColor(Color themeColor) {
return Action(GlobalAction.changeThemeColor, payload: themeColor);
}
static Action onChangeLocale(Locale localization) {
return Action(GlobalAction.changeLocale, payload: localization);
}
static Action onChangeFontFamily(String fontFamily) {
return Action(GlobalAction.changeFontFamily, payload: fontFamily);
}
}
/// reducer 的作用就是刷新主題色,字體和語言
Reducer<GlobalState> buildReducer() {
return asReducer(<Object, Reducer<GlobalState>>{
GlobalAction.changeThemeColor: _onThemeChange,
GlobalAction.changeLocale: _onLocalChange,
GlobalAction.changeFontFamily: _onFontFamilyChange,
});
}
GlobalState _onThemeChange(GlobalState state, Action action) {
return state.clone()..themeColor = action.payload;
}
GlobalState _onLocalChange(GlobalState state, Action action) {
return state.clone()..localization = action.payload;
}
GlobalState _onFontFamilyChange(GlobalState state, Action action) {
return state.clone()..fontFamily = action.payload;
}
定義完全局 State 和 Store 后,回到我們的 main.dart 下注冊路由部分,一開始我們使用 PageRoutes 的時候只傳入了 page 參數(shù),還有個 visitor 參數(shù)沒有使用,這個就是用來刷新全局狀態(tài)的。
final AbstractRoutes routes = PageRoutes(
pages: <String, Page<Object, dynamic>>{
// ...
},
visitor: (String path, Page<Object, dynamic> page) {
if (page.isTypeof<GlobalBaseState>()) {
// connectExtraStore 方法將 page store 和 app store 連接起來
// globalUpdate() 就是具體的實現(xiàn)邏輯
page.connectExtraStore<GlobalState>(GlobalStore.store, globalUpdate());
}
});
/// globalUpdate
globalUpdate() => (Object pageState, GlobalState appState) {
final GlobalBaseState p = pageState;
if (pageState is Cloneable) {
final Object copy = pageState.clone();
final GlobalBaseState newState = copy;
// pageState 屬性和 appState 屬性不相同,則把 appState 對應(yīng)的屬性賦值給 newState
if (p.themeColor != appState.themeColor) {
newState.themeColor = appState.themeColor;
}
if (p.localization != appState.localization) {
newState.localization = appState.localization;
}
if (p.fontFamily != appState.fontFamily) {
newState.fontFamily = appState.fontFamily;
}
return newState; // 返回新的 state 并將數(shù)據(jù)設(shè)置到 ui
}
return pageState;
};
定義好全局 State 和 Store 之后,只需要 PageState 繼承 GlobalBaseState 就可以愉快的全局狀態(tài)更新了,例如我們查看 ui/settings 該界面涉及了全局狀態(tài)的修改,state,action 等可自行查看,我們直接看 view
Widget buildView(SettingsState state, Dispatch dispatch, ViewService viewService) {
return Theme(
data: ThemeData(primarySwatch: state.themeColor),
child: Scaffold(
appBar: AppBar(
title: Text(
FlutterI18n.translate(_ctx, I18nKeys.settings),
style: TextStyle(fontSize: SpValues.titleTextSize, fontFamily: state.fontFamily),
),
),
body: ListView(
children: <Widget>[
ExpansionTile(
leading: Icon(Icons.color_lens),
title: Text(
FlutterI18n.translate(_ctx, I18nKeys.themeColor),
style: TextStyle(fontSize: SpValues.settingTextSize, fontFamily: state.fontFamily),
),
children: List.generate(ResourceConfigs.themeColors.length, (index) {
return GestureDetector(
onTap: () {
// 發(fā)送對應(yīng)的修改主題色的 action,effect 根據(jù) action 做出相應(yīng)的響應(yīng)策略
dispatch(SettingsActionCreator.onChangeThemeColor(index));
},
child: Container(
margin: EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 4.0),
width: _size.width,
height: _itemHeight,
color: ResourceConfigs.themeColors[index],
),
);
}),
),
// 省略語言選擇,字體選擇,邏輯同主題色選擇,具體查看 `setting/view.dart` 文件
],
),
),
);
}
/// effect
Effect<SettingsState> buildEffect() {
return combineEffects(<Object, Effect<SettingsState>>{
SettingsAction.changeThemeColor: _onChangeThemeColor,
});
}
void _onChangeThemeColor(Action action, Context<SettingsState> ctx) {
// 通過 GlobalStore dispatch 全局變化的 action,全局的 reducer 做出響應(yīng),并修改主題色
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor(ResourceConfigs.themeColors[action.payload]));
}
別的界面也需要做類似的處理,就可以實現(xiàn)全局切換狀態(tài)啦~
一些小坑
在使用 fish_redux 的過程中,肯定會遇到這樣那樣的坑,這邊簡單列舉幾個遇到的小坑
保持 PageView 子頁面的狀態(tài)
如果不使用 fish_redux 的情況下,PageView 的子頁面我們都需要混入一個 AutomaticKeepAliveClientMixin 來防止頁面重復(fù)刷新的問題,但是在 fish_redux 下,并沒有顯得那么容易,好在官方在 Page 中提供了一個 WidgetWrapper 類型參數(shù),可以方便解決這個問題。首先需要定義一個 WidgetWrapper
class KeepAliveWidget extends StatefulWidget {
final Widget child;
KeepAliveWidget(this.child);
@override
_KeepAliveWidgetState createState() => _KeepAliveWidgetState();
}
class _KeepAliveWidgetState extends State<KeepAliveWidget> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
return widget.child;
}
@override
bool get wantKeepAlive => true;
}
Widget keepAliveWrapper(Widget child) => KeepAliveWidget(child);
定義完成后,在 page 的 wrapper 屬性設(shè)置為 keepAliveWrapper 即可。
PageView 子頁面實現(xiàn)全局狀態(tài)
我們在前面提到了實現(xiàn)全局狀態(tài)的方案,通過設(shè)置 PageRoutres 的 visitor 屬性實現(xiàn),但是設(shè)置完成后,發(fā)現(xiàn) PageView 的子頁面不會跟隨修改,官方也沒有給出原因,那么如何解決呢,其實也很方便,我們定義了全局的 globalUpdate 方法,在 Page 的構(gòu)造中,connectExtraStore 下就可以解決啦
class HomeArticlePage extends Page<HomeArticleState, Map<String, dynamic>> {
HomeArticlePage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<HomeArticleState>(
adapter: null,
slots: <String, Dependent<HomeArticleState>>{},
),
wrapper: keepAliveWrapper, // 實現(xiàn) `PageView` 子頁面狀態(tài)保持,不重復(fù)刷新
) {
// 實現(xiàn) `PageView` 子頁面的全局狀態(tài)
connectExtraStore<GlobalState>(GlobalStore.store, globalUpdate());
}
}
如何實現(xiàn) Dialog 等提示
在 flutter 中,Dialog 等也屬于組件,所以,通過 component 來定義一個 dialog 再合適不過了,比如我們 dispatch 一個 action 需要顯示一個 dialog,那么可以通過如下步驟進行實現(xiàn)
-
定義一個
dialog componentclass DescriptionDialogComponent extends Component<DescriptionDialogState> { DescriptionDialogComponent() : super( effect: buildEffect(), view: buildView, ); } /// view Widget buildView(DescriptionDialogState state, Dispatch dispatch, ViewService viewService) { var _ctx = viewService.context; return AlertDialog( title: Text(FlutterI18n.translate(_ctx, I18nKeys.operatorDescTitle)), content: Text(FlutterI18n.translate(_ctx, I18nKeys.operatorDescContent)), actions: <Widget>[ FlatButton( onPressed: () { dispatch(DescriptionDialogActionCreator.onClose()); }, child: Text( FlutterI18n.translate(_ctx, I18nKeys.dialogPositiveGet), ), ) ], ); } /// effect Effect<DescriptionDialogState> buildEffect() { return combineEffects(<Object, Effect<DescriptionDialogState>>{ DescriptionDialogAction.close: _onClose, }); } void _onClose(Action action, Context<DescriptionDialogState> ctx) { Navigator.of(ctx.context).pop(); } // action,state 省略,具體可以查看 `home\drawer_component\description_component` 在需要展示
dialog的page或者component注冊slots-
在對應(yīng)的
effect調(diào)用showDialog,通過Context.buildComponent生成對應(yīng)的dialog viewvoid _onDescription(Action action, Context<SettingItemState> ctx) { showDialog( barrierDismissible: false, context: ctx.context, // ctx.buildComponent('componentName') 會生成對應(yīng)的 widget builder: (context) => ctx.buildComponent('desc'), // desc 為注冊 dialog 的 slotName ); }
目前遇到的坑都在這,如果大家在使用過程中遇到別的坑,可以放評論一起討論,或者查找 fis_redux 的 issue,很多時候都可以找到滿意的解決方案。