好久沒更新文章了,最近趁著娃睡覺的功夫,嘗試了下 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
,很多時候都可以找到滿意的解決方案。