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
,很多时候都可以找到满意的解决方案。