很久沒更新文章了,最近趁着娃睡覺的功夫,嘗試了下 fish_redux
,這邊作下記錄,安全無毒,小夥伴們可放心食用(本文基於版本 fish_redux 0.3.1
)。android
fish_redux
的介紹就不在這廢話了,須要的小夥伴能夠直接查看 fish_redux
官方文檔,這裏咱們直接經過例子來踩坑。git
項目的大概結構以下所示,具體能夠查看 倉庫代碼github
能夠看到 UI
包下充斥着許多的 action
,effect
,reducer
,state
,view
,page
,component
,adapter
類,不要慌,接下來大概的會說明下每一個類的職責。redux
fish_redux
的分工合做action
是用來定義一些操做的聲明,其內部包含一個枚舉類 XxxAction
和 聲明類 XxxActionCreator
,枚舉類用來定義一個操做,ActionCreator
用來定義一個 Action
,經過 dispatcher
發送對應 Action
就能夠實現一個操做。例如咱們須要打開一個行的頁面,能夠以下進行定義api
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
一份出來,而後把新的值賦值上去,例若有個網絡請求,發生了數據的變化,可經過以下方式實現markdown
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
展現效果app
page
和 component
就是上述的載體,用來將數據和 UI
整合到一塊兒async
adapter
用來整合列表視圖
這邊要實現的例子大概長下面的樣子,一個 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);
}
}
複製代碼
爲了增強頁面的複用性,能夠經過 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
,不少時候均可以找到滿意的解決方案。