好久沒更新文章了鳖眼,最近趁著娃睡覺的功夫惶傻,嘗試了下 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
為了加強頁面的復用性胶滋,可以通過 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
來防止頁面重復刷新的問題驯击,但是在 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)保持琼讽,不重復刷新
) {
// 實現(xiàn) `PageView` 子頁面的全局狀態(tài)
connectExtraStore<GlobalState>(GlobalStore.store, globalUpdate());
}
}
如何實現(xiàn) Dialog
等提示
在 flutter
中必峰,Dialog
等也屬于組件,所以钻蹬,通過 component
來定義一個 dialog
再合適不過了吼蚁,比如我們 dispatch
一個 action
需要顯示一個 dialog
,那么可以通過如下步驟進行實現(xiàn)
-
定義一個
dialog component
class 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 view
void _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
注整,很多時候都可以找到滿意的解決方案能曾。