说句内心话,这篇文章,来来回回修改了不少次,若是认真看完这篇文章,还不会写fish_redux,请在评论里喷我。react
来学学难搞的fish_redux框架吧,这个框架,官方的文档真是一言难尽,比flutter_bloc官网的文档真是逊色太多了,可是一旦知道怎么写,页面堆起来也是很是爽呀,结构分明,逻辑也会错落有致。android
其实在当时搞懂这个框架的时候,就一直想写一篇文章记录下,可是由于忙(lan),致使一直没写,如今以为仍是必须把使用的过程记录下,毕竟刚上手这个框架是个蛋痛的过程,必需要把这个过程作个记录。ios
这不只仅是记录的文章,文中所给出的示例,也是我从新构思去写的,过程也是力求阐述清楚且详细。git
怎么将Page当作widget使用(BottomNavigationBar,NavigationRail等等导航栏控件会使用到)github
若是你在使用fish_redux的过程当中遇到过上述的问题,那就来看看这篇文章吧!这里,会解答上面全部的问题点!web
fish_redux相关地址数据库
我用的是0.3.X的版本,算是第三版,相对于前几版,改动较大json
fish_redux: ^0.3.4 #演示列表须要用到的库 dio: ^3.0.9 #网络请求框架 json_annotation: ^2.4.0 #json序列化和反序列化用的
此处选择:Page,底下的“Select Fils”所有选择,这是标准的redux文件结构;这边命名建议使用大驼峰:Countredux
建立成功的文件结构api
在写代码前,先看写下流程图,这图是凭着本身的理解画的
经过俩个流程图对比,其中仍是有一些差异的
这边写几个示例,来演示fish_redux的使用
计数器
页面跳转
列表文章
全局模块
全局模式优化
Component使用
开发小技巧
这个例子演示,view中点击此操做,而后更新页面数据;下述的流程,在effect中把数据处理好,经过action中转传递给reducer更新数据
main
///须要使用hide隐藏Page import 'package:flutter/cupertino.dart'hide Page; import 'package:flutter/material.dart' hide Page; void main() { runApp(MyApp()); } Widget createApp() { ///定义路由 final AbstractRoutes routes = PageRoutes( pages: <String, Page<Object, dynamic>>{ "CountPage": CountPage(), }, ); return MaterialApp( title: 'FishDemo', home: routes.buildPage("CountPage", null), //做为默认页面 onGenerateRoute: (RouteSettings settings) { //ios页面切换风格 return CupertinoPageRoute(builder: (BuildContext context) { return routes.buildPage(settings.name, settings.arguments); }) // Material页面切换风格 // return MaterialPageRoute<Object>(builder: (BuildContext context) { // return routes.buildPage(settings.name, settings.arguments); // }); }, ); }
state
class CountState implements Cloneable<CountState> { int count; @override CountState clone() { return CountState()..count = count; } } CountState initState(Map<String, dynamic> args) { return CountState()..count = 0; }
view:这里面就是写界面的模块,buildView里面有三个参数
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) { return _bodyWidget(state, dispatch); } Widget _bodyWidget(CountState state, Dispatch dispatch) { return Scaffold( appBar: AppBar( title: Text("FishRedux"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('You have pushed the button this many times:'), ///使用state中的变量,控住数据的变换 Text(state.count.toString()), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { ///点击事件,调用action 计数自增方法 dispatch(CountActionCreator.countIncrease()); }, child: Icon(Icons.add), ), ); }
action
enum CountAction { increase, updateCount } class CountActionCreator { ///去effect层去处理自增数据 static Action countIncrease() { return Action(CountAction.increase); } ///去reducer层更新数据,传参能够放在Action类中的payload字段中,payload是dynamic类型,可传任何类型 static Action updateCount(int count) { return Action(CountAction.updateCount, payload: count); } }
effect
若是在调用action里面的XxxxActionCreator类中的方法,相应的枚举字段,会在combineEffects中被调用,在这里,咱们就能写相应的方法处理逻辑,方法中带俩个参数:action,ctx
Effect<CountState> buildEffect() { return combineEffects(<Object, Effect<CountState>>{ CountAction.increase: _onIncrease, }); } ///自增数 void _onIncrease(Action action, Context<CountState> ctx) { ///处理自增数逻辑 int count = ctx.state.count + 1; ctx.dispatch(CountActionCreator.updateCount(count)); }
reducer
Reducer<CountState> buildReducer() { return asReducer( <Object, Reducer<CountState>>{ CountAction.updateCount: _updateCount, }, ); } ///通知View层更新界面 CountState _updateCount(CountState state, Action action) { final CountState newState = state.clone(); newState..count = action.payload; return newState; }
从上面的例子看到,如此简单数据变换,仅仅是个state中一个参数自增的过程,effect层就显得有些多余;因此,把流程简化成下面
搞起来
view
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) { return _bodyWidget(state, dispatch); } Widget _bodyWidget(CountState state, Dispatch dispatch) { return Scaffold( appBar: AppBar( title: Text("FishRedux"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('You have pushed the button this many times:'), Text(state.count.toString()), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { ///点击事件,调用action 计数自增方法 dispatch(CountActionCreator.updateCount()); }, child: Icon(Icons.add), ), ); }
action
enum CountAction { updateCount } class CountActionCreator { ///去reducer层更新数据,传参能够放在Action类中的payload字段中,payload是dynamic类型,可传任何类型 static Action updateCount() { return Action(CountAction.updateCount); } }
reducer
Reducer<CountState> buildReducer() { return asReducer( <Object, Reducer<CountState>>{ CountAction.updateCount: _updateCount, }, ); } ///通知View层更新界面 CountState _updateCount(CountState state, Action action) { final CountState newState = state.clone(); newState..count = state.count + 1; return newState; }
从效果图,很容易看到,俩个页面相互传值
main
Widget createApp() { ///定义路由 final AbstractRoutes routes = PageRoutes( pages: <String, Page<Object, dynamic>>{ ///计数器模块演示 "CountPage": CountPage(), ///页面传值跳转模块演示 "FirstPage": FirstPage(), "SecondPage": SecondPage(), }, ); return MaterialApp( title: 'FishRedux', home: routes.buildPage("FirstPage", null), //做为默认页面 onGenerateRoute: (RouteSettings settings) { //ios页面切换风格 return CupertinoPageRoute(builder: (BuildContext context) { return routes.buildPage(settings.name, settings.arguments); }); }, ); }
先来看看该页面的一个流程
state
先写state文件,这边须要定义俩个变量来
class FirstState implements Cloneable<FirstState> { ///传递给下个页面的值 static const String fixedMsg = "\n我是FirstPage页面传递过来的数据:FirstValue"; ///展现传递过来的值 String msg; @override FirstState clone() { return FirstState()..msg = msg; } } FirstState initState(Map<String, dynamic> args) { return FirstState()..msg = "\n暂无"; }
view
Widget buildView(FirstState state, Dispatch dispatch, ViewService viewService) { return _bodyWidget(state, dispatch); } Widget _bodyWidget(FirstState state, Dispatch dispatch) { return Scaffold( appBar: AppBar( title: Text("FirstPage"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('下方数据是SecondPage页面传递过来的:'), Text(state.msg), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { ///跳转到Second页面 dispatch(FirstActionCreator.toSecond()); }, child: Icon(Icons.arrow_forward), ), ); }
action:这里须要定义俩个枚举事件
enum FirstAction { toSecond , updateMsg} class FirstActionCreator { ///跳转到第二个页面 static Action toSecond() { return const Action(FirstAction.toSecond); } ///拿到第二个页面返回的数据,执行更新数据操做 static Action updateMsg(String msg) { return Action(FirstAction.updateMsg, payload: msg); } }
effect
/// 使用hide方法,隐藏系统包里面的Action类 import 'package:flutter/cupertino.dart' hide Action; Effect<FirstState> buildEffect() { return combineEffects(<Object, Effect<FirstState>>{ FirstAction.toSecond: _toSecond, }); } void _toSecond(Action action, Context<FirstState> ctx) async{ ///页面之间传值;这地方必须写个异步方法,等待上个页面回传过来的值;as关键字是类型转换 var result = await Navigator.of(ctx.context).pushNamed("SecondPage", arguments: {"firstValue": FirstState.fixedMsg}); ///获取到数据,更新页面上的数据 ctx.dispatch(FirstActionCreator.updateMsg( (result as Map)["secondValue"]) ); }
reducer
Reducer<FirstState> buildReducer() { return asReducer( <Object, Reducer<FirstState>>{ FirstAction.updateMsg: _updateMsg, }, ); } FirstState _updateMsg(FirstState state, Action action) { return state.clone()..msg = action.payload; }
这个页面比较简单,后续不涉及到页面数据更新,因此reducer模块能够不写,看看该页面的流程
state
class SecondState implements Cloneable<SecondState> { ///传递给下个页面的值 static const String fixedMsg = "\n我是SecondPage页面传递过来的数据:SecondValue"; ///展现传递过来的值 String msg; @override SecondState clone() { return SecondState()..msg = msg; } } SecondState initState(Map<String, dynamic> args) { ///获取上个页面传递过来的数据 return SecondState()..msg = args["firstValue"]; }
view
Widget buildView(SecondState state, Dispatch dispatch, ViewService viewService) { return WillPopScope( child: _bodyWidget(state), onWillPop: () { dispatch(SecondActionCreator.backFirst()); ///true:表示执行页面返回 false:表示不执行返回页面操做,这里由于要传值,因此接管返回操做 return Future.value(false); }, ); } Widget _bodyWidget(SecondState state) { return Scaffold( appBar: AppBar( title: Text("SecondPage"), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('下方数据是FirstPage页面传递过来的:'), Text(state.msg), ], ), ), ); }
enum SecondAction { backFirst } class SecondActionCreator { ///返回到第一个页面,而后从栈中移除自身,同时传回去一些数据 static Action backFirst() { return Action(SecondAction.backFirst); } }
effect
///隐藏系统包中的Action类 import 'package:flutter/cupertino.dart' hide Action; Effect<SecondState> buildEffect() { return combineEffects(<Object, Effect<SecondState>>{ SecondAction.backFirst: _backFirst, }); } void _backFirst(Action action, Context<SecondState> ctx) { ///pop当前页面,而且返回相应的数据 Navigator.pop(ctx.context, {"secondValue": SecondState.fixedMsg}); }
理解了上面俩个案例,相信你可使用fish_redux实现一部分页面了;可是,咱们堆页面的过程当中,能体会列表模块是很是重要的一部分,如今就来学学,在fish_redux中怎么使用ListView吧!
效果图对于列表的滚动,作了俩个操做:一个是拖拽列表;另外一个是滚动鼠标的滚轮。flutter对鼠标触发的相关事件也支持的愈来愈好了!
main
这边改动很是小,只在路由里,新增了:GuidePage,ListPage;同时将home字段中的默认页面,改为了:GuidePage页面;导航页面代码就不贴在文章里了,下面贴下该页面连接
void main() { runApp(createApp()); } Widget createApp() { ///定义路由 final AbstractRoutes routes = PageRoutes( pages: <String, Page<Object, dynamic>>{ ///导航页面 "GuidePage": GuidePage(), ///计数器模块演示 "CountPage": CountPage(), ///页面传值跳转模块演示 "FirstPage": FirstPage(), "SecondPage": SecondPage(), ///列表模块演示 "ListPage": ListPage(), }, ); return MaterialApp( title: 'FishRedux', home: routes.buildPage("GuidePage", null), //做为默认页面 onGenerateRoute: (RouteSettings settings) { //ios页面切换风格 return CupertinoPageRoute(builder: (BuildContext context) { return routes.buildPage(settings.name, settings.arguments); }); }, ); }
Adapter实现的流程
总流程:初始化列表模块 ---> item模块 ---> 列表模块逻辑完善
初始化列表模块
item模块
列表模块逻辑完善:俩地方分俩步(adapter建立及其绑定,正常page页面编辑)
按照流程走
准备工做
建立bean实体
根据api返回的json数据,生成相应的实体
json转实体
这地方生成了:ItemDetailBean;代码俩百多行就不贴了,具体的内容,点击下面连接
建立item模块
文件结构
OK,bean文件搞定了,再来看看,item文件中的文件,这里component文件不须要改动,因此这地方,咱们只须要看:state.dart,view.dart
state
import 'package:fish_redux/fish_redux.dart'; import 'package:fish_redux_demo/list/bean/item_detail_bean.dart'; class ItemState implements Cloneable<ItemState> { Datas itemDetail; ItemState({this.itemDetail}); @override ItemState clone() { return ItemState() ..itemDetail = itemDetail; } } ItemState initState(Map<String, dynamic> args) { return ItemState(); }
view
这里item布局稍稍有点麻烦,总体上采用的是:水平布局(Row),分左右俩大块
Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) { return _bodyWidget(state); } Widget _bodyWidget(ItemState state) { return Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), elevation: 5, margin: EdgeInsets.only(left: 20, right: 20, top: 20), child: Row( children: <Widget>[ //左边图片 Container( margin: EdgeInsets.all(10), width: 180, height: 100, child: Image.network( state.itemDetail.envelopePic, fit: BoxFit.fill, ), ), //右边的纵向布局 _rightContent(state), ], ), ); } ///item中右边的纵向布局,比例布局 Widget _rightContent(ItemState state) { return Expanded( child: Container( margin: EdgeInsets.all(10), height: 120, child: Column( mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ //标题 Expanded( flex: 2, child: Container( alignment: Alignment.centerLeft, child: Text( state.itemDetail.title, style: TextStyle(fontSize: 16), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), //内容 Expanded( flex: 4, child: Container( alignment: Alignment.centerLeft, child: Text( state.itemDetail.desc, style: TextStyle(fontSize: 12), maxLines: 3, overflow: TextOverflow.ellipsis, ), )), Expanded( flex: 3, child: Column( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ //做者 Row( children: <Widget>[ Text("做者:", style: TextStyle(fontSize: 12)), Expanded( child: Text(state.itemDetail.author, style: TextStyle(color: Colors.blue, fontSize: 12), overflow: TextOverflow.ellipsis), ) ], ), //时间 Row(children: <Widget>[ Text("时间:", style: TextStyle(fontSize: 12)), Expanded( child: Text(state.itemDetail.niceDate, style: TextStyle(color: Colors.blue, fontSize: 12), overflow: TextOverflow.ellipsis), ) ]) ], ), ), ], ), )); }
item模块,就这样写完了,不须要改动什么了,接下来看看List模块
首先最重要的,咱们须要将adapter创建起来,并和page绑定
adapter建立及其绑定
建立adapter
class ListItemAdapter extends SourceFlowAdapter<ListState> { static const String item_style = "project_tab_item"; ListItemAdapter() : super( pool: <String, Component<Object>>{ ///定义item的样式 item_style: ItemComponent(), }, ); }
state调整
class ListState extends MutableSource implements Cloneable<ListState> { ///这地方必定要注意,List里面的泛型,须要定义为ItemState ///怎么更新列表数据,只须要更新这个items里面的数据,列表数据就会相应更新 ///使用多样式,请写出 List<Object> items; List<ItemState> items; @override ListState clone() { return ListState()..items = items; } ///使用上面定义的List,继承MutableSource,就把列表和item绑定起来了 @override Object getItemData(int index) => items[index]; @override String getItemType(int index) => ListItemAdapter.item_style; @override int get itemCount => items.length; @override void setItemData(int index, Object data) { items[index] = data; } } ListState initState(Map<String, dynamic> args) { return ListState(); }
page中绑定adapter
class ListPage extends Page<ListState, Map<String, dynamic>> { ListPage() : super( initState: initState, effect: buildEffect(), reducer: buildReducer(), view: buildView, dependencies: Dependencies<ListState>( ///绑定Adapter adapter: NoneConn<ListState>() + ListItemAdapter(), slots: <String, Dependent<ListState>>{}), middleware: <Middleware<ListState>>[], ); }
正常page页面编辑
总体流程
view
Widget buildView(ListState state, Dispatch dispatch, ViewService viewService) { return Scaffold( appBar: AppBar( title: Text("ListPage"), ), body: _itemWidget(state, viewService), ); } Widget _itemWidget(ListState state, ViewService viewService) { if (state.items != null) { ///使用列表 return ListView.builder( itemBuilder: viewService.buildAdapter().itemBuilder, itemCount: viewService.buildAdapter().itemCount, ); } else { return Center( child: CircularProgressIndicator(), ); } }
action
enum ListAction { updateItem } class ListActionCreator { static Action updateItem(var list) { return Action(ListAction.updateItem, payload: list); } }
effect
Effect<ListState> buildEffect() { return combineEffects(<Object, Effect<ListState>>{ ///进入页面就执行的初始化操做 Lifecycle.initState: _init, }); } void _init(Action action, Context<ListState> ctx) async { String apiUrl = "https://www.wanandroid.com/project/list/1/json"; Response response = await Dio().get(apiUrl); ItemDetailBean itemDetailBean = ItemDetailBean.fromJson(json.decode(response.toString())); List<Datas> itemDetails = itemDetailBean.data.datas; ///构建符合要求的列表数据源 List<ItemState> items = List.generate(itemDetails.length, (index) { return ItemState(itemDetail: itemDetails[index]); }); ///通知更新列表数据源 ctx.dispatch(ListActionCreator.updateItem(items)); }
reducer
Reducer<ListState> buildReducer() { return asReducer( <Object, Reducer<ListState>>{ ListAction.updateItem: _updateItem, }, ); } ListState _updateItem(ListState state, Action action) { return state.clone()..items = action.payload; }
此次列表模块是很是的简单,基本不涉及什么流程,就是最基本初始化的一个过程,将state里初始化的数据在view中展现
state
class ListEditState extends MutableSource implements Cloneable<ListEditState> { List<ItemState> items; @override ListEditState clone() { return ListEditState()..items = items; } @override Object getItemData(int index) => items[index]; @override String getItemType(int index) => ListItemAdapter.itemName; @override int get itemCount => items.length; @override void setItemData(int index, Object data) { items[index] = data; } } ListEditState initState(Map<String, dynamic> args) { return ListEditState() ..items = [ ItemState(id: 1, title: "列表Item-1", itemStatus: false), ItemState(id: 2, title: "列表Item-2", itemStatus: false), ItemState(id: 3, title: "列表Item-3", itemStatus: false), ItemState(id: 4, title: "列表Item-4", itemStatus: false), ItemState(id: 5, title: "列表Item-5", itemStatus: false), ItemState(id: 6, title: "列表Item-6", itemStatus: false), ]; }
view
Widget buildView(ListEditState state, Dispatch dispatch, ViewService viewService) { return Scaffold( appBar: AppBar( title: Text("ListEditPage"), ), body: ListView.builder( itemBuilder: viewService.buildAdapter().itemBuilder, itemCount: viewService.buildAdapter().itemCount, ), ); }
adapter
class ListItemAdapter extends SourceFlowAdapter<ListEditState> { static const String itemName = "item"; ListItemAdapter() : super( pool: <String, Component<Object>>{itemName: ItemComponent()}, ); }
page
class ListEditPage extends Page<ListEditState, Map<String, dynamic>> { ListEditPage() : super( initState: initState, view: buildView, dependencies: Dependencies<ListEditState>( ///绑定适配器 adapter: NoneConn<ListEditState>() + ListItemAdapter(), slots: <String, Dependent<ListEditState>>{}), middleware: <Middleware<ListEditState>>[], ); }
接下就是比较重要的item模块了,item模块的流程,也是很是的清晰
state
class ItemState implements Cloneable<ItemState> { int id; String title; bool itemStatus; ItemState({this.id, this.title, this.itemStatus}); @override ItemState clone() { return ItemState() ..title = title ..itemStatus = itemStatus ..id = id; } } ItemState initState(Map<String, dynamic> args) { return ItemState(); }
view
Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) { return Container( child: InkWell( onTap: () {}, child: ListTile( title: Text(state.title), trailing: Checkbox( value: state.itemStatus, ///Checkbox的点击操做:状态变动 onChanged: (value) => dispatch(ItemActionCreator.onChange(state.id)), ), ), ), ); }
action
enum ItemAction { onChange } class ItemActionCreator { //状态改变 static Action onChange(int id) { return Action(ItemAction.onChange, payload: id); } }
reducer
Reducer<ItemState> buildReducer() { return asReducer( <Object, Reducer<ItemState>>{ ItemAction.onChange: _onChange, }, ); } ItemState _onChange(ItemState state, Action action) { if (state.id == action.payload) { return state.clone()..itemStatus = !state.itemStatus; } ///这地方必定要注意,要返回:state;不能返回:state.clone(),不然会形成后续更新失灵 return state; }
注意:若是使用多样式,items的列表泛型不要写成ItemState,写成Object就好了;在下面代码,咱们能够看到,实现的getItemData()方法返回的类型是Object,因此Items的列表泛型写成Object,是彻底能够的。
假设一种状况,在index是奇数时展现:OneComponent;在index是奇数时展现:TwoComponent;
下述代码可作思路参考
class ListState extends MutableSource implements Cloneable<PackageCardState> { List<Object> items; @override ListState clone() { return PackageCardState()..items = items; } @override Object getItemData(int index) => items[index]; @override String getItemType(int index) { if(items[index] is OneState) { return PackageCardAdapter.itemStyleOne; }else{ return PackageCardAdapter.itemStyleTwo; } } @override int get itemCount => items.length; @override void setItemData(int index, Object data) => items[index] = data; }
这里搞定了单item刷新场景,还存在一种多item刷新的场景
举例:假设一种场景,对于上面的item只能单选,一个item项被选中,其它item状态被重置到未选状态,具体效果看下方效果图
下述代码为总体流程
Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) { return InkWell( onTap: () {}, child: ListTile( title: Text(state.title), trailing: Checkbox( value: state.itemStatus, ///CheckBox的点击操做:状态变动 onChanged: (value) { //单选模式,清除选中的item,以便作单选 dispatch(ItemActionCreator.clear()); //刷新选中item dispatch(ItemActionCreator.onChange(state.id)); } ), ), ); }
enum ItemAction { onChange, clear, } class ItemActionCreator { //状态改变 static Action onChange(int id) { return Action(ItemAction.onChange, payload: id); } //清除改变的状态 static Action clear() { return Action(ItemAction.clear); } }
Reducer<ItemState> buildReducer() { return asReducer( <Object, Reducer<ItemState>>{ ItemAction.onChange: _onChange, ItemAction.clear: _clear, }, ); } ItemState _onChange(ItemState state, Action action) { if (state.id == action.payload) { return state.clone()..itemStatus = !state.itemStatus; } ///这地方必定要注意,要返回:state;不能返回:state.clone(),不然会形成后续更新失灵 return state; } ///单选模式 ItemState _clear(ItemState state, Action action) { if (state.itemStatus) { return state.clone()..itemStatus = false; } ///这地方必定要注意,要返回:state;不能返回:state.clone(),不然会形成后续更新失灵 return state; }
这个问题实际上解决起来很简单,可是若是一直在 _onChange 方法重置状态,你会发现和你预期的结果一直对不上;完整且详细的效果,能够去看demo里面代码
文件结构
state
abstract class GlobalBaseState{ Color themeColor; } class GlobalState implements GlobalBaseState, Cloneable<GlobalState>{ @override Color themeColor; @override GlobalState clone() { return GlobalState(); } }
action
enum GlobalAction { changeThemeColor } class GlobalActionCreator{ static Action onChangeThemeColor(){ return const Action(GlobalAction.changeThemeColor); } }
reducer
import 'package:flutter/material.dart' hide Action; Reducer<GlobalState> buildReducer(){ return asReducer( <Object, Reducer<GlobalState>>{ GlobalAction.changeThemeColor: _onChangeThemeColor, }, ); } List<Color> _colors = <Color>[ Colors.green, Colors.red, Colors.black, Colors.blue ]; GlobalState _onChangeThemeColor(GlobalState state, Action action) { final Color next = _colors[((_colors.indexOf(state.themeColor) + 1) % _colors.length)]; return state.clone()..themeColor = next; }
store
/// 创建一个AppStore /// 目前它的功能只有切换主题 class GlobalStore{ static Store<GlobalState> _globalStore; static Store<GlobalState> get store => _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer()); }
这里面将PageRoutes里面的visitor字段使用起来,状态更新操做代码有点多,就单独提出来了;因此main文件里面,增长了:
void main() { runApp(createApp()); } Widget createApp() { ///全局状态更新 _updateState() { return (Object pageState, GlobalState appState) { final GlobalBaseState p = pageState; if (pageState is Cloneable) { final Object copy = pageState.clone(); final GlobalBaseState newState = copy; if (p.themeColor != appState.themeColor) { newState.themeColor = appState.themeColor; } /// 返回新的 state 并将数据设置到 ui return newState; } return pageState; }; } final AbstractRoutes routes = PageRoutes( ///全局状态管理:只有特定的范围的Page(State继承了全局状态),才须要创建和 AppStore 的链接关系 visitor: (String path, Page<Object, dynamic> page) { if (page.isTypeof<GlobalBaseState>()) { ///创建AppStore驱动PageStore的单向数据链接: 参数1 AppStore 参数2 当AppStore.state变化时,PageStore.state该如何变化 page.connectExtraStore<GlobalState>(GlobalStore.store, _updateState()); } }, ///定义路由 pages: <String, Page<Object, dynamic>>{ ///导航页面 "GuidePage": GuidePage(), ///计数器模块演示 "CountPage": CountPage(), ///页面传值跳转模块演示 "FirstPage": FirstPage(), "SecondPage": SecondPage(), ///列表模块演示 "ListPage": ListPage(), }, ); return MaterialApp( title: 'FishRedux', home: routes.buildPage("GuidePage", null), //做为默认页面 onGenerateRoute: (RouteSettings settings) { //ios页面切换风格 return CupertinoPageRoute(builder: (BuildContext context) { return routes.buildPage(settings.name, settings.arguments); }); }, ); }
state
class CountState implements Cloneable<CountState>,GlobalBaseState { int count; @override CountState clone() { return CountState()..count = count; } @override Color themeColor; } CountState initState(Map<String, dynamic> args) { return CountState()..count = 0; }
view
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) { return _bodyWidget(state, dispatch); } Widget _bodyWidget(CountState state, Dispatch dispatch) { return Scaffold( appBar: AppBar( title: Text("FishRedux"), ///全局主题,仅仅在此处改动了一行 backgroundColor: state.themeColor, ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('You have pushed the button this many times:'), Text(state.count.toString()), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { ///点击事件,调用action 计数自增方法 dispatch(CountActionCreator.updateCount()); }, child: Icon(Icons.add), ), ); }
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());
在上面的全局模式里说了,使用全局模块,前期须要规划好字段,否则项目进行到中期的时候,想添加字段,多个模块的State会出现大范围爆红,提示去实现你添加的字段;项目开始规划好全部的字段,显然这须要全面的考虑好大部分场景,可是人的灵感老是无限的,不改代码是不可能,这辈子都不可能。只能想办法看能不能添加一次字段后,后期添加字段,并不会引发其余模块爆红,试了屡次,成功的使用中间实体,来解决该问题
这里优化俩个方面
使用通用的全局实体
将路由模块和全局模块封装
由于使用中间实体,有一些地方会出现空指针问题,我都在流程里面写清楚了,你们能够把优化流程完整看一遍哈,都配置好,后面拓展使用就不会报空指针了
main:大改
void main() { runApp(createApp()); } Widget createApp() { return MaterialApp( title: 'FishRedux', home: RouteConfig.routes.buildPage(RouteConfig.guidePage, null), //做为默认页面 onGenerateRoute: (RouteSettings settings) { //ios页面切换风格 return CupertinoPageRoute(builder: (BuildContext context) { return RouteConfig.routes.buildPage(settings.name, settings.arguments); }); }, ); } ///路由管理 class RouteConfig { ///定义你的路由名称好比 static final String routeHome = 'page/home'; ///导航页面 static const String guidePage = 'page/guide'; ///计数器页面 static const String countPage = 'page/count'; ///页面传值跳转模块演示 static const String firstPage = 'page/first'; static const String secondPage = 'page/second'; ///列表模块演示 static const String listPage = 'page/list'; static const String listEditPage = 'page/listEdit'; static final AbstractRoutes routes = PageRoutes( pages: <String, Page<Object, dynamic>>{ ///将你的路由名称和页面映射在一块儿,好比:RouteConfig.homePage : HomePage(), RouteConfig.guidePage: GuidePage(), RouteConfig.countPage: CountPage(), RouteConfig.firstPage: FirstPage(), RouteConfig.secondPage: SecondPage(), RouteConfig.listPage: ListPage(), RouteConfig.listEditPage: ListEditPage(), }, visitor: StoreConfig.visitor, ); } ///全局模式 class StoreConfig { ///全局状态管理 static _updateState() { return (Object pageState, GlobalState appState) { final GlobalBaseState p = pageState; if (pageState is Cloneable) { final Object copy = pageState.clone(); final GlobalBaseState newState = copy; if (p.store == null) { ///这地方的判断是必须的,判断第一次store对象是否为空 newState.store = appState.store; } else { /// 这地方增长字段判断,是否须要更新 if ((p.store.themeColor != appState.store.themeColor)) { newState.store.themeColor = appState.store.themeColor; } /// 若是增长字段,同理上面的判断而后赋值... } /// 返回新的 state 并将数据设置到 ui return newState; } return pageState; }; } static visitor(String path, Page<Object, dynamic> page) { if (page.isTypeof<GlobalBaseState>()) { ///创建AppStore驱动PageStore的单向数据链接 ///参数1 AppStore 参数2 当AppStore.state变化时,PageStore.state该如何变化 page.connectExtraStore<GlobalState>(GlobalStore.store, _updateState()); } } }
下面俩个模块是须要改动代码的模块
state
abstract class GlobalBaseState{ StoreModel store; } class GlobalState implements GlobalBaseState, Cloneable<GlobalState>{ @override GlobalState clone() { return GlobalState(); } @override StoreModel store = StoreModel( /// store这个变量,在这必须示例化,否则引用该变量中的字段,会报空指针 /// 下面的字段,赋初值,就是初始时展现的全局状态 /// 这地方初值,理应从缓存或数据库中取,代表用户选择的全局状态 themeColor: Colors.lightBlue ); } ///中间全局实体 ///须要增长字段就在这个实体里面添加就好了 class StoreModel { Color themeColor; StoreModel({this.themeColor}); }
reducer
Reducer<GlobalState> buildReducer(){ return asReducer( <Object, Reducer<GlobalState>>{ GlobalAction.changeThemeColor: _onChangeThemeColor, }, ); } List<Color> _colors = <Color>[ Colors.green, Colors.red, Colors.black, Colors.blue ]; GlobalState _onChangeThemeColor(GlobalState state, Action action) { final Color next = _colors[((_colors.indexOf(state.store.themeColor) + 1) % _colors.length)]; return state.clone()..store.themeColor = next; }
下面俩个模块代码没有改动,可是为了思路完整,一样贴出来
enum GlobalAction { changeThemeColor } class GlobalActionCreator{ static Action onChangeThemeColor(){ return const Action(GlobalAction.changeThemeColor); } }
class GlobalStore{ static Store<GlobalState> _globalStore; static Store<GlobalState> get store => _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer()); }
state
class CountState implements Cloneable<CountState>, GlobalBaseState { int count; @override CountState clone() { return CountState() ..count = count ..store = store; } @override StoreModel store; } CountState initState(Map<String, dynamic> args) { return CountState()..count = 0; }
view
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) { return _bodyWidget(state, dispatch); } Widget _bodyWidget(CountState state, Dispatch dispatch) { return Scaffold( appBar: AppBar( title: Text("FishRedux"), ///全局主题,仅仅在此处改动了一行 backgroundColor: state.store.themeColor, ), ///下面其他代码省略.... }
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());
经过上面的优化,使用体验提高不是一个级别,大大提高的全局模式的扩展性,咱们就算后期增长了大量的全局字段,也能够一个个模块慢慢改,不用一次爆肝全改完,猝死的几率又大大减小了!
Component是个比较经常使用的模块,上面使用列表的时候,就使用到了Component,此次咱们来看看,在页面中直接使用Component,可插拔式使用!Component的使用总的来讲是比较简单了,比较关键的是在State中创建起链接。
这地方写了一个Component,代码很简单,来看看吧
这地方代码是自动生成了,没有任何改动,就不贴了
state
class AreaState implements Cloneable<AreaState> { String title; String text; Color color; AreaState({ this.title = "", this.color = Colors.blue, this.text = "", }); @override AreaState clone() { return AreaState() ..color = color ..text = text ..title = title; } }
Widget buildView( AreaState state, Dispatch dispatch, ViewService viewService) { return Scaffold( appBar: AppBar( title: Text(state.title), automaticallyImplyLeading: false, ), body: Container( height: double.infinity, width: double.infinity, alignment: Alignment.center, color: state.color, child: Text(state.text), ), ); }
CompPage中,没用到effete这层,就没建立该文件,老规矩,先看看state
state
computed():该方法是必须实现的,这个相似直接的get()方法,可是切记不能像get()直接返回state.leftAreaState()或state.rightAreaState,某些场景初始化没法刷新,由于是同一个对象,会被判断未更改,因此会不刷新控件
class CompState implements Cloneable<CompState> { AreaState leftAreaState; AreaState rightAreaState; @override CompState clone() { return CompState() ..rightAreaState = rightAreaState ..leftAreaState = leftAreaState; } } CompState initState(Map<String, dynamic> args) { ///初始化数据 return CompState() ..rightAreaState = AreaState( title: "LeftAreaComponent", text: "LeftAreaComponent", color: Colors.indigoAccent, ) ..leftAreaState = AreaState( title: "RightAreaComponent", text: "RightAreaComponent", color: Colors.blue, ); } ///左边Component链接器 class LeftAreaConnector extends ConnOp<CompState, AreaState> with ReselectMixin<CompState, AreaState> { @override AreaState computed(CompState state) { return state.leftAreaState.clone(); } @override void set(CompState state, AreaState subState) { state.leftAreaState = subState; } } ///右边Component链接器 class RightAreaConnector extends ConnOp<CompState, AreaState> with ReselectMixin<CompState, AreaState> { @override AreaState computed(CompState state) { return state.rightAreaState.clone(); } @override void set(CompState state, AreaState subState) { state.rightAreaState = subState; } }
page
class CompPage extends Page<CompState, Map<String, dynamic>> { CompPage() : super( initState: initState, reducer: buildReducer(), view: buildView, dependencies: Dependencies<CompState>( adapter: null, slots: <String, Dependent<CompState>>{ //绑定Component "leftArea": LeftAreaConnector() + AreaComponent(), "rightArea": RightAreaConnector() + AreaComponent(), }), middleware: <Middleware<CompState>>[], ); }
view
Widget buildView(CompState state, Dispatch dispatch, ViewService viewService) { return Container( color: Colors.white, child: Column( children: [ ///Component组件部分 Expanded( flex: 3, child: Row( children: [ Expanded(child: viewService.buildComponent("leftArea")), Expanded(child: viewService.buildComponent("rightArea")), ], ), ), ///按钮 Expanded( flex: 1, child: Center( child: RawMaterialButton( fillColor: Colors.blue, shape: StadiumBorder(), onPressed: () => dispatch(CompActionCreator.change()), child: Text("改变"), ), )) ], ), ); }
enum CompAction { change } class CompActionCreator { static Action change() { return const Action(CompAction.change); } }
Reducer<CompState> buildReducer() { return asReducer( <Object, Reducer<CompState>>{ CompAction.change: _change, }, ); } CompState _change(CompState state, Action action) { final CompState newState = state.clone(); //改变leftAreaComponent中state newState.leftAreaState.text = "LeftAreaState:${Random().nextInt(1000)}"; newState.leftAreaState.color = Color.fromRGBO(randomColor(), randomColor(), randomColor(), 1); //改变rightAreaComponent中state newState.rightAreaState.text = "RightAreaState:${Random().nextInt(1000)}"; newState.rightAreaState.color = Color.fromRGBO(randomColor(), randomColor(), randomColor(), 1); return newState; } int randomColor() { return Random().nextInt(255); }
总的来讲,Component的使用仍是比较简单的;若是咱们把某个复杂的列表提炼出一个Component的,很明显有个初始化的过程,这里咱们须要将:请求参数调体或列表详情操做,在page页面处理好,而后再更新给咱们绑定的子Component的State,这样就能起到初始化某个模块的做用;至于刷新,下拉等后续操做,就让Component内部本身去处理了
广播在复杂的业务场景,可以起到很是巨大的做用,能很是轻松使用跨页面交互,跨Component交互!
fish_redux中是带有广播的通讯方式,使用的方式很简单,这本是effect层,ctx参数自带的一个api,这里介绍一下
说明:请注意广播能够通知任何页面的枚举方法,你能够单独写一个枚举事件,也能够不写,直接使用某个页面的枚举事件,是彻底能够
action
enum BroadcastAction { toNotify } class BroadcastActionCreator { ///广播通知 static Action toNotify(String msg) { return Action(BroadcastAction.toNotify, payload: msg); } }
发送广播
void _backFirst(Action action, Context<SecondState> ctx) { //广播通讯 ctx.broadcast(BroadcastActionCreator.toNotify("页面二发送广播通知")); }
Effect<FirstState> buildEffect() { return combineEffects(<Object, Effect<FirstState>>{ //接受发送的广播消息 BroadcastAction.toNotify: _receiveNotify, }); } void _receiveNotify(Action action, Context<FirstState> ctx) async { ///接受广播 print("跳转一页面:${action.payload}"); }
广播的使用仍是挺简单的,基本和dispatch的使用是一致的,dispatch是模块的,而broadcast是处于Page或Component都能进行通讯交互,不少状况下,咱们在一个页面进行了操做,其余页面也须要同步作一些处理,使用广播就很简单了
注意: 广播发送和接受是一对多的关系,一处发送,能够在多处接受;和dispatch发送事件,若是在effect里面接受,在reducer就没法接受的状况是不同的(被拦截了)
无限弱化了reducer层做用
Reducer<TestState> buildReducer() { return asReducer( <Object, Reducer<TestState>>{ TestAction.onRefresh: _onRefresh, }, ); } TestState _onRefresh(TreeState state, Action action) { return state.clone(); }
说明
这种开发形式,能够说是个惯例,在android里面是封装一个个View,View里有对应的一套,逻辑自洽的功能,而后在主xm里面组合这些View;这种思想彻底能够引伸到Flutter里,并且,开发体验更上几百层楼,让你的widget组合能够更加灵活百变,百变星君
为何用widget组合方式构造页面?
组合widget关键点
解决方案
状态管理