fish_redux
的介紹就不在這廢話了,需要的小夥伴可以直接檢視
fish_redux
官方文檔 ,這裡我們直接通過例子來踩坑。
項目的大概結構如下所示,具體可以檢視
倉庫代碼可以看到
UI
包下充斥着許多的
action
,
effect
reducer
state
view
page
component
adapter
類,不要慌,接下來大概的會說明下每個類的職責。
fish_redux
的分工合作
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
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
在上一步,我們已經定義好了
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
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
資料
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
功能清單
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
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
子頁面的狀态
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
子頁面實作全局狀态
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
等提示
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
,很多時候都可以找到滿意的解決方案。