好久沒更新文章了,最近趁著娃睡覺的功夫,嘗試了下 fish_redux
,這邊做下記錄,安全無毒,小伙伴們可放心食用(本文基于版本 fish_redux 0.3.1
)。
fish_redux
的介紹就不在這廢話了,需要的小伙伴可以直接查看 fish_redux
官方文檔,這里我們直接通過例子來踩坑。
項目的大概結構如下所示,具體可以查看 倉庫代碼
可以看到 UI
包下充斥著許多的 action
,effect
,reducer
,state
,view
,page
,component
,adapter
類,不要慌,接下來大概的會說明下每個類的職責。
fish_redux
的分工合作
-
action
是用來定義一些操作的聲明,其內部包含一個枚舉類XxxAction
和 聲明類XxxActionCreator
,枚舉類用來定義一個操作,ActionCreator
用來定義一個Action
,通過dispatcher
發送對應Action
就可以實現一個操作。例如我們需要打開一個行的頁面,可以如下進行定義enum ExamAction { openNewPage, openNewPageWithParams } class ExamActionCreator { static Action onOpenNewPage(){ // Action 可以傳入一個 payload,例如我們需要攜帶參數跳轉界面,則可以通過 payload 傳遞 // 然后在 effect 或者 reducer 層通過 action.payload 獲取 return const Action(ExamAction.openNewPage); } static Action onOpenNewPageWithParams(String str){ return Action(ExamAction.openNewPageWithParams, payload: str); } }
-
effect
用來定義一些副作用的操作,例如網絡請求,頁面跳轉等,通過buildEffect
方法結合Action
和最終要實現的副作用,例如還是打開頁面的操作,可通過如下方式實現Effect<ExamState> buildEffect() { return combineEffects(<Object, Effect<ExamState>>{ ExamAction.openNewPage: _onOpenNewPage, }); } void _onOpenNewPage(Action action, Context<ExamState> ctx) { Navigator.of(ctx.context).pushNamed('路由地址'); }
-
reducer
用來定義數據發生變化的操作,比如網絡請求后,數據發生了變化,則把原先的數據clone
一份出來,然后把新的值賦值上去,例如有個網絡請求,發生了數據的變化,可通過如下方式實現Reducer<ExamState> buildReducer() { return asReducer( <Object, Reducer<ExamState>>{ HomeAction.onDataRequest: _onDataRequest, }, ); } ExamState _onDataRequest(ExamState state, Action action) { // data 的數據通過 action 的 payload 進行傳遞,reducer 只負責數據刷新 return state.clone()..data = action.payload; }
state
就是當前頁面需要展示的一些數據view
就是當前的UI
展示效果page
和component
就是上述的載體,用來將數據和UI
整合到一起adapter
用來整合列表視圖
Show the code
這邊要實現的例子大概長下面的樣子,一個 Drawer
列表,實現主題色,語言,字體的切換功能,當然后期會增加別的功能,目前先看這部分[home
模塊],基本上涵蓋了上述所有的內容。在寫代碼之前,可以先安裝下 FishRedux
插件,可以快速構建類,直接在插件市場搜索即可
整體配置
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
整體構建
Home
頁面整體就是一個帶 Drawer
,主體是一個 PageView
,頂部帶一個 banner
控件,banner
的數據我們通過網絡進行獲取,在 Drawer
是一個點擊列表,包括圖標,文字和動作,那么我們可以創建一個 DrawerSettingItem
類,用了創建列表,頭部的用戶信息目前可以先寫死。所以我們可以先搭建 HomeState
class HomeState implements Cloneable<HomeState> {
int currentPage; // PageView 的當前項
List<HomeBannerDetail> banners; // 頭部 banner 數據
List<SettingItemState> settings; // Drawer 列表數據
@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 數據
return Action(HomeAction.fetchBanner, payload: banner);
}
static Action onLoadSettings(List<SettingItemState> settings) { // 加載 setting 數據
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);
}
}
構建 banner
為了加強頁面的復用性,可以通過 component
進行模塊構建,具體查看 banner_component
包下文件。首先定義 state
,因為 banner
作為 home
下的內容,所以其 state
不能包含 HomeState
外部的屬性,因此定義如下
class HomeBannerState implements Cloneable<HomeBannerState> {
List<HomeBannerDetail> banners; // banner 數據列表
@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);
}
}
由于不涉及到數據的改變,所以可以不需要定義 reducer
,通過 effect
來處理 openBannerDetail
即可
Effect<HomeBannerState> buildEffect() {
return combineEffects(<Object, Effect<HomeBannerState>>{
// 當收到 openBannerDetail 對應的 Action 的時候,執行對應的方法
HomeBannerAction.openBannerDetail: _onOpenBannerDetail,
});
}
void _onOpenBannerDetail(Action action, Context<HomeBannerState> ctx) {
// payload 中攜帶了 bannerUrl 參數,用來打開對應的網址
// 可查看 [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, // 設置固定高度
child: state.banners == null || state.banners.isEmpty
? SizedBox()
: Swiper( // 當有數據存在時,才顯示 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 對應的 Action,當 effect 或者 reduce 收到會進行對應處理
dispatch(HomeBannerActionCreator.onOpenBannerDetail(state.banners[index].url));
},
);
},
),
);
}
最后再回到 component
,這個類插件已經定義好了,基本上不需要做啥修改
class HomeBannerComponent extends Component<HomeBannerState> {
HomeBannerComponent()
: super(
effect: buildEffect(), // 對應 effect 的方法
reducer: buildReducer(), // 對應 reducer 的方法
view: buildView, // 對應 view 的方法
dependencies: Dependencies<HomeBannerState>(
adapter: null, // 用于展示數據列表
// 組件插槽,注冊后可通過 viewService.buildComponent 方法生成對應組件
slots: <String, Dependent<HomeBannerState>>{},
),
);
}
這樣就定義好了一個 component
,可以通過注冊 slot
方法使用該 component
使用 banner component
在上一步,我們已經定義好了 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 數據的轉換
return HomeBannerState()..banners = state.banners;
}
@override
List factors(HomeState state) {
// factors 為轉換的因子,返回所有改變的因子即可
return state.banners ?? [];
}
}
在 Page
中注冊 slot
page
的結構和 component
的結構是一樣的,使用 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(), // 定義側滑組件,方式同 banner
},
),
middleware: <Middleware<HomeState>>[],
);
}
注冊完成 slot
之后,就可以直接在 view
上使用了,使用的方法也很簡單
Widget buildView(HomeState state, Dispatch dispatch, ViewService viewService) {
var _pageChildren = <Widget>[
// page 轉換成 widget 通過 buildPage 實現,參數表示要傳遞的參數,無需傳遞則為 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
數據
在前面的 HomeActionCreator
中,我們定義了 onFetchBanner
這個 Action
,需要傳入一個 banner
列表作為參數,所以更新數據可以這么進行操作
Effect<HomeState> buildEffect() {
return combineEffects(<Object, Effect<HomeState>>{
// Lifecycle 的生命周期同 StatefulWidget 對應,所以在初始化的時候處理請求 banner 數據等初始化操作
Lifecycle.initState: _onPageInit,
});
}
void _onPageInit(Action action, Context<HomeState> ctx) async {
ctx.dispatch(HomeActionCreator.onPageChange(0));
var banners = await Api().fetchHomeBanner(); // 網絡請求,具體的可以查看 `api.dart` 文件
ctx.dispatch(HomeActionCreator.onFetchBanner(banners)); // 通過 dispatch 發送 Action
}
一開始我們提到過,effect
只負責一些副作用的操作,reducer
負責數據的修改操作,所以在 reducer
需要做數據的刷新
Reducer<HomeState> buildReducer() {
return asReducer(
<Object, Reducer<HomeState>>{
// 當 dispatch 發送了對應的 Action 的時候,就會調用對應方法
HomeAction.fetchBanner: _onFetchBanner,
},
);
}
HomeState _onFetchBanner(HomeState state, Action action) {
// reducer 修改數據方式是先 clone 一份數據,然后進行賦值
// 這樣就把網絡請求返回的數據更新到 view 層了
return state.clone()..banners = action.payload;
}
通過上述操作,就將網絡的 banner
數據加載到 UI
了
使用 adapter
構建 drawer
功能列表
drawer
由一個頭部和列表構成,頭部可以通過 component
進行構建,方法類似上述 banner component
和 drawer component
,唯一區別就是一個在 page
的 slots
注冊,一個在 component
的 slots
注冊。所以構建 drawer
就是需要去構建一個列表,這里就需要用到 adapter
來處理了。
在老的版本中(本文版本 0.3.1),構建 adapter
一般通過 DynamicFlowAdapter
實現,而且在插件中也可以發現,但是在該版本下,DynamicFlowAdapter
已經被標記為過時,并且官方推薦使用 SourceFlowAdapter
。SourceFlowAdapter
需要指定一個 State
,并且該 State
必須繼承自 AdapterSource
。AdapterSource
有兩個子類,分別是可變數據源的 MutableSource
和不可變數據源的 ImmutableSource
,兩者的差別因為官方也沒有給出具體的說明,本文使用 MutableSource
來處理 adapter
。所以對應的 state
定義如下
class HomeDrawerState extends MutableSource implements Cloneable<HomeDrawerState> {
List<SettingItemState> settings; // state 為列表 item component 對應的 state
@override
HomeDrawerState clone() {
return HomeDrawerState()
..settings = settings;
}
@override
Object getItemData(int index) => settings[index]; // 對應 index 下的數據
@override
String getItemType(int index) => DrawerSettingAdapter.settingType; // 對應 index 下的數據類型
@override
int get itemCount => settings?.length ?? 0; // 數據源長度
@override
void setItemData(int index, Object data) => settings[index] = data; // 對應 index 下的數據如何修改
}
同樣,adapter
也可以如下進行定義
class DrawerSettingAdapter extends SourceFlowAdapter<HomeDrawerState> {
static const settingType = 'setting';
DrawerSettingAdapter()
: super(pool: <String, Component<Object>>{
// 不同數據類型,對應的 component 組件,type 和 state getItemType 方法對應
// 允許多種 type
settingType: SettingItemComponent(),
});
}
經過上述兩部分,就定義好了 adapter
的主體部分啦,接著就是要實現 SettingItemComponent
這個組件,只需要簡單的 ListTile
即可,ListTile
的展示內容通過對應的 state
來設置
/// 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),
);
}
因為不涉及數據的修改,所以不需要定義 reducer
,點擊實現通過 effect
實現即可,具體的代碼可查看對應文件,這邊不貼多余代碼了.
經過上述步驟,adapter
就定義完成了,接下來就是要使用對應的 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(),
},
),
);
}
/// 對應 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,
),
)
],
),
);
}
將列表設置到界面后,就剩下最后的數據源了,數據從哪來呢,答案當然是和 banner component
一樣,通過上層獲取,這邊不需要通過網絡獲取,直接在本地定義就行了,具體的獲取查看文件 home\effect.dart
下的 _loadSettingItems
方法,實現和獲取 banner
數據無多大差別,除了一個本地加載,一個網絡獲取。
fish_redux
實現全局狀態
fish_redux
全局狀態的實現,我們參考 官方 demo,首先構造一個 GlobalBaseState
抽象類(涉及到全局狀態變化的 state
都需要繼承該類),這個類定義了全局變化的狀態屬性,例如我們該例中需要實現全局的主題色,語言和字體的改變,那么我們就可以如下定義
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
并實現 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
來存儲狀態值
class GlobalStore {
// Store 用來存儲全局狀態 GlobalState,當刷新狀態值的時候,通過
// store 的 dispatch 發送相關的 action 即可做出相應的調整
static Store<GlobalState> _globalStore;
static Store<GlobalState> get store => _globalStore ??= createStore(
GlobalState(),
buildReducer(), // reducer 用來刷新狀態值
);
}
/// 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
參數,還有個 visitor
參數沒有使用,這個就是用來刷新全局狀態的。
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() 就是具體的實現邏輯
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 對應的屬性賦值給 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 并將數據設置到 ui
}
return pageState;
};
定義好全局 State
和 Store
之后,只需要 PageState
繼承 GlobalBaseState
就可以愉快的全局狀態更新了,例如我們查看 ui/settings
該界面涉及了全局狀態的修改,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: () {
// 發送對應的修改主題色的 action,effect 根據 action 做出相應的響應策略
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 做出響應,并修改主題色
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor(ResourceConfigs.themeColors[action.payload]));
}
別的界面也需要做類似的處理,就可以實現全局切換狀態啦~
一些小坑
在使用 fish_redux
的過程中,肯定會遇到這樣那樣的坑,這邊簡單列舉幾個遇到的小坑
保持 PageView
子頁面的狀態
如果不使用 fish_redux
的情況下,PageView
的子頁面我們都需要混入一個 AutomaticKeepAliveClientMixin
來防止頁面重復刷新的問題,但是在 fish_redux
下,并沒有顯得那么容易,好在官方在 Page
中提供了一個 WidgetWrapper
類型參數,可以方便解決這個問題。首先需要定義一個 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
屬性設置為 keepAliveWrapper
即可。
PageView
子頁面實現全局狀態
我們在前面提到了實現全局狀態的方案,通過設置 PageRoutres
的 visitor
屬性實現,但是設置完成后,發現 PageView
的子頁面不會跟隨修改,官方也沒有給出原因,那么如何解決呢,其實也很方便,我們定義了全局的 globalUpdate
方法,在 Page
的構造中,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, // 實現 `PageView` 子頁面狀態保持,不重復刷新
) {
// 實現 `PageView` 子頁面的全局狀態
connectExtraStore<GlobalState>(GlobalStore.store, globalUpdate());
}
}
如何實現 Dialog
等提示
在 flutter
中,Dialog
等也屬于組件,所以,通過 component
來定義一個 dialog
再合適不過了,比如我們 dispatch
一個 action
需要顯示一個 dialog
,那么可以通過如下步驟進行實現
-
定義一個
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
-
在對應的
effect
調用showDialog
,通過Context.buildComponent
生成對應的dialog view
void _onDescription(Action action, Context<SettingItemState> ctx) { showDialog( barrierDismissible: false, context: ctx.context, // ctx.buildComponent('componentName') 會生成對應的 widget builder: (context) => ctx.buildComponent('desc'), // desc 為注冊 dialog 的 slotName ); }
目前遇到的坑都在這,如果大家在使用過程中遇到別的坑,可以放評論一起討論,或者查找 fis_redux
的 issue
,很多時候都可以找到滿意的解決方案。