说句心里话,这篇文章,来来回回批改了很屡次,如果认真看完这篇文章,还不会写fish_redux,请在评论里喷我。
前言
来学学难搞的fish_redux框架吧,这个框架,官网的文档真是一言难尽,比flutter_bloc官网的文档真是逊色太多了,然而一旦晓得怎么写,页面堆起来也是十分爽呀,构造明显,逻辑也会错落有致。
其实在过后搞懂这个框架的时候,就始终想写一篇文章记录下,然而因为忙(lan),导致始终没写,当初感觉还是必须把应用的过程记录下,毕竟刚上手这个框架是个蛋痛的过程,必须要把这个过程做个记录。
这不仅仅是记录的文章,文中所给出的示例,也是我从新构思去写的,过程也是力求论述分明且具体。
几个问题点
- 页面切换的转场动画
- 页面怎么更新数据
- fish_redux各个模块之间,怎么传递数据
- 页面跳转传值,及其承受下个页面回传的值
- 怎么配合ListView应用
- ListView怎么应用adapter,数据怎么和item绑定
怎么将Page当做widget应用(BottomNavigationBar,NavigationRail等等导航栏控件会应用到)
- 这个间接应用:XxxPage.buildPage(null) 即可
如果你在应用fish_redux的过程中遇到过上述的问题,那就来看看这篇文章吧!这里,会解答下面所有的问题点!
筹备
引入
fish_redux相干地址
- GitHub地址:https://github.com/alibaba/fish-redux
- Pub地址:https://pub.dev/packages/fish_redux
我用的是0.3.X的版本,算是第三版,绝对于前几版,改变较大
- 引入fish_redux插件,想用最新版插件,可进入pub地址外面查看
fish_redux: ^0.3.4#演示列表须要用到的库dio: ^3.0.9 #网络申请框架json_annotation: ^2.4.0 #json序列化和反序列化用的
开发插件
- 此处咱们须要装置代码生成插件,能够帮咱们生成大量文件和模板代码
- 在Android Studio外面搜寻”fish“就能搜出插件了,插件名叫:FishReduxTemplate
- BakerJQ编写:Android Studio的Fish Redux模板。
- huangjianke编写:VSCode的Fish Redux模板
创立
- 这里我在新建的count文件夹上,抉择新建文件,抉择:New ---> FishReduxTemplate
此处抉择:Page,底下的“Select Fils”全副抉择,这是规范的redux文件构造;这边命名倡议应用大驼峰:Count
- Component:这个个别是可复用的相干的组件;列表的item,也能够抉择这个
- Adapter:这里有三个Adapter,都能够不必了;fish_redux第三版推出了性能更弱小的adapter,更加灵便的绑定形式
- 创立胜利后,记得在创立的文件夹上右击,抉择:Reload From Disk;把创立的文件刷新进去
创立胜利的文件构造
- page:总页面,注册effect,reducer,component,adapter的性能,相干的配置都在此页面操作
- state:这中央就是咱们寄存子模块变量的中央;初始化变量和承受上个页面参数,也在此处,是个很重要的模块
- view:次要是咱们写页面的模块
- action:这是一个十分重要的模块,所有的事件都在此处定义和直达
- effect:相干的业务逻辑,网络申请等等的“副作用”操作,都能够写在该模块
- reducer:该模块次要是用来更新数据的,也能够写一些简略的逻辑或者和数据无关的逻辑操作
- OK,至此就把所有的筹备工作搞定了,上面能够开搞代码了
开发流程
redux流程
- 下图是阮一峰老师博客上放的redux流程图
fish_redux流程
在写代码前,先看写下流程图,这图是凭着本人的了解画的
- 能够发现,事件的传递,都是通过dispatch这个办法,而且action这层很显著是十分要害的一层,事件的传递,都是在该层定义和直达的
- 这图在语雀上调了半天,就在下面加了个本人的github水印地址
通过俩个流程图比照,其中还是有一些差异的
- redux外面的store是全局的。fish_redux外面也有这个全局store的概念,放在子模块外面了解store,react;对应fish_redux里的就是:state,view
- fish_redux外面多了effect层:这层次要是解决逻辑,和相干网络申请之类
- reducer外面,实践上也是能够解决一些和数据相干,简略的逻辑;然而简单的,会产生相应较大的“副作用”的业务逻辑,还是须要在effect中写
范例阐明
这边写几个示例,来演示fish_redux的应用
计数器
- fish_redux失常状况下的流转过程
- fish_redux各模块怎么传递数据
页面跳转
- A ---> B(A跳转到B,并传值给B页面)
- B ---> A(B返回到A,并返回值给A页面)
列表文章
- 列表展现-网络申请
- 列表批改-单item刷新
- 多样式列表
- 列表存在的问题+解决方案
全局模块
- 全局切换主题
全局模式优化
- 大幅度晋升开发体验
Component应用
- page中应用component
- 播送
开发小技巧
- 弱化reducer
- widget组合式开发
计数器
效果图
这个例子演示,view中点击此操作,而后更新页面数据;下述的流程,在effect中把数据处理好,通过action直达传递给reducer更新数据
- view ---> action ---> effect ---> reducer(更新数据)
- 留神:该流程将展现,怎么将数据在各流程中相互传递
规范模式
main
- 这中央须要留神,cupertino,material这类零碎包和fish_redux里蕴含的“Page”类名反复了,须要在这类零碎包上应用hide,暗藏零碎包里的Page类
- 对于页面的切换格调,能够在MaterialApp中的onGenerateRoute办法中,应用相应页面切换格调,这边应用ios的页面切换格调:cupertino
///须要应用hide暗藏Pageimport '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
- 定义咱们在页面展现的一些变量,initState中能够初始化变量;clone办法的赋值写法是必须的
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外面有三个参数
- state:这个就是咱们的数据层,页面须要的变量都写在state层
- dispatch:相似调度器,调用action层中的办法,从而去回调effect,reducer层的办法
- viewService:这个参数,咱们能够应用其中的办法:buildComponent("组件名"),调用咱们封装的相干组件
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
- 该层是十分重要的模块,页面所有的行为都能够在本层直观的看到
- XxxxAction中的枚举字段是必须的,一个事件对应有一个枚举字段,枚举字段是:effect,reducer层标识的入口
- XxxxActionCreator类中的办法是直达办法,办法中能够传参数,参数类型可任意;办法中的参数放在Action类中的payload字段中,而后在effect,reducer中的action参数中拿到payload值去解决就行了
- 这中央须要留神下,默认生成的模板代码,return的Action类加了const润饰,如果应用Action的payload字段赋值并携带数据,是会报错的;所以这里如果须要携带参数,请去掉const润饰关键字
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
- action:该对象中,咱们能够拿到payload字段外面,在action外面保留的值
- ctx:该对象中,能够拿到state的参数,还能够通过ctx调用dispatch办法,调用action中的办法,在这里调用dispatch办法,个别是把解决好的数据,通过action直达到reducer层中更新数据
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
- 该层是更新数据的,action中调用的XxxxActionCreator类中的办法,相应的枚举字段,会在asReducer办法中回调,这里就能够写个办法,克隆state数据进行一些解决,这外面有俩个参数:state,action
- state参数常常应用的是clone办法,clone一个新的state对象;action参数根本就是拿到其中的payload字段,将其中的值,赋值给state
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;}
- page模块不须要改变,这边就不贴代码了
优化
从下面的例子看到,如此简略数据变换,仅仅是个state中一个参数自增的过程,effect层就显得有些多余;所以,把流程简化成上面
- view ---> action ---> reducer
- 留神:这边把effect层删掉,该层能够舍弃了;而后对view,action,reducer层代码进行一些小改变
搞起来
view
- 这边仅仅把点击事件的办法,微微改了下:CountActionCreator.countIncrease()改成CountActionCreator.updateCount()
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
- 这里间接在:_updateCount办法中解决下简略的自增逻辑
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;}
搞定
- 能够看见优化了后,代码量减少了很多,看待不同的业务场景,能够灵便的变动,应用框架,但不要拘泥框架;然而如果有网络申请,很简单的业务逻辑,就万万不能写在reducer外面了,肯定要写在effect中,这样能力保障一个清晰的解耦构造,保障解决数据和更新数据过程拆散
页面跳转
效果图
从效果图,很容易看到,俩个页面互相传值
- FirstPage ---> SecondPage(FirstPage跳转到SecondPage,并传值给SecondPage页面)
- SecondPage ---> FirstPage(SecondPage返回到FirstPage,并返回值给FirstPage页面)
实现
- 从下面效果图上看,很显著,这边须要实现俩个页面,先看看main页面的改变
main
- 这里只减少了俩个页面:FirstPage和SecondPage;并将主页面入口换成了:FirstPage
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); }); }, );}
FirstPage
先来看看该页面的一个流程
- view ---> action ---> effect(跳转到SecondPage页面)
- effect(拿到SecondPage返回的数据) ---> action ---> reducer(更新页面数据)
state
先写state文件,这边须要定义俩个变量来
- fixedMsg:这个是传给下个页面的值
- msg:在页面上展现传值得变量
- initState办法是初始化变量和承受页面传值的,这边咱们给他赋个初始值
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
- 该页面逻辑相当简略,次要的仅仅是在onPressed办法中解决逻辑
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:这里须要定义俩个枚举事件
- toSecond:跳转到SecondPage页面
- updateMsg:拿到SecondPage页面返回的数据,而后更新页面数据
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
- 此处须要留神:fish_redux 框架中的Action类和零碎包中的重名了,须要把零碎包中Action类暗藏掉
- 传值间接用pushNamed办法即可,携带的参数能够写在arguments字段中;pushNamed返回值是Future类型,如果想获取他的返回值,跳转办法就须要写成异步的,期待从SecondPage页面获取返回的值,
/// 应用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
- 这里就是从action外面获取传递的值,赋值给克隆对象中msg字段即可
Reducer<FirstState> buildReducer() { return asReducer( <Object, Reducer<FirstState>>{ FirstAction.updateMsg: _updateMsg, }, );}FirstState _updateMsg(FirstState state, Action action) { return state.clone()..msg = action.payload;}
SecondPage
这个页面比较简单,后续不波及到页面数据更新,所以reducer模块能够不写,看看该页面的流程
- view ---> action ---> effect(pop以后页面,并携带值返回)
state
- 该模块的变量和FirstPage类型,就不论述了
- initState外面通过args变量获取上个页面传递的值,上个页面传值须要传递Map类型,这边通过key获取相应的value
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
- 这边须要留神的就是:WillPopScope控件接管AppBar的返回事件
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), ], ), ), );}
- action
enum SecondAction { backFirst }class SecondActionCreator { ///返回到第一个页面,而后从栈中移除本身,同时传回去一些数据 static Action backFirst() { return Action(SecondAction.backFirst); }}
effect
- 此处同样须要暗藏零碎包中的Action类
- 这边间接在pop办法的第二个参数,写入返回数据
///暗藏零碎包中的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});}
搞定
- 因为page模块不须要改变,所以就没必要将page模块代码附上了哈
- OK,到这里,咱们也曾经把俩个页面互相传值的形式get到了!
列表文章
了解了下面俩个案例,置信你能够应用fish_redux实现一部分页面了;然而,咱们堆页面的过程中,能领会列表模块是十分重要的一部分,当初就来学学,在fish_redux中怎么应用ListView吧!
- 废话少说,上号!
列表展现-网络申请
效果图
效果图对于列表的滚动,做了俩个操作:一个是拖拽列表;另一个是滚动鼠标的滚轮。flutter对鼠标触发的相干事件也反对的越来越好了!
- 这边咱们应用的是玩Android的api,这个api有个坑的中央,没设置开启跨域,所以运行在web上,这个api应用会报错,我在玩Android的github上提了issue,哎,也不晓得作者啥时候解决,,,
- 这中央只能曲线救国,敞开浏览器跨域限度,设置看这里:https://www.jianshu.com/p/56b...
- 如果运行在虚拟机上,就齐全不会呈现这个问题!
筹备
- 先看下文件构造
main
这边改变十分小,只在路由里,新增了:GuidePage,ListPage;同时将home字段中的默认页面,改成了:GuidePage页面;导航页面代码就不贴在文章里了,上面贴下该页面链接
- https://github.com/CNAD666/Ex...
- ListPage才是重点,下文会具体阐明
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(Component) ---> 创立adapter文件 ---> state集成相应的Source ---> page外面绑定adapter
- 通过以上四步,就能在fish_redux应用相应列表外面的adapter了,过程有点麻烦,然而游刃有余,多用用就能很快搭建一个简单的列表了
总流程:初始化列表模块 ---> item模块 ---> 列表模块逻辑欠缺
初始化列表模块
- 这个就是失常的创立fish_redux模板代码和文件
item模块
- 依据接口返回json,创立相应的bean ---> 创立item模块 ---> 编写state ---> 编写view界面
列表模块逻辑欠缺:俩中央分俩步(adapter创立及其绑定,失常page页面编辑)
- 创立adapter文件 ---> state调整 ---> page中绑定adapter
- view模块编写 ---> action增加更新数据事件 ---> effect初始化时获取数据并解决 ---> reducer更新数据
- 整体流程的确有些多,然而咱们依照整体三步流程流程走,保障思路清晰就行了
初始化列表模块
- 此处新建个文件夹,在文件夹上新建fis_redux文件就行了;这中央,咱们抉择page,整体的五个文件:action,effect,reducer,state,view;全副都要用到,所以默认全选,填入Module的名字,点击OK
item模块
依照流程走
- 依据接口返回json,创立相应的bean ---> 创立item模块 ---> 编写state ---> 编写view界面
筹备工作
创立bean实体
依据api返回的json数据,生成相应的实体
- api:https://www.wanandroid.com/pr...
json转实体
- 网站:https://javiercbk.github.io/j...
- 插件:AS中能够搜寻:FlutterJsonBeanFactory
这中央生成了:ItemDetailBean;代码俩百多行就不贴了,具体的内容,点击上面链接
- ItemDetailBean代码:https://github.com/CNAD666/Ex...
创立item模块
- 这边咱们实现一个简略的列表,item仅仅做展现性能;不做点击,更新ui等操作,所以这边咱们就不须要创立:effect,reducer,action文件;只抉择:state和view就行了
- 创立item,这里抉择component
文件构造
OK,bean文件搞定了,再来看看,item文件中的文件,这里component文件不须要改变,所以这中央,咱们只须要看:state.dart,view.dart
state
- 这中央还是惯例的写法,因为json生成的bean外面,能用到的所有数据,都在Datas类外面,所以,这中央建一个Datas类的变量即可
- 因为,没用到reducer,实际上clone实现办法都能删掉,避免前面可能须要clone对象,暂且留着
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),分左右俩大块
- 右边:单纯的图片展现
- 左边:采纳了纵向布局(Column),联合Expanded造成比例布局,别离展现三块货色:题目,内容,作者和工夫
- OK,这边view只是简略用到了state提供的数据造成的布局,没有什么要特地留神的中央
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文件 ---> state调整 ---> page中绑定adapter
adapter创立及其绑定
创立adapter
- 首先须要创立adapter文件,而后写入上面代码:这中央须要继承SourceFlowAdapter适配器,外面的泛型须要填入ListState,ListState这中央会报错,因为咱们的ListState没有继承MutableSource,上面state的调整就是对这个的解决
- ListItemAdapter的构造函数就是通用的写法了,在super外面写入咱们下面写好item款式,这是个pool应该能够了解为款式池,这个key最好都提出来,因为在state模块还须要用到,能够定义多个不同的item,很容易做成多样式item的列表;目前,咱们这边只须要用一个,填入:ItemComponent()
class ListItemAdapter extends SourceFlowAdapter<ListState> { static const String item_style = "project_tab_item"; ListItemAdapter() : super( pool: <String, Component<Object>>{ ///定义item的款式 item_style: ItemComponent(), }, );}
state调整
- state文件中的代码须要做一些调整,须要继承相应的类,和adapter建设起关联
- ListState须要继承MutableSource;还必须定义一个泛型是item的ItemState类型的List,这俩个是必须的;而后实现相应的形象办法就行了
- 这里只有向items里写入ItemState的数据,列表就会更新了
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
- 这里就是将咱们的ListSate和ListItemAdapter适配器建设起连贯
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模块编写 ---> action增加更新数据事件 ---> effect初始化时获取数据并解决 ---> reducer更新数据
view
- 这外面的列表应用就相当简略了,填入itemBuilder和itemCount参数就行了,这里就须要用viewService参数了哈
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
- 只须要写个更新items的事件就ok了
enum ListAction { updateItem }class ListActionCreator { static Action updateItem(var list) { return Action(ListAction.updateItem, payload: list); }}
effect
- Lifecycle.initState是进入页面初始化的回调,这边能够间接用这个状态回调,来申请接口获取相应的数据,而后去更新列表
- 这中央有个坑,dio必须联合json序列号和反序列的库一起用,不然Dio无奈将数据源解析成Response类型
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;}
列表批改-单item刷新
效果图
- 这次来演示列表的单item更新,没有网络申请的操作,所以代码逻辑就相当简略了
构造
- 来看看代码构造
- 这中央很显著得发现,list_edit主体文件很少,因为这边间接在state里初始化了数据源,就没有前期更新数据的操作,所以就不须要:action,effect,reducer这三个文件!item模块则间接在reducer里更新数据,不波及相干简单的逻辑,所以不须要:effect文件。
列表模块
这次列表模块是十分的简略,根本不波及什么流程,就是最根本初始化的一个过程,将state里初始化的数据在view中展现
- state ---> view
state
- 老规矩,先来看看state中的代码
- 这里一些新建了变量,泛型是ItemState(item的State),items变量初始化了一组数据;而后,同样继承了MutableSource,实现其相干办法
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
- view的代码主体仅仅是个ListView.builder,没有什么额定Widget
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
- 和下面类型,adapter继承SourceFlowAdapter适配器
class ListItemAdapter extends SourceFlowAdapter<ListEditState> { static const String itemName = "item"; ListItemAdapter() : super( pool: <String, Component<Object>>{itemName: ItemComponent()}, );}
page
- 在page外面绑定adapter
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模块了,item模块的流程,也是十分的清晰
- view ---> action ---> reducer
state
- 老规矩,先来看看state外面的代码;此处就是写惯例变量的定义,这些在view中都能用得着
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
- 能够看到Checkbox的外部点击操作,咱们传递了一个id参数,留神这个id参数是必须的,在更新item的时候来做辨别用的
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
- _onChange会回调所有ItemState,所以这中央必须用id或其它惟一标识去界定,咱们所操作的item具体是哪一个
- _onChange办法,未操作的item返回的时候要留神,须要返回:state原对象,表明该state对象未变动,其item不须要刷新;不能返回state.clone(),这样返回的就是个全新的state对象,每个item都会刷新,还会造成一个很奇怪的bug,会造成后续点击item操作失灵
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,是齐全能够的。
- 咱们定义数据源的时候把泛型写成Object是齐全能够的,然而初始化数据的时候肯定要留神,写成对应adapter类型外面的state
假如一种状况,在index是奇数时展现:OneComponent;在index是奇数时展现:TwoComponent;
- getItemType:这个重写办法外面,在index为奇偶数时别离返回:OneComponent和TwoComponent的标识
- 数据赋值时也肯定要在index为奇偶数时赋值泛型别离为:OneState和TwoState
- 也能够这样优化去做,在getItemType外面判断以后泛型是什么数据类型,而后再返回对应的XxxxComponent的标识
- 数据源的数据类型必须和getItemType返回的XxxxComponent的标识绝对应,如果数据源搞成Object类型,映射到对应地位的item数据时,会报类型不适配的谬误
下述代码可做思路参考
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的,只能一次刷新一个item(一个clone对应着一次刷新),一个事件对应着刷新一个item;这边是打印多个日志剖析进去了
- 解决:解决办法是,多个事件去解决刷新操作
举例:假如一种场景,对于下面的item只能单选,一个item项被选中,其它item状态被重置到未选状态,具体成果看下方效果图
- 效果图
- 这种成果的实现非常简单,然而如果思路不对,会掉进坑里出不来
- 还原被选的状态,不能在同一个事件里写,须要新写一个革除事件
下述代码为整体流程
- view
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)); } ), ), );}
- action
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
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外面代码
搞定
- 呼,终于将列表这块写完,说实话,这个列表的应用的确有点麻烦;实际上,如果大家用心看了的话,麻烦的中央,其实就是在这块:adapter创立及其绑定;只能多写写了,游刃有余!
- 列表模块功败垂成,当前就能欢快的写列表了!
全局模式
效果图
- 了解了下面的是三个例子,置信大部分页面,对于你来说都不在话下了;当初咱们再来看个例子,官网提供的全局主题性能,当然,这不仅仅是全局主题,全局字体款式,字体大小等等,都是能够全局治理,当然了,写app之前要做好布局
开搞
store模块
文件构造
- 这中央须要新建一个文件夹,新建四个文件:action,reducer,state,store
state
- 老规矩,先来看看state,咱们这里只在抽象类外面定义了一个主题色,这个抽象类是很重要的,须要做全局模式所有子模块的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
- 这里就是解决变色的一些操作,这是咸鱼官网demo外面代码;这阐明简略的逻辑,是能够放在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());}
main改变
这外面将PageRoutes外面的visitor字段应用起来,状态更新操作代码有点多,就独自提出来了;所以main文件外面,减少了:
- visitor字段应用
- 减少_updateState办法
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和view,所以其它模块代码也不反复贴出了
state
- 这中央,仅仅让CountState多实现了GlobalBaseState类,很小的改变
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
- 这外面仅仅改变了一行,在AppBar外面加了backgroundColor,而后应用state外面的全局主题色
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), ), );}
- 如果其余模块也须要做主题色,也依照此处逻辑改变即可
调用
- 调用状态更新就非常简单了,和失常模块更新View一样,这里咱们调用全局的就行了,一行代码搞定,在须要的中央调用就OK了
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());
搞定
- 通过下面的的三步,咱们就能够应用全局状态了;从上体面模块的应用,能够很显著的感触到,全局状态,必须后期做好字段的布局,确定之后,最好不要再减少字段,不然继承抽象类的多个模块都会爆红,提醒去实现xxx变量
全局模块优化
反思
在下面的全局模式里说了,应用全局模块,后期须要布局好字段,不然我的项目进行到中期的时候,想增加字段,多个模块的State会呈现大范畴爆红,提醒去实现你增加的字段;我的项目开始布局好所有的字段,显然这须要全面的思考好大部分场景,然而人的灵感总是有限的,不改代码是不可能,这辈子都不可能。只能想方法看能不能增加一次字段后,前期增加字段,并不会引起其余模块爆红,试了屡次,胜利的应用两头实体,来解决该问题
这里优化俩个方面
应用通用的全局实体
- 这样前期增加字段,就不会影响其余模块,这样咱们就能一个个模块的去整改,不会呈现整个我的项目不能运行的状况
将路由模块和全局模块封装
- 路由模块前期页面多了,代码会很多,放在主入口,真的不好治理;全局模块同理
因为应用两头实体,有一些中央会呈现空指针问题,我都在流程外面写分明了,大家能够把优化流程残缺看一遍哈,都配置好,前面拓展应用就不会报空指针了
优化
入口模块
main:大改
- 从上面代码能够看到,这里将路由模块和全局模块独自提出来了,这中央为了不便观看,就写在一个文件里;阐明下,RouteConfig和StoreConfig这俩个类,能够放在俩个不同的文件里,这样治理路由和全局字段更新就会很不便了!
- RouteConfig:这里将页面标识和页面映射离开写,这样咱们跳转页面的时候,就能够间接援用RouteConfig外面的页面标识
- StoreConfig:全局模块里最重要的就是状态的判断,正文写的很分明了,能够看看正文哈
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()); } }}
Store模块
上面俩个模块是须要改变代码的模块
state
- 这里应用了StoreModel两头实体,留神,这中央实体字段store,初始化是必须的,不然在子模块援用该实体下的字段会报空指针
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
- 这中央改变十分小,将state.themeColor改成state.store.themeColor
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;}
上面俩个模块代码没有改变,然而为了思路残缺,同样贴出来
- action
enum GlobalAction { changeThemeColor }class GlobalActionCreator{ static Action onChangeThemeColor(){ return const Action(GlobalAction.changeThemeColor); }}
- store
class GlobalStore{ static Store<GlobalState> _globalStore; static Store<GlobalState> get store => _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer());}
子模块应用
- 这里就用计数器模块的来举例,因为仅仅只须要改变大量代码,且只波及state和view,所以其它模块代码也不反复贴出了
state
- 因为是用两头实体,所以在clone办法外面必须将实现的store字段加上,不然会报空指针
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
- 这外面仅仅改变了一行,在AppBar外面加了backgroundColor,而后应用state外面的全局主题色
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,可插拔式应用!Component的应用总的来说是比较简单了,比拟要害的是在State中建设起连贯。
效果图
- 上图的成果是在页面中嵌入了俩个Component,扭转子Component的操作是在页面中实现的
- 先看下页面构造
Component
这中央写了一个Component,代码很简略,来看看吧
- component
这中央代码是主动生成了,没有任何改变,就不贴了
state
- initState():咱们须要留神,Component中的initState()办法在外部没有调用,尽管主动生成的代码有这个办法,然而无奈起到初始化作用,能够删掉该办法
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; }}
- view
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), ), );}
Page
CompPage中,没用到effete这层,就没创立该文件,老规矩,先看看state
state
- 这中央是十分重要的中央,XxxxConnecto的实现模式是看官网代码写的
computed():该办法是必须实现的,这个相似间接的get()办法,然而切记不能像get()间接返回state.leftAreaState()或state.rightAreaState,某些场景初始化无奈刷新,因为是同一个对象,会被判断未更改,所以会不刷新控件
- 留神了留神了,这边做了优化,间接返回clone办法,这是对官网赋值写法的一个优化,也能够防止下面说的问题,大家能够思考思考
- set():该办法是Component数据流回推到页面的state,放弃俩者state数据统一;如果Component模块更新了本人的State,不写这个办法会报错的
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
- 写完连接器后,咱们在Page外面绑定下,就能应用Component了
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
- 应用Component就非常简单了:viewService.buildComponent("xxxxxx")
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("扭转"), ), )) ], ), );}
- action
enum CompAction { change }class CompActionCreator { static Action change() { return const Action(CompAction.change); }}
- reducer
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外部本人去解决了
播送
fish_redux中是带有播送的通信形式,应用的形式很简略,这本是effect层,ctx参数自带的一个api,这里简略介绍一下
应用
action
- 播送事件独自写了一个action文件,便于对立治理
enum BroadcastAction { toNotify }class BroadcastActionCreator { ///播送告诉 static Action toNotify(String msg) { return Action(BroadcastAction.toNotify, payload: msg); }}
发送播送
- 这是页面跳转的办法,就在此处写了,如果想看具体代码的话,能够去demo地址外面看下
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是有页面栈,就能告诉其余页面,很多状况下,咱们在一个页面进行了操作,其余页面也须要同步做一些解决,应用播送就很简略了
留神: 播送发送和承受是一对多的关系,一处发送,能够在多处承受;和dispatch发送事件,如果在effect外面承受,在reducer就无奈承受的状况是不一样的(被拦挡了)
开发小技巧
弱化reducer
有限弱化了reducer层作用
- 在日常应用fish_redux和flutter_bloc后,理论能粗浅领会reducer层实际上只是相当于bloc中yield
或emit关键字的作用,职能齐全能够弱化为,仅仅作为状态刷新;这样能够大大简化开发流程,只须要关注
view -> action -> effect (reducer:应用对立的刷新事件) - 上面范例代码,解决数据的操作间接在effect层解决,如须要更改数据,间接对ctx.state进行操作,波及刷新页面的操作,对立调用onRefresh事件;对于一个页面有几十个表单的状况,这种操作,能大大晋升你的开发速度和体验,亲自体验,大家能够尝试下
Reducer<TestState> buildReducer() { return asReducer( <Object, Reducer<TestState>>{ TestAction.onRefresh: _onRefresh, }, );}TestState _onRefresh(TreeState state, Action action) { return state.clone();}
- 具体能够查看 玩android 我的项目代码;花了一些工夫,把玩android我的项目代码所有模块全副重构了,肝痛
widget组合式开发
阐明
这种开发模式,能够说是个常规,在android外面是封装一个个View,View里有对应的一套,逻辑自洽的性能,而后在主xm外面组合这些View;这种思维齐全能够引申到Flutter里,而且,开发体验更上几百层楼,让你的widget组合能够更加灵便百变,百变星君
- view模块中,页面应用widget组合的形式去结构的,只传入必要的数据源和保留一些点击回调
为什么用widget组合形式结构页面?
- 非常复杂的界面,必须将页面分成一个个小模块,而后再将其组合, 每个小模块Widget外部该当对本身的的职能,能逻辑自洽的去解决;这种组合的形式出现的代码,会十分的层次分明,不会让你的代码写着写着,忽然就变成shit
组合widget关键点
- 一般来说,咱们并不关注widget外部页面的实现,只须要关怀的是widget须要的数据源, 以及widget对交互的反馈;例如:我点击widget后,widget回调事件,并传播一些数据给我;至于外部怎么实现, 内部并不关怀,请勿将dispatch传递到封装的widget外部,这会使咱们关注的事件被封装在外部
- 具体请查看 玩android 我的项目代码
最初
- 这片文章,说实话,花了不少精力去写的,也花了不少工夫构思;次要是例子,必须要本人重写下,重复思考例子是否正当等等,头皮微凉。
- 代码地址:代码demo地址
- fish_redux版-玩Android:fish_redux版-玩android
- 大家如果感觉有播种,就给我点个赞吧!你的点赞,是我码字的最大能源!