很久没更新文章了,最近趁着娃睡觉的功夫,尝试了下 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
,不少时候均可以找到满意的解决方案。