说句心里话,这篇文章,来来回回修改了很多次,如果认真看完这篇文章,还不会写fish_redux,请在评论里喷我。

前言

来学学难搞的fish_redux框架吧,这个框架,官方的文档真是一言难尽,比flutter_bloc官网的文档真是逊色太多了,但是一旦知道怎么写,页面堆起来也是非常爽呀,结构分明,逻辑也会错落有致。

其实在当时搞懂这个框架的时候,就一直想写一篇文章记录下,但是因为忙(lan),导致一直没写,现在觉得还是必须把使用的过程记录下,毕竟刚上手这个框架是个蛋痛的过程,必须要把这个过程做个记录。

这不仅仅是记录的文章,文中所给出的示例,也是我重新构思去写的,过程也是力求阐述清楚且详细。

几个问题点

  • 页面切换的转场动画
  • 页面怎么更新数据
  • fish_redux各个模块之间,怎么传递数据
  • 页面跳转传值,及其接受下个页面回传的值
  • 怎么配合ListView使用
  • ListView怎么使用adapter,数据怎么和item绑定
  • 怎么将Page当做widget使用(BottomNavigationBar,NavigationRail等等导航栏控件会使用到)
    • 这个直接使用:XxxPage.buildPage(null) 即可

如果你在使用fish_redux的过程中遇到过上述的问题,那就来看看这篇文章吧!这里,会解答上面所有的问题点!

准备

引入

fish_redux相关地址

我用的是0.3.X的版本,算是第三版,相对于前几版,改动较大

  • 引入fish_redux插件,想用最新版插件,可进入pub地址里面查看
  1. fish_redux: ^0.3.4
  2. #演示列表需要用到的库
  3. dio: ^3.0.9 #网络请求框架
  4. json_annotation: ^2.4.0 #json序列化和反序列化用的

开发插件

创建

  • 这里我在新建的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
  1. ///需要使用hide隐藏Page
  2. import 'package:flutter/cupertino.dart'hide Page;
  3. import 'package:flutter/material.dart' hide Page;
  4. void main() {
  5. runApp(MyApp());
  6. }
  7. Widget createApp() {
  8. ///定义路由
  9. final AbstractRoutes routes = PageRoutes(
  10. pages: <String, Page<Object, dynamic>>{
  11. "CountPage": CountPage(),
  12. },
  13. );
  14. return MaterialApp(
  15. title: 'FishDemo',
  16. home: routes.buildPage("CountPage", null), //作为默认页面
  17. onGenerateRoute: (RouteSettings settings) {
  18. //ios页面切换风格
  19. return CupertinoPageRoute(builder: (BuildContext context) {
  20. return routes.buildPage(settings.name, settings.arguments);
  21. })
  22. // Material页面切换风格
  23. // return MaterialPageRoute<Object>(builder: (BuildContext context) {
  24. // return routes.buildPage(settings.name, settings.arguments);
  25. // });
  26. },
  27. );
  28. }
  • state

    • 定义我们在页面展示的一些变量,initState中可以初始化变量;clone方法的赋值写法是必须的
  1. class CountState implements Cloneable<CountState> {
  2. int count;
  3. @override
  4. CountState clone() {
  5. return CountState()..count = count;
  6. }
  7. }
  8. CountState initState(Map<String, dynamic> args) {
  9. return CountState()..count = 0;
  10. }
  • view:这里面就是写界面的模块,buildView里面有三个参数

    • state:这个就是我们的数据层,页面需要的变量都写在state层
    • dispatch:类似调度器,调用action层中的方法,从而去回调effect,reducer层的方法
    • viewService:这个参数,我们可以使用其中的方法:buildComponent("组件名"),调用我们封装的相关组件
  1. Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
  2. return _bodyWidget(state, dispatch);
  3. }
  4. Widget _bodyWidget(CountState state, Dispatch dispatch) {
  5. return Scaffold(
  6. appBar: AppBar(
  7. title: Text("FishRedux"),
  8. ),
  9. body: Center(
  10. child: Column(
  11. mainAxisAlignment: MainAxisAlignment.center,
  12. children: <Widget>[
  13. Text('You have pushed the button this many times:'),
  14. ///使用state中的变量,控住数据的变换
  15. Text(state.count.toString()),
  16. ],
  17. ),
  18. ),
  19. floatingActionButton: FloatingActionButton(
  20. onPressed: () {
  21. ///点击事件,调用action 计数自增方法
  22. dispatch(CountActionCreator.countIncrease());
  23. },
  24. child: Icon(Icons.add),
  25. ),
  26. );
  27. }
  • action

    • 该层是非常重要的模块,页面所有的行为都可以在本层直观的看到
    • XxxxAction中的枚举字段是必须的,一个事件对应有一个枚举字段,枚举字段是:effect,reducer层标识的入口
    • XxxxActionCreator类中的方法是中转方法,方法中可以传参数,参数类型可任意;方法中的参数放在Action类中的payload字段中,然后在effect,reducer中的action参数中拿到payload值去处理就行了
    • 这地方需要注意下,默认生成的模板代码,return的Action类加了const修饰,如果使用Action的payload字段赋值并携带数据,是会报错的;所以这里如果需要携带参数,请去掉const修饰关键字
  1. enum CountAction { increase, updateCount }
  2. class CountActionCreator {
  3. ///去effect层去处理自增数据
  4. static Action countIncrease() {
  5. return Action(CountAction.increase);
  6. }
  7. ///去reducer层更新数据,传参可以放在Action类中的payload字段中,payload是dynamic类型,可传任何类型
  8. static Action updateCount(int count) {
  9. return Action(CountAction.updateCount, payload: count);
  10. }
  11. }
  • effect

    • 如果在调用action里面的XxxxActionCreator类中的方法,相应的枚举字段,会在combineEffects中被调用,在这里,我们就能写相应的方法处理逻辑,方法中带俩个参数:action,ctx

      • action:该对象中,我们可以拿到payload字段里面,在action里面保存的值
      • ctx:该对象中,可以拿到state的参数,还可以通过ctx调用dispatch方法,调用action中的方法,在这里调用dispatch方法,一般是把处理好的数据,通过action中转到reducer层中更新数据
  1. Effect<CountState> buildEffect() {
  2. return combineEffects(<Object, Effect<CountState>>{
  3. CountAction.increase: _onIncrease,
  4. });
  5. }
  6. ///自增数
  7. void _onIncrease(Action action, Context<CountState> ctx) {
  8. ///处理自增数逻辑
  9. int count = ctx.state.count + 1;
  10. ctx.dispatch(CountActionCreator.updateCount(count));
  11. }
  • reducer

    • 该层是更新数据的,action中调用的XxxxActionCreator类中的方法,相应的枚举字段,会在asReducer方法中回调,这里就可以写个方法,克隆state数据进行一些处理,这里面有俩个参数:state,action
    • state参数经常使用的是clone方法,clone一个新的state对象;action参数基本就是拿到其中的payload字段,将其中的值,赋值给state
  1. Reducer<CountState> buildReducer() {
  2. return asReducer(
  3. <Object, Reducer<CountState>>{
  4. CountAction.updateCount: _updateCount,
  5. },
  6. );
  7. }
  8. ///通知View层更新界面
  9. CountState _updateCount(CountState state, Action action) {
  10. final CountState newState = state.clone();
  11. newState..count = action.payload;
  12. return newState;
  13. }
  • page模块不需要改动,这边就不贴代码了

优化

  • 从上面的例子看到,如此简单数据变换,仅仅是个state中一个参数自增的过程,effect层就显得有些多余;所以,把流程简化成下面

    • view ---> action ---> reducer
  • 注意:这边把effect层删掉,该层可以舍弃了;然后对view,action,reducer层代码进行一些小改动

搞起来

  • view

    • 这边仅仅把点击事件的方法,微微改了下:CountActionCreator.countIncrease()改成CountActionCreator.updateCount()
  1. Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
  2. return _bodyWidget(state, dispatch);
  3. }
  4. Widget _bodyWidget(CountState state, Dispatch dispatch) {
  5. return Scaffold(
  6. appBar: AppBar(
  7. title: Text("FishRedux"),
  8. ),
  9. body: Center(
  10. child: Column(
  11. mainAxisAlignment: MainAxisAlignment.center,
  12. children: <Widget>[
  13. Text('You have pushed the button this many times:'),
  14. Text(state.count.toString()),
  15. ],
  16. ),
  17. ),
  18. floatingActionButton: FloatingActionButton(
  19. onPressed: () {
  20. ///点击事件,调用action 计数自增方法
  21. dispatch(CountActionCreator.updateCount());
  22. },
  23. child: Icon(Icons.add),
  24. ),
  25. );
  26. }
  • action

    • 这里只使用一个枚举字段,和一个方法就行了,也不用传啥参数了
  1. enum CountAction { updateCount }
  2. class CountActionCreator {
  3. ///去reducer层更新数据,传参可以放在Action类中的payload字段中,payload是dynamic类型,可传任何类型
  4. static Action updateCount() {
  5. return Action(CountAction.updateCount);
  6. }
  7. }
  • reducer

    • 这里直接在:_updateCount方法中处理下简单的自增逻辑
  1. Reducer<CountState> buildReducer() {
  2. return asReducer(
  3. <Object, Reducer<CountState>>{
  4. CountAction.updateCount: _updateCount,
  5. },
  6. );
  7. }
  8. ///通知View层更新界面
  9. CountState _updateCount(CountState state, Action action) {
  10. final CountState newState = state.clone();
  11. newState..count = state.count + 1;
  12. return newState;
  13. }

搞定

  • 可以看见优化了后,代码量减少了很多,对待不同的业务场景,可以灵活的变动,使用框架,但不要拘泥框架;但是如果有网络请求,很复杂的业务逻辑,就万万不能写在reducer里面了,一定要写在effect中,这样才能保证一个清晰的解耦结构,保证处理数据和更新数据过程分离

页面跳转

效果图

  • 从效果图,很容易看到,俩个页面相互传值

    • FirstPage ---> SecondPage(FirstPage跳转到SecondPage,并传值给SecondPage页面)
    • SecondPage ---> FirstPage(SecondPage返回到FirstPage,并返回值给FirstPage页面)

实现

  • 从上面效果图上看,很明显,这边需要实现俩个页面,先看看main页面的改动
  • main
    • 这里只增加了俩个页面:FirstPage和SecondPage;并将主页面入口换成了:FirstPage
  1. Widget createApp() {
  2. ///定义路由
  3. final AbstractRoutes routes = PageRoutes(
  4. pages: <String, Page<Object, dynamic>>{
  5. ///计数器模块演示
  6. "CountPage": CountPage(),
  7. ///页面传值跳转模块演示
  8. "FirstPage": FirstPage(),
  9. "SecondPage": SecondPage(),
  10. },
  11. );
  12. return MaterialApp(
  13. title: 'FishRedux',
  14. home: routes.buildPage("FirstPage", null), //作为默认页面
  15. onGenerateRoute: (RouteSettings settings) {
  16. //ios页面切换风格
  17. return CupertinoPageRoute(builder: (BuildContext context) {
  18. return routes.buildPage(settings.name, settings.arguments);
  19. });
  20. },
  21. );
  22. }

FirstPage

  • 先来看看该页面的一个流程

    • view ---> action ---> effect(跳转到SecondPage页面)
    • effect(拿到SecondPage返回的数据) ---> action ---> reducer(更新页面数据)
  • state

    • 先写state文件,这边需要定义俩个变量来

      • fixedMsg:这个是传给下个页面的值
      • msg:在页面上展示传值得变量
    • initState方法是初始化变量和接受页面传值的,这边我们给他赋个初始值
  1. class FirstState implements Cloneable<FirstState> {
  2. ///传递给下个页面的值
  3. static const String fixedMsg = "\n我是FirstPage页面传递过来的数据:FirstValue";
  4. ///展示传递过来的值
  5. String msg;
  6. @override
  7. FirstState clone() {
  8. return FirstState()..msg = msg;
  9. }
  10. }
  11. FirstState initState(Map<String, dynamic> args) {
  12. return FirstState()..msg = "\n暂无";
  13. }
  • view

    • 该页面逻辑相当简单,主要的仅仅是在onPressed方法中处理逻辑
  1. Widget buildView(FirstState state, Dispatch dispatch, ViewService viewService) {
  2. return _bodyWidget(state, dispatch);
  3. }
  4. Widget _bodyWidget(FirstState state, Dispatch dispatch) {
  5. return Scaffold(
  6. appBar: AppBar(
  7. title: Text("FirstPage"),
  8. ),
  9. body: Center(
  10. child: Column(
  11. mainAxisAlignment: MainAxisAlignment.center,
  12. children: <Widget>[
  13. Text('下方数据是SecondPage页面传递过来的:'),
  14. Text(state.msg),
  15. ],
  16. ),
  17. ),
  18. floatingActionButton: FloatingActionButton(
  19. onPressed: () {
  20. ///跳转到Second页面
  21. dispatch(FirstActionCreator.toSecond());
  22. },
  23. child: Icon(Icons.arrow_forward),
  24. ),
  25. );
  26. }
  • action:这里需要定义俩个枚举事件

    • toSecond:跳转到SecondPage页面
    • updateMsg:拿到SecondPage页面返回的数据,然后更新页面数据
  1. enum FirstAction { toSecond , updateMsg}
  2. class FirstActionCreator {
  3. ///跳转到第二个页面
  4. static Action toSecond() {
  5. return const Action(FirstAction.toSecond);
  6. }
  7. ///拿到第二个页面返回的数据,执行更新数据操作
  8. static Action updateMsg(String msg) {
  9. return Action(FirstAction.updateMsg, payload: msg);
  10. }
  11. }
  • effect

    • 此处需要注意:fish_redux 框架中的Action类和系统包中的重名了,需要把系统包中Action类隐藏掉
    • 传值直接用pushNamed方法即可,携带的参数可以写在arguments字段中;pushNamed返回值是Future类型,如果想获取他的返回值,跳转方法就需要写成异步的,等待从SecondPage页面获取返回的值,
  1. /// 使用hide方法,隐藏系统包里面的Action类
  2. import 'package:flutter/cupertino.dart' hide Action;
  3. Effect<FirstState> buildEffect() {
  4. return combineEffects(<Object, Effect<FirstState>>{
  5. FirstAction.toSecond: _toSecond,
  6. });
  7. }
  8. void _toSecond(Action action, Context<FirstState> ctx) async{
  9. ///页面之间传值;这地方必须写个异步方法,等待上个页面回传过来的值;as关键字是类型转换
  10. var result = await Navigator.of(ctx.context).pushNamed("SecondPage", arguments: {"firstValue": FirstState.fixedMsg});
  11. ///获取到数据,更新页面上的数据
  12. ctx.dispatch(FirstActionCreator.updateMsg( (result as Map)["secondValue"]) );
  13. }
  • reducer

    • 这里就是从action里面获取传递的值,赋值给克隆对象中msg字段即可
  1. Reducer<FirstState> buildReducer() {
  2. return asReducer(
  3. <Object, Reducer<FirstState>>{
  4. FirstAction.updateMsg: _updateMsg,
  5. },
  6. );
  7. }
  8. FirstState _updateMsg(FirstState state, Action action) {
  9. return state.clone()..msg = action.payload;
  10. }

SecondPage

  • 这个页面比较简单,后续不涉及到页面数据更新,所以reducer模块可以不写,看看该页面的流程

    • view ---> action ---> effect(pop当前页面,并携带值返回)
  • state
    • 该模块的变量和FirstPage类型,就不阐述了
    • initState里面通过args变量获取上个页面传递的值,上个页面传值需要传递Map类型,这边通过key获取相应的value
  1. class SecondState implements Cloneable<SecondState> {
  2. ///传递给下个页面的值
  3. static const String fixedMsg = "\n我是SecondPage页面传递过来的数据:SecondValue";
  4. ///展示传递过来的值
  5. String msg;
  6. @override
  7. SecondState clone() {
  8. return SecondState()..msg = msg;
  9. }
  10. }
  11. SecondState initState(Map<String, dynamic> args) {
  12. ///获取上个页面传递过来的数据
  13. return SecondState()..msg = args["firstValue"];
  14. }
  • view

    • 这边需要注意的就是:WillPopScope控件接管AppBar的返回事件
  1. Widget buildView(SecondState state, Dispatch dispatch, ViewService viewService) {
  2. return WillPopScope(
  3. child: _bodyWidget(state),
  4. onWillPop: () {
  5. dispatch(SecondActionCreator.backFirst());
  6. ///true:表示执行页面返回 false:表示不执行返回页面操作,这里因为要传值,所以接管返回操作
  7. return Future.value(false);
  8. },
  9. );
  10. }
  11. Widget _bodyWidget(SecondState state) {
  12. return Scaffold(
  13. appBar: AppBar(
  14. title: Text("SecondPage"),
  15. ),
  16. body: Center(
  17. child: Column(
  18. mainAxisAlignment: MainAxisAlignment.center,
  19. children: <Widget>[
  20. Text('下方数据是FirstPage页面传递过来的:'),
  21. Text(state.msg),
  22. ],
  23. ),
  24. ),
  25. );
  26. }
  • action
  1. enum SecondAction { backFirst }
  2. class SecondActionCreator {
  3. ///返回到第一个页面,然后从栈中移除自身,同时传回去一些数据
  4. static Action backFirst() {
  5. return Action(SecondAction.backFirst);
  6. }
  7. }
  • effect

    • 此处同样需要隐藏系统包中的Action类
    • 这边直接在pop方法的第二个参数,写入返回数据
  1. ///隐藏系统包中的Action类
  2. import 'package:flutter/cupertino.dart' hide Action;
  3. Effect<SecondState> buildEffect() {
  4. return combineEffects(<Object, Effect<SecondState>>{
  5. SecondAction.backFirst: _backFirst,
  6. });
  7. }
  8. void _backFirst(Action action, Context<SecondState> ctx) {
  9. ///pop当前页面,并且返回相应的数据
  10. Navigator.pop(ctx.context, {"secondValue": SecondState.fixedMsg});
  11. }

搞定

  • 因为page模块不需要改动,所以就没必要将page模块代码附上了哈
  • OK,到这里,咱们也已经把俩个页面相互传值的方式get到了!

列表文章

  • 理解了上面俩个案例,相信你可以使用fish_redux实现一部分页面了;但是,我们堆页面的过程中,能体会列表模块是非常重要的一部分,现在就来学学,在fish_redux中怎么使用ListView吧!

    • 废话少说,上号!

列表展示-网络请求

效果图

  • 效果图对于列表的滚动,做了俩个操作:一个是拖拽列表;另一个是滚动鼠标的滚轮。flutter对鼠标触发的相关事件也支持的越来越好了!

    • 这边我们使用的是玩Android的api,这个api有个坑的地方,没设置开启跨域,所以运行在web上,这个api使用会报错,我在玩Android的github上提了issue,哎,也不知道作者啥时候解决,,,
  • 这地方只能曲线救国,关闭浏览器跨域限制,设置看这里:https://www.jianshu.com/p/56b1e01e6b6a
  • 如果运行在虚拟机上,就完全不会出现这个问题!

准备

  • 先看下文件结构

  1. void main() {
  2. runApp(createApp());
  3. }
  4. Widget createApp() {
  5. ///定义路由
  6. final AbstractRoutes routes = PageRoutes(
  7. pages: <String, Page<Object, dynamic>>{
  8. ///导航页面
  9. "GuidePage": GuidePage(),
  10. ///计数器模块演示
  11. "CountPage": CountPage(),
  12. ///页面传值跳转模块演示
  13. "FirstPage": FirstPage(),
  14. "SecondPage": SecondPage(),
  15. ///列表模块演示
  16. "ListPage": ListPage(),
  17. },
  18. );
  19. return MaterialApp(
  20. title: 'FishRedux',
  21. home: routes.buildPage("GuidePage", null), //作为默认页面
  22. onGenerateRoute: (RouteSettings settings) {
  23. //ios页面切换风格
  24. return CupertinoPageRoute(builder: (BuildContext context) {
  25. return routes.buildPage(settings.name, settings.arguments);
  26. });
  27. },
  28. );
  29. }

流程

  • 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界面

准备工作

文件结构

OK,bean文件搞定了,再来看看,item文件中的文件,这里component文件不需要改动,所以这地方,我们只需要看:state.dart,view.dart

  • state

    • 这地方还是常规的写法,因为json生成的bean里面,能用到的所有数据,都在Datas类里面,所以,这地方建一个Datas类的变量即可
    • 因为,没用到reducer,实际上clone实现方法都能删掉,防止后面可能需要clone对象,暂且留着
  1. import 'package:fish_redux/fish_redux.dart';
  2. import 'package:fish_redux_demo/list/bean/item_detail_bean.dart';
  3. class ItemState implements Cloneable<ItemState> {
  4. Datas itemDetail;
  5. ItemState({this.itemDetail});
  6. @override
  7. ItemState clone() {
  8. return ItemState()
  9. ..itemDetail = itemDetail;
  10. }
  11. }
  12. ItemState initState(Map<String, dynamic> args) {
  13. return ItemState();
  14. }
  • view

    • 这里item布局稍稍有点麻烦,整体上采用的是:水平布局(Row),分左右俩大块

      • 左边:单纯的图片展示
      • 右边:采用了纵向布局(Column),结合Expanded形成比例布局,分别展示三块东西:标题,内容,作者和时间
    • OK,这边view只是简单用到了state提供的数据形成的布局,没有什么要特别注意的地方
  1. Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
  2. return _bodyWidget(state);
  3. }
  4. Widget _bodyWidget(ItemState state) {
  5. return Card(
  6. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
  7. elevation: 5,
  8. margin: EdgeInsets.only(left: 20, right: 20, top: 20),
  9. child: Row(
  10. children: <Widget>[
  11. //左边图片
  12. Container(
  13. margin: EdgeInsets.all(10),
  14. width: 180,
  15. height: 100,
  16. child: Image.network(
  17. state.itemDetail.envelopePic,
  18. fit: BoxFit.fill,
  19. ),
  20. ),
  21. //右边的纵向布局
  22. _rightContent(state),
  23. ],
  24. ),
  25. );
  26. }
  27. ///item中右边的纵向布局,比例布局
  28. Widget _rightContent(ItemState state) {
  29. return Expanded(
  30. child: Container(
  31. margin: EdgeInsets.all(10),
  32. height: 120,
  33. child: Column(
  34. mainAxisAlignment: MainAxisAlignment.start,
  35. children: <Widget>[
  36. //标题
  37. Expanded(
  38. flex: 2,
  39. child: Container(
  40. alignment: Alignment.centerLeft,
  41. child: Text(
  42. state.itemDetail.title,
  43. style: TextStyle(fontSize: 16),
  44. maxLines: 1,
  45. overflow: TextOverflow.ellipsis,
  46. ),
  47. ),
  48. ),
  49. //内容
  50. Expanded(
  51. flex: 4,
  52. child: Container(
  53. alignment: Alignment.centerLeft,
  54. child: Text(
  55. state.itemDetail.desc,
  56. style: TextStyle(fontSize: 12),
  57. maxLines: 3,
  58. overflow: TextOverflow.ellipsis,
  59. ),
  60. )),
  61. Expanded(
  62. flex: 3,
  63. child: Column(
  64. mainAxisAlignment: MainAxisAlignment.end,
  65. children: <Widget>[
  66. //作者
  67. Row(
  68. children: <Widget>[
  69. Text("作者:", style: TextStyle(fontSize: 12)),
  70. Expanded(
  71. child: Text(state.itemDetail.author,
  72. style: TextStyle(color: Colors.blue, fontSize: 12),
  73. overflow: TextOverflow.ellipsis),
  74. )
  75. ],
  76. ),
  77. //时间
  78. Row(children: <Widget>[
  79. Text("时间:", style: TextStyle(fontSize: 12)),
  80. Expanded(
  81. child: Text(state.itemDetail.niceDate,
  82. style: TextStyle(color: Colors.blue, fontSize: 12),
  83. overflow: TextOverflow.ellipsis),
  84. )
  85. ])
  86. ],
  87. ),
  88. ),
  89. ],
  90. ),
  91. ));
  92. }

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()
  1. class ListItemAdapter extends SourceFlowAdapter<ListState> {
  2. static const String item_style = "project_tab_item";
  3. ListItemAdapter()
  4. : super(
  5. pool: <String, Component<Object>>{
  6. ///定义item的样式
  7. item_style: ItemComponent(),
  8. },
  9. );
  10. }
  • state调整

    • state文件中的代码需要做一些调整,需要继承相应的类,和adapter建立起关联
    • ListState需要继承MutableSource;还必须定义一个泛型是item的ItemState类型的List,这俩个是必须的;然后实现相应的抽象方法就行了
    • 这里只要向items里写入ItemState的数据,列表就会更新了
  1. class ListState extends MutableSource implements Cloneable<ListState> {
  2. ///这地方一定要注意,List里面的泛型,需要定义为ItemState
  3. ///怎么更新列表数据,只需要更新这个items里面的数据,列表数据就会相应更新
  4. ///使用多样式,请写出 List<Object> items;
  5. List<ItemState> items;
  6. @override
  7. ListState clone() {
  8. return ListState()..items = items;
  9. }
  10. ///使用上面定义的List,继承MutableSource,就把列表和item绑定起来了
  11. @override
  12. Object getItemData(int index) => items[index];
  13. @override
  14. String getItemType(int index) => ListItemAdapter.item_style;
  15. @override
  16. int get itemCount => items.length;
  17. @override
  18. void setItemData(int index, Object data) {
  19. items[index] = data;
  20. }
  21. }
  22. ListState initState(Map<String, dynamic> args) {
  23. return ListState();
  24. }
  • page中绑定adapter
  • 这里就是将我们的ListSate和ListItemAdapter适配器建立起连接
  1. class ListPage extends Page<ListState, Map<String, dynamic>> {
  2. ListPage()
  3. : super(
  4. initState: initState,
  5. effect: buildEffect(),
  6. reducer: buildReducer(),
  7. view: buildView,
  8. dependencies: Dependencies<ListState>(
  9. ///绑定Adapter
  10. adapter: NoneConn<ListState>() + ListItemAdapter(),
  11. slots: <String, Dependent<ListState>>{}),
  12. middleware: <Middleware<ListState>>[],
  13. );
  14. }

正常page页面编辑

整体流程

  • view模块编写 ---> action添加更新数据事件 ---> effect初始化时获取数据并处理 ---> reducer更新数据

  • view

    • 这里面的列表使用就相当简单了,填入itemBuilder和itemCount参数就行了,这里就需要用viewService参数了哈
  1. Widget buildView(ListState state, Dispatch dispatch, ViewService viewService) {
  2. return Scaffold(
  3. appBar: AppBar(
  4. title: Text("ListPage"),
  5. ),
  6. body: _itemWidget(state, viewService),
  7. );
  8. }
  9. Widget _itemWidget(ListState state, ViewService viewService) {
  10. if (state.items != null) {
  11. ///使用列表
  12. return ListView.builder(
  13. itemBuilder: viewService.buildAdapter().itemBuilder,
  14. itemCount: viewService.buildAdapter().itemCount,
  15. );
  16. } else {
  17. return Center(
  18. child: CircularProgressIndicator(),
  19. );
  20. }
  21. }
  • action

    • 只需要写个更新items的事件就ok了
  1. enum ListAction { updateItem }
  2. class ListActionCreator {
  3. static Action updateItem(var list) {
  4. return Action(ListAction.updateItem, payload: list);
  5. }
  6. }
  • effect

    • Lifecycle.initState是进入页面初始化的回调,这边可以直接用这个状态回调,来请求接口获取相应的数据,然后去更新列表
    • 这地方有个坑,dio必须结合json序列号和反序列的库一起用,不然Dio无法将数据源解析成Response类型
  1. Effect<ListState> buildEffect() {
  2. return combineEffects(<Object, Effect<ListState>>{
  3. ///进入页面就执行的初始化操作
  4. Lifecycle.initState: _init,
  5. });
  6. }
  7. void _init(Action action, Context<ListState> ctx) async {
  8. String apiUrl = "https://www.wanandroid.com/project/list/1/json";
  9. Response response = await Dio().get(apiUrl);
  10. ItemDetailBean itemDetailBean =
  11. ItemDetailBean.fromJson(json.decode(response.toString()));
  12. List<Datas> itemDetails = itemDetailBean.data.datas;
  13. ///构建符合要求的列表数据源
  14. List<ItemState> items = List.generate(itemDetails.length, (index) {
  15. return ItemState(itemDetail: itemDetails[index]);
  16. });
  17. ///通知更新列表数据源
  18. ctx.dispatch(ListActionCreator.updateItem(items));
  19. }
  • reducer

    • 最后就是更新操作了哈,这里就是常规写法了
  1. Reducer<ListState> buildReducer() {
  2. return asReducer(
  3. <Object, Reducer<ListState>>{
  4. ListAction.updateItem: _updateItem,
  5. },
  6. );
  7. }
  8. ListState _updateItem(ListState state, Action action) {
  9. return state.clone()..items = action.payload;
  10. }

列表修改-单item刷新

效果图

  • 这次来演示列表的单item更新,没有网络请求的操作,所以代码逻辑就相当简单了

结构

  • 来看看代码结构

  • 这地方很明显得发现,list_edit主体文件很少,因为这边直接在state里初始化了数据源,就没有后期更新数据的操作,所以就不需要:action,effect,reducer这三个文件!item模块则直接在reducer里更新数据,不涉及相关复杂的逻辑,所以不需要:effect文件。

列表模块

  • 这次列表模块是非常的简单,基本不涉及什么流程,就是最基本初始化的一个过程,将state里初始化的数据在view中展示

    • state ---> view
  • state

    • 老规矩,先来看看state中的代码
    • 这里一些新建了变量,泛型是ItemState(item的State),items变量初始化了一组数据;然后,同样继承了MutableSource,实现其相关方法
  1. class ListEditState extends MutableSource implements Cloneable<ListEditState> {
  2. List<ItemState> items;
  3. @override
  4. ListEditState clone() {
  5. return ListEditState()..items = items;
  6. }
  7. @override
  8. Object getItemData(int index) => items[index];
  9. @override
  10. String getItemType(int index) => ListItemAdapter.itemName;
  11. @override
  12. int get itemCount => items.length;
  13. @override
  14. void setItemData(int index, Object data) {
  15. items[index] = data;
  16. }
  17. }
  18. ListEditState initState(Map<String, dynamic> args) {
  19. return ListEditState()
  20. ..items = [
  21. ItemState(id: 1, title: "列表Item-1", itemStatus: false),
  22. ItemState(id: 2, title: "列表Item-2", itemStatus: false),
  23. ItemState(id: 3, title: "列表Item-3", itemStatus: false),
  24. ItemState(id: 4, title: "列表Item-4", itemStatus: false),
  25. ItemState(id: 5, title: "列表Item-5", itemStatus: false),
  26. ItemState(id: 6, title: "列表Item-6", itemStatus: false),
  27. ];
  28. }
  • view

    • view的代码主体仅仅是个ListView.builder,没有什么额外Widget
  1. Widget buildView(ListEditState state, Dispatch dispatch, ViewService viewService) {
  2. return Scaffold(
  3. appBar: AppBar(
  4. title: Text("ListEditPage"),
  5. ),
  6. body: ListView.builder(
  7. itemBuilder: viewService.buildAdapter().itemBuilder,
  8. itemCount: viewService.buildAdapter().itemCount,
  9. ),
  10. );
  11. }
  • adapter

    • 和上面类型,adapter继承SourceFlowAdapter适配器
  1. class ListItemAdapter extends SourceFlowAdapter<ListEditState> {
  2. static const String itemName = "item";
  3. ListItemAdapter()
  4. : super(
  5. pool: <String, Component<Object>>{itemName: ItemComponent()},
  6. );
  7. }
  • page

    • 在page里面绑定adapter
  1. class ListEditPage extends Page<ListEditState, Map<String, dynamic>> {
  2. ListEditPage()
  3. : super(
  4. initState: initState,
  5. view: buildView,
  6. dependencies: Dependencies<ListEditState>(
  7. ///绑定适配器
  8. adapter: NoneConn<ListEditState>() + ListItemAdapter(),
  9. slots: <String, Dependent<ListEditState>>{}),
  10. middleware: <Middleware<ListEditState>>[],
  11. );
  12. }

item模块

  • 接下就是比较重要的item模块了,item模块的流程,也是非常的清晰

    • view ---> action ---> reducer
  • state
    • 老规矩,先来看看state里面的代码;此处就是写常规变量的定义,这些在view中都能用得着
  1. class ItemState implements Cloneable<ItemState> {
  2. int id;
  3. String title;
  4. bool itemStatus;
  5. ItemState({this.id, this.title, this.itemStatus});
  6. @override
  7. ItemState clone() {
  8. return ItemState()
  9. ..title = title
  10. ..itemStatus = itemStatus
  11. ..id = id;
  12. }
  13. }
  14. ItemState initState(Map<String, dynamic> args) {
  15. return ItemState();
  16. }
  • view

    • 可以看到Checkbox的内部点击操作,我们传递了一个id参数,注意这个id参数是必须的,在更新item的时候来做区分用的
  1. Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
  2. return Container(
  3. child: InkWell(
  4. onTap: () {},
  5. child: ListTile(
  6. title: Text(state.title),
  7. trailing: Checkbox(
  8. value: state.itemStatus,
  9. ///Checkbox的点击操作:状态变更
  10. onChanged: (value) => dispatch(ItemActionCreator.onChange(state.id)),
  11. ),
  12. ),
  13. ),
  14. );
  15. }
  • action

    • 一个状态改变的事件
  1. enum ItemAction { onChange }
  2. class ItemActionCreator {
  3. //状态改变
  4. static Action onChange(int id) {
  5. return Action(ItemAction.onChange, payload: id);
  6. }
  7. }
  • reducer

    • _onChange会回调所有ItemState,所以这地方必须用id或其它唯一标识去界定,我们所操作的item具体是哪一个
    • _onChange方法,未操作的item返回的时候要注意,需要返回:state原对象,标明该state对象未变动,其item不需要刷新;不能返回state.clone(),这样返回的就是个全新的state对象,每个item都会刷新,还会造成一个很奇怪的bug,会造成后续点击item操作失灵
  1. Reducer<ItemState> buildReducer() {
  2. return asReducer(
  3. <Object, Reducer<ItemState>>{
  4. ItemAction.onChange: _onChange,
  5. },
  6. );
  7. }
  8. ItemState _onChange(ItemState state, Action action) {
  9. if (state.id == action.payload) {
  10. return state.clone()..itemStatus = !state.itemStatus;
  11. }
  12. ///这地方一定要注意,要返回:state;不能返回:state.clone(),否则会造成后续更新失灵
  13. return state;
  14. }

多样式列表

注意:如果使用多样式,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数据时,会报类型不适配的错误

下述代码可做思路参考

  1. class ListState extends MutableSource implements Cloneable<PackageCardState> {
  2. List<Object> items;
  3. @override
  4. ListState clone() {
  5. return PackageCardState()..items = items;
  6. }
  7. @override
  8. Object getItemData(int index) => items[index];
  9. @override
  10. String getItemType(int index) {
  11. if(items[index] is OneState) {
  12. return PackageCardAdapter.itemStyleOne;
  13. }else{
  14. return PackageCardAdapter.itemStyleTwo;
  15. }
  16. }
  17. @override
  18. int get itemCount => items.length;
  19. @override
  20. void setItemData(int index, Object data) => items[index] = data;
  21. }

列表存在的问题+解决方案

列表多item刷新问题

这里搞定了单item刷新场景,还存在一种多item刷新的场景

  • 说明下,列表item是没办法一次刷新多个item的,只能一次刷新一个item(一个clone对应着一次刷新),一个事件对应着刷新一个item;这边是打印多个日志分析出来了
  • 解决:解决办法是,多个事件去处理刷新操作

举例:假设一种场景,对于上面的item只能单选,一个item项被选中,其它item状态被重置到未选状态,具体效果看下方效果图

  • 效果图

  • 这种效果的实现非常简单,但是如果思路不对,会掉进坑里出不来

  • 还原被选的状态,不能在同一个事件里写,需要新写一个清除事件

下述代码为整体流程

  • view
  1. Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
  2. return InkWell(
  3. onTap: () {},
  4. child: ListTile(
  5. title: Text(state.title),
  6. trailing: Checkbox(
  7. value: state.itemStatus,
  8. ///CheckBox的点击操作:状态变更
  9. onChanged: (value) {
  10. //单选模式,清除选中的item,以便做单选
  11. dispatch(ItemActionCreator.clear());
  12. //刷新选中item
  13. dispatch(ItemActionCreator.onChange(state.id));
  14. }
  15. ),
  16. ),
  17. );
  18. }
  • action
  1. enum ItemAction {
  2. onChange,
  3. clear,
  4. }
  5. class ItemActionCreator {
  6. //状态改变
  7. static Action onChange(int id) {
  8. return Action(ItemAction.onChange, payload: id);
  9. }
  10. //清除改变的状态
  11. static Action clear() {
  12. return Action(ItemAction.clear);
  13. }
  14. }
  • reducer
  1. Reducer<ItemState> buildReducer() {
  2. return asReducer(
  3. <Object, Reducer<ItemState>>{
  4. ItemAction.onChange: _onChange,
  5. ItemAction.clear: _clear,
  6. },
  7. );
  8. }
  9. ItemState _onChange(ItemState state, Action action) {
  10. if (state.id == action.payload) {
  11. return state.clone()..itemStatus = !state.itemStatus;
  12. }
  13. ///这地方一定要注意,要返回:state;不能返回:state.clone(),否则会造成后续更新失灵
  14. return state;
  15. }
  16. ///单选模式
  17. ItemState _clear(ItemState state, Action action) {
  18. if (state.itemStatus) {
  19. return state.clone()..itemStatus = false;
  20. }
  21. ///这地方一定要注意,要返回:state;不能返回:state.clone(),否则会造成后续更新失灵
  22. return state;
  23. }

这个问题实际上解决起来很简单,但是如果一直在 _onChange 方法重置状态,你会发现和你预期的结果一直对不上;完整且详细的效果,可以去看demo里面代码

搞定

  • 呼,终于将列表这块写完,说实话,这个列表的使用确实有点麻烦;实际上,如果大家用心看了的话,麻烦的地方,其实就是在这块:adapter创建及其绑定;只能多写写了,熟能生巧!

  • 列表模块大功告成,以后就能愉快的写列表了!

全局模式

效果图

  • 理解了上面的是三个例子,相信大部分页面,对于你来说都不在话下了;现在我们再来看个例子,官方提供的全局主题功能,当然,这不仅仅是全局主题,全局字体样式,字体大小等等,都是可以全局管理,当然了,写app之前要做好规划

开搞

store模块

  • 文件结构

    • 这地方需要新建一个文件夹,新建四个文件:action,reducer,state,store

  • state

    • 老规矩,先来看看state,我们这里只在抽象类里面定义了一个主题色,这个抽象类是很重要的,需要做全局模式所有子模块的state,都必须实现这个抽象类
  1. abstract class GlobalBaseState{
  2. Color themeColor;
  3. }
  4. class GlobalState implements GlobalBaseState, Cloneable<GlobalState>{
  5. @override
  6. Color themeColor;
  7. @override
  8. GlobalState clone() {
  9. return GlobalState();
  10. }
  11. }
  • action

    • 因为只做切换主题色,这地方只需要定义一个事件即可
  1. enum GlobalAction { changeThemeColor }
  2. class GlobalActionCreator{
  3. static Action onChangeThemeColor(){
  4. return const Action(GlobalAction.changeThemeColor);
  5. }
  6. }
  • reducer

    • 这里就是处理变色的一些操作,这是咸鱼官方demo里面代码;这说明简单的逻辑,是可以放在reducer里面写的
  1. import 'package:flutter/material.dart' hide Action;
  2. Reducer<GlobalState> buildReducer(){
  3. return asReducer(
  4. <Object, Reducer<GlobalState>>{
  5. GlobalAction.changeThemeColor: _onChangeThemeColor,
  6. },
  7. );
  8. }
  9. List<Color> _colors = <Color>[
  10. Colors.green,
  11. Colors.red,
  12. Colors.black,
  13. Colors.blue
  14. ];
  15. GlobalState _onChangeThemeColor(GlobalState state, Action action) {
  16. final Color next =
  17. _colors[((_colors.indexOf(state.themeColor) + 1) % _colors.length)];
  18. return state.clone()..themeColor = next;
  19. }
  • store

    • 切换全局状态的时候,就需要调用这个类了
  1. /// 建立一个AppStore
  2. /// 目前它的功能只有切换主题
  3. class GlobalStore{
  4. static Store<GlobalState> _globalStore;
  5. static Store<GlobalState> get store => _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer());
  6. }

main改动

  • 这里面将PageRoutes里面的visitor字段使用起来,状态更新操作代码有点多,就单独提出来了;所以main文件里面,增加了:

    • visitor字段使用
    • 增加_updateState方法
  1. void main() {
  2. runApp(createApp());
  3. }
  4. Widget createApp() {
  5. ///全局状态更新
  6. _updateState() {
  7. return (Object pageState, GlobalState appState) {
  8. final GlobalBaseState p = pageState;
  9. if (pageState is Cloneable) {
  10. final Object copy = pageState.clone();
  11. final GlobalBaseState newState = copy;
  12. if (p.themeColor != appState.themeColor) {
  13. newState.themeColor = appState.themeColor;
  14. }
  15. /// 返回新的 state 并将数据设置到 ui
  16. return newState;
  17. }
  18. return pageState;
  19. };
  20. }
  21. final AbstractRoutes routes = PageRoutes(
  22. ///全局状态管理:只有特定的范围的Page(State继承了全局状态),才需要建立和 AppStore 的连接关系
  23. visitor: (String path, Page<Object, dynamic> page) {
  24. if (page.isTypeof<GlobalBaseState>()) {
  25. ///建立AppStore驱动PageStore的单向数据连接: 参数1 AppStore 参数2 当AppStore.state变化时,PageStore.state该如何变化
  26. page.connectExtraStore<GlobalState>(GlobalStore.store, _updateState());
  27. }
  28. },
  29. ///定义路由
  30. pages: <String, Page<Object, dynamic>>{
  31. ///导航页面
  32. "GuidePage": GuidePage(),
  33. ///计数器模块演示
  34. "CountPage": CountPage(),
  35. ///页面传值跳转模块演示
  36. "FirstPage": FirstPage(),
  37. "SecondPage": SecondPage(),
  38. ///列表模块演示
  39. "ListPage": ListPage(),
  40. },
  41. );
  42. return MaterialApp(
  43. title: 'FishRedux',
  44. home: routes.buildPage("GuidePage", null), //作为默认页面
  45. onGenerateRoute: (RouteSettings settings) {
  46. //ios页面切换风格
  47. return CupertinoPageRoute(builder: (BuildContext context) {
  48. return routes.buildPage(settings.name, settings.arguments);
  49. });
  50. },
  51. );
  52. }

子模块使用

  • 这里就用计数器模块的来举例,因为仅仅只需要改动少量代码,且只涉及state和view,所以其它模块代码也不重复贴出了
  • state
    • 这地方,仅仅让CountState多实现了GlobalBaseState类,很小的改动
  1. class CountState implements Cloneable<CountState>,GlobalBaseState {
  2. int count;
  3. @override
  4. CountState clone() {
  5. return CountState()..count = count;
  6. }
  7. @override
  8. Color themeColor;
  9. }
  10. CountState initState(Map<String, dynamic> args) {
  11. return CountState()..count = 0;
  12. }
  • view

    • 这里面仅仅改动了一行,在AppBar里面加了backgroundColor,然后使用state里面的全局主题色
  1. Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
  2. return _bodyWidget(state, dispatch);
  3. }
  4. Widget _bodyWidget(CountState state, Dispatch dispatch) {
  5. return Scaffold(
  6. appBar: AppBar(
  7. title: Text("FishRedux"),
  8. ///全局主题,仅仅在此处改动了一行
  9. backgroundColor: state.themeColor,
  10. ),
  11. body: Center(
  12. child: Column(
  13. mainAxisAlignment: MainAxisAlignment.center,
  14. children: <Widget>[
  15. Text('You have pushed the button this many times:'),
  16. Text(state.count.toString()),
  17. ],
  18. ),
  19. ),
  20. floatingActionButton: FloatingActionButton(
  21. onPressed: () {
  22. ///点击事件,调用action 计数自增方法
  23. dispatch(CountActionCreator.updateCount());
  24. },
  25. child: Icon(Icons.add),
  26. ),
  27. );
  28. }
  • 如果其他模块也需要做主题色,也按照此处逻辑改动即可

调用

  • 调用状态更新就非常简单了,和正常模块更新View一样,这里我们调用全局的就行了,一行代码搞定,在需要的地方调用就OK了
  1. GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());

搞定

  • 经过上面的的三步,我们就可以使用全局状态了;从上面子模块的使用,可以很明显的感受到,全局状态,必须前期做好字段的规划,确定之后,最好不要再增加字段,不然继承抽象类的多个模块都会爆红,提示去实现xxx变量

全局模块优化

反思

在上面的全局模式里说了,使用全局模块,前期需要规划好字段,不然项目进行到中期的时候,想添加字段,多个模块的State会出现大范围爆红,提示去实现你添加的字段;项目开始规划好所有的字段,显然这需要全面的考虑好大部分场景,但是人的灵感总是无限的,不改代码是不可能,这辈子都不可能。只能想办法看能不能添加一次字段后,后期添加字段,并不会引起其他模块爆红,试了多次,成功的使用中间实体,来解决该问题

这里优化俩个方面

  • 使用通用的全局实体

    • 这样后期添加字段,就不会影响其他模块,这样我们就能一个个模块的去整改,不会出现整个项目不能运行的情况
  • 将路由模块和全局模块封装
    • 路由模块后期页面多了,代码会很多,放在主入口,真的不好管理;全局模块同理

因为使用中间实体,有一些地方会出现空指针问题,我都在流程里面写清楚了,大家可以把优化流程完整看一遍哈,都配置好,后面拓展使用就不会报空指针了

优化

入口模块

  • main:大改

    • 从下面代码可以看到,这里将路由模块和全局模块单独提出来了,这地方为了方便观看,就写在一个文件里;说明下,RouteConfig和StoreConfig这俩个类,可以放在俩个不同的文件里,这样管理路由和全局字段更新就会很方便了!
    • RouteConfig:这里将页面标识和页面映射分开写,这样我们跳转页面的时候,就可以直接引用RouteConfig里面的页面标识
    • StoreConfig:全局模块里最重要的就是状态的判断,注释写的很清楚了,可以看看注释哈
  1. void main() {
  2. runApp(createApp());
  3. }
  4. Widget createApp() {
  5. return MaterialApp(
  6. title: 'FishRedux',
  7. home: RouteConfig.routes.buildPage(RouteConfig.guidePage, null), //作为默认页面
  8. onGenerateRoute: (RouteSettings settings) {
  9. //ios页面切换风格
  10. return CupertinoPageRoute(builder: (BuildContext context) {
  11. return RouteConfig.routes.buildPage(settings.name, settings.arguments);
  12. });
  13. },
  14. );
  15. }
  16. ///路由管理
  17. class RouteConfig {
  18. ///定义你的路由名称比如 static final String routeHome = 'page/home';
  19. ///导航页面
  20. static const String guidePage = 'page/guide';
  21. ///计数器页面
  22. static const String countPage = 'page/count';
  23. ///页面传值跳转模块演示
  24. static const String firstPage = 'page/first';
  25. static const String secondPage = 'page/second';
  26. ///列表模块演示
  27. static const String listPage = 'page/list';
  28. static const String listEditPage = 'page/listEdit';
  29. static final AbstractRoutes routes = PageRoutes(
  30. pages: <String, Page<Object, dynamic>>{
  31. ///将你的路由名称和页面映射在一起,比如:RouteConfig.homePage : HomePage(),
  32. RouteConfig.guidePage: GuidePage(),
  33. RouteConfig.countPage: CountPage(),
  34. RouteConfig.firstPage: FirstPage(),
  35. RouteConfig.secondPage: SecondPage(),
  36. RouteConfig.listPage: ListPage(),
  37. RouteConfig.listEditPage: ListEditPage(),
  38. },
  39. visitor: StoreConfig.visitor,
  40. );
  41. }
  42. ///全局模式
  43. class StoreConfig {
  44. ///全局状态管理
  45. static _updateState() {
  46. return (Object pageState, GlobalState appState) {
  47. final GlobalBaseState p = pageState;
  48. if (pageState is Cloneable) {
  49. final Object copy = pageState.clone();
  50. final GlobalBaseState newState = copy;
  51. if (p.store == null) {
  52. ///这地方的判断是必须的,判断第一次store对象是否为空
  53. newState.store = appState.store;
  54. } else {
  55. /// 这地方增加字段判断,是否需要更新
  56. if ((p.store.themeColor != appState.store.themeColor)) {
  57. newState.store.themeColor = appState.store.themeColor;
  58. }
  59. /// 如果增加字段,同理上面的判断然后赋值...
  60. }
  61. /// 返回新的 state 并将数据设置到 ui
  62. return newState;
  63. }
  64. return pageState;
  65. };
  66. }
  67. static visitor(String path, Page<Object, dynamic> page) {
  68. if (page.isTypeof<GlobalBaseState>()) {
  69. ///建立AppStore驱动PageStore的单向数据连接
  70. ///参数1 AppStore 参数2 当AppStore.state变化时,PageStore.state该如何变化
  71. page.connectExtraStore<GlobalState>(GlobalStore.store, _updateState());
  72. }
  73. }
  74. }

Store模块

下面俩个模块是需要改动代码的模块

  • state

    • 这里使用了StoreModel中间实体,注意,这地方实体字段store,初始化是必须的,不然在子模块引用该实体下的字段会报空指针
  1. abstract class GlobalBaseState{
  2. StoreModel store;
  3. }
  4. class GlobalState implements GlobalBaseState, Cloneable<GlobalState>{
  5. @override
  6. GlobalState clone() {
  7. return GlobalState();
  8. }
  9. @override
  10. StoreModel store = StoreModel(
  11. /// store这个变量,在这必须示例化,不然引用该变量中的字段,会报空指针
  12. /// 下面的字段,赋初值,就是初始时展示的全局状态
  13. /// 这地方初值,理应从缓存或数据库中取,表明用户选择的全局状态
  14. themeColor: Colors.lightBlue
  15. );
  16. }
  17. ///中间全局实体
  18. ///需要增加字段就在这个实体里面添加就行了
  19. class StoreModel {
  20. Color themeColor;
  21. StoreModel({this.themeColor});
  22. }
  • reducer

    • 这地方改动非常小,将state.themeColor改成state.store.themeColor
  1. Reducer<GlobalState> buildReducer(){
  2. return asReducer(
  3. <Object, Reducer<GlobalState>>{
  4. GlobalAction.changeThemeColor: _onChangeThemeColor,
  5. },
  6. );
  7. }
  8. List<Color> _colors = <Color>[
  9. Colors.green,
  10. Colors.red,
  11. Colors.black,
  12. Colors.blue
  13. ];
  14. GlobalState _onChangeThemeColor(GlobalState state, Action action) {
  15. final Color next =
  16. _colors[((_colors.indexOf(state.store.themeColor) + 1) % _colors.length)];
  17. return state.clone()..store.themeColor = next;
  18. }

下面俩个模块代码没有改动,但是为了思路完整,同样贴出来

  • action
  1. enum GlobalAction { changeThemeColor }
  2. class GlobalActionCreator{
  3. static Action onChangeThemeColor(){
  4. return const Action(GlobalAction.changeThemeColor);
  5. }
  6. }
  • store
  1. class GlobalStore{
  2. static Store<GlobalState> _globalStore;
  3. static Store<GlobalState> get store => _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer());
  4. }

子模块使用

  • 这里就用计数器模块的来举例,因为仅仅只需要改动少量代码,且只涉及state和view,所以其它模块代码也不重复贴出了
  • state
    • 因为是用中间实体,所以在clone方法里面必须将实现的store字段加上,不然会报空指针
  1. class CountState implements Cloneable<CountState>, GlobalBaseState {
  2. int count;
  3. @override
  4. CountState clone() {
  5. return CountState()
  6. ..count = count
  7. ..store = store;
  8. }
  9. @override
  10. StoreModel store;
  11. }
  12. CountState initState(Map<String, dynamic> args) {
  13. return CountState()..count = 0;
  14. }
  • view

    • 这里面仅仅改动了一行,在AppBar里面加了backgroundColor,然后使用state里面的全局主题色
  1. Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
  2. return _bodyWidget(state, dispatch);
  3. }
  4. Widget _bodyWidget(CountState state, Dispatch dispatch) {
  5. return Scaffold(
  6. appBar: AppBar(
  7. title: Text("FishRedux"),
  8. ///全局主题,仅仅在此处改动了一行
  9. backgroundColor: state.store.themeColor,
  10. ),
  11. ///下面其余代码省略....
  12. }
  • 如果其他模块也需要做主题色,也按照此处逻辑改动即可

调用

  • 调用和上面说的一样,用下述全局方式在合适的地方调用
  1. GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());

体验

通过上面的优化,使用体验提升不是一个级别,大大提升的全局模式的扩展性,我们就算后期增加了大量的全局字段,也可以一个个模块慢慢改,不用一次爆肝全改完,猝死的概率又大大减少了!

Component使用

Component是个比较常用的模块,上面使用列表的时候,就使用到了Component,这次我们来看看,在页面中直接使用Component,可插拔式使用!Component的使用总的来说是比较简单了,比较关键的是在State中建立起连接。

效果图

  • 上图的效果是在页面中嵌入了俩个Component,改变子Component的操作是在页面中完成的
  • 先看下页面结构

Component

这地方写了一个Component,代码很简单,来看看吧

  • component

这地方代码是自动生成了,没有任何改动,就不贴了

  • state

    • initState():我们需要注意,Component中的initState()方法在内部没有调用,虽然自动生成的代码有这个方法,但是无法起到初始化作用,可以删掉该方法
  1. class AreaState implements Cloneable<AreaState> {
  2. String title;
  3. String text;
  4. Color color;
  5. AreaState({
  6. this.title = "",
  7. this.color = Colors.blue,
  8. this.text = "",
  9. });
  10. @override
  11. AreaState clone() {
  12. return AreaState()
  13. ..color = color
  14. ..text = text
  15. ..title = title;
  16. }
  17. }
  • view
  1. Widget buildView(
  2. AreaState state, Dispatch dispatch, ViewService viewService) {
  3. return Scaffold(
  4. appBar: AppBar(
  5. title: Text(state.title),
  6. automaticallyImplyLeading: false,
  7. ),
  8. body: Container(
  9. height: double.infinity,
  10. width: double.infinity,
  11. alignment: Alignment.center,
  12. color: state.color,
  13. child: Text(state.text),
  14. ),
  15. );
  16. }

Page

CompPage中,没用到effete这层,就没创建该文件,老规矩,先看看state

  • state

    • 这地方是非常重要的地方,XxxxConnecto的实现形式是看官方代码写的
    • computed():该方法是必须实现的,这个类似直接的get()方法,但是切记不能像get()直接返回state.leftAreaState()或state.rightAreaState,某些场景初始化无法刷新,因为是同一个对象,会被判断未更改,所以会不刷新控件
      • 注意了注意了,这边做了优化,直接返回clone方法,这是对官方赋值写法的一个优化,也可以避免上面说的问题,大家可以思考思考
    • set():该方法是Component数据流回推到页面的state,保持俩者state数据一致;如果Component模块更新了自己的State,不写这个方法会报错的
  1. class CompState implements Cloneable<CompState> {
  2. AreaState leftAreaState;
  3. AreaState rightAreaState;
  4. @override
  5. CompState clone() {
  6. return CompState()
  7. ..rightAreaState = rightAreaState
  8. ..leftAreaState = leftAreaState;
  9. }
  10. }
  11. CompState initState(Map<String, dynamic> args) {
  12. ///初始化数据
  13. return CompState()
  14. ..rightAreaState = AreaState(
  15. title: "LeftAreaComponent",
  16. text: "LeftAreaComponent",
  17. color: Colors.indigoAccent,
  18. )
  19. ..leftAreaState = AreaState(
  20. title: "RightAreaComponent",
  21. text: "RightAreaComponent",
  22. color: Colors.blue,
  23. );
  24. }
  25. ///左边Component连接器
  26. class LeftAreaConnector extends ConnOp<CompState, AreaState>
  27. with ReselectMixin<CompState, AreaState> {
  28. @override
  29. AreaState computed(CompState state) {
  30. return state.leftAreaState.clone();
  31. }
  32. @override
  33. void set(CompState state, AreaState subState) {
  34. state.leftAreaState = subState;
  35. }
  36. }
  37. ///右边Component连接器
  38. class RightAreaConnector extends ConnOp<CompState, AreaState>
  39. with ReselectMixin<CompState, AreaState> {
  40. @override
  41. AreaState computed(CompState state) {
  42. return state.rightAreaState.clone();
  43. }
  44. @override
  45. void set(CompState state, AreaState subState) {
  46. state.rightAreaState = subState;
  47. }
  48. }
  • page

    • 写完连接器后,我们在Page里面绑定下,就能使用Component了
  1. class CompPage extends Page<CompState, Map<String, dynamic>> {
  2. CompPage()
  3. : super(
  4. initState: initState,
  5. reducer: buildReducer(),
  6. view: buildView,
  7. dependencies: Dependencies<CompState>(
  8. adapter: null,
  9. slots: <String, Dependent<CompState>>{
  10. //绑定Component
  11. "leftArea": LeftAreaConnector() + AreaComponent(),
  12. "rightArea": RightAreaConnector() + AreaComponent(),
  13. }),
  14. middleware: <Middleware<CompState>>[],
  15. );
  16. }
  • view

    • 使用Component就非常简单了:viewService.buildComponent("xxxxxx")
  1. Widget buildView(CompState state, Dispatch dispatch, ViewService viewService) {
  2. return Container(
  3. color: Colors.white,
  4. child: Column(
  5. children: [
  6. ///Component组件部分
  7. Expanded(
  8. flex: 3,
  9. child: Row(
  10. children: [
  11. Expanded(child: viewService.buildComponent("leftArea")),
  12. Expanded(child: viewService.buildComponent("rightArea")),
  13. ],
  14. ),
  15. ),
  16. ///按钮
  17. Expanded(
  18. flex: 1,
  19. child: Center(
  20. child: RawMaterialButton(
  21. fillColor: Colors.blue,
  22. shape: StadiumBorder(),
  23. onPressed: () => dispatch(CompActionCreator.change()),
  24. child: Text("改变"),
  25. ),
  26. ))
  27. ],
  28. ),
  29. );
  30. }
  • action
  1. enum CompAction { change }
  2. class CompActionCreator {
  3. static Action change() {
  4. return const Action(CompAction.change);
  5. }
  6. }
  • reducer
  1. Reducer<CompState> buildReducer() {
  2. return asReducer(
  3. <Object, Reducer<CompState>>{
  4. CompAction.change: _change,
  5. },
  6. );
  7. }
  8. CompState _change(CompState state, Action action) {
  9. final CompState newState = state.clone();
  10. //改变leftAreaComponent中state
  11. newState.leftAreaState.text = "LeftAreaState:${Random().nextInt(1000)}";
  12. newState.leftAreaState.color =
  13. Color.fromRGBO(randomColor(), randomColor(), randomColor(), 1);
  14. //改变rightAreaComponent中state
  15. newState.rightAreaState.text = "RightAreaState:${Random().nextInt(1000)}";
  16. newState.rightAreaState.color =
  17. Color.fromRGBO(randomColor(), randomColor(), randomColor(), 1);
  18. return newState;
  19. }
  20. int randomColor() {
  21. return Random().nextInt(255);
  22. }

总结下

总的来说,Component的使用还是比较简单的;如果我们把某个复杂的列表提炼出一个Component的,很明显有个初始化的过程,这里我们需要将:请求参数调体或列表详情操作,在page页面处理好,然后再更新给我们绑定的子Component的State,这样就能起到初始化某个模块的作用;至于刷新,下拉等后续操作,就让Component内部自己去处理了

广播

fish_redux中是带有广播的通信方式,使用的方式很简单,这本是effect层,ctx参数自带的一个api,这里简单介绍一下

使用

  • action

    • 广播事件单独写了一个action文件,便于统一管理
  1. enum BroadcastAction { toNotify }
  2. class BroadcastActionCreator {
  3. ///广播通知
  4. static Action toNotify(String msg) {
  5. return Action(BroadcastAction.toNotify, payload: msg);
  6. }
  7. }
  • 发送广播

    • 这是页面跳转的方法,就在此处写了,如果想看详细代码的话,可以去demo地址里面看下
  1. void _backFirst(Action action, Context<SecondState> ctx) {
  2. //广播通信
  3. ctx.broadcast(BroadcastActionCreator.toNotify("页面二发送广播通知"));
  4. }
  • 接受广播
  1. Effect<FirstState> buildEffect() {
  2. return combineEffects(<Object, Effect<FirstState>>{
  3. //接受发送的广播消息
  4. BroadcastAction.toNotify: _receiveNotify,
  5. });
  6. }
  7. void _receiveNotify(Action action, Context<FirstState> ctx) async {
  8. ///接受广播
  9. print("跳转一页面:${action.payload}");
  10. }

说明

广播的使用还是挺简单的,基本和dispatch的使用是一致的,dispatch是模块的,而broadcast是有页面栈,就能通知其他页面,很多情况下,我们在一个页面进行了操作,其他页面也需要同步做一些处理,使用广播就很简单了

注意: 广播发送和接受是一对多的关系,一处发送,可以在多处接受;和dispatch发送事件,如果在effect里面接受,在reducer就无法接受的情况是不一样的(被拦截了)

开发小技巧

弱化reducer

无限弱化了reducer层作用

  • 在日常使用fish_redux和flutter_bloc后,实际能深刻体会reducer层实际上只是相当于bloc中yield

    或emit关键字的作用,职能完全可以弱化为,仅仅作为状态刷新;这样可以大大简化开发流程,只需要关注

    view -> action -> effect (reducer:使用统一的刷新事件)
  • 下面范例代码,处理数据的操作直接在effect层处理,如需要更改数据,直接对ctx.state进行操作,涉及刷新页面的操作,统一调用onRefresh事件;对于一个页面有几十个表单的情况,这种操作,能大大提升你的开发速度和体验,亲身体验,大家可以尝试下
  1. Reducer<TestState> buildReducer() {
  2. return asReducer(
  3. <Object, Reducer<TestState>>{
  4. TestAction.onRefresh: _onRefresh,
  5. },
  6. );
  7. }
  8. TestState _onRefresh(TreeState state, Action action) {
  9. return state.clone();
  10. }
  • 具体可以查看 玩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

  • 大家如果觉得有收获,就给我点个赞吧!你的点赞,是我码字的最大动力!

fish_redux使用详解---看完就会用!的更多相关文章

  1. Python虚拟环境和包管理工具Pipenv的使用详解--看完这一篇就够了

    前言 Python虚拟环境是一个虚拟化,从电脑独立开辟出来的环境.在这个虚拟环境中,我们可以pip安装各个项目不同的依赖包,从全局中隔离出来,利于管理. 传统的Python虚拟环境有virtualen ...

  2. 【甘道夫】HBase基本数据操作详解【完整版,绝对精品】

    引言 之前详细写了一篇HBase过滤器的文章,今天把基础的表和数据相关操作补上. 本文档参考最新(截止2014年7月16日)的官方Ref Guide.Developer API编写. 所有代码均基于“ ...

  3. HBase基本数据操作详解【完整版,绝对精品】

    欢迎转载,请注明来源: http://blog.csdn.net/u010967382/article/details/37878701 概述 对于建表,和RDBMS类似,HBase也有namespa ...

  4. 【转载】HBase基本数据操作详解【完整版,绝对精品】

    转载自: http://blog.csdn.net/u010967382/article/details/37878701 概述 对于建表,和RDBMS类似,HBase也有namespace的概念,可 ...

  5. 第206天:http协议终极详解---看这一篇就够了

    HTTP简介 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送 ...

  6. kafka3.x原理详解看这篇就够了

    一.概述 (一).kafka的定义 1.定义 1)kafka传统的定义:kafka是一个分布式的基于发布/订阅模式的消息队列,主要用于大数据实时处理领域 2)kafka最新的定义:kafka是一个开源 ...

  7. nginx配置文件详解( 看着好长,其实不长,看了就知道了,精心整理,有些配置也是没用到呢 )

    user www www; #定义Nginx运行的用户和用户组        worker_processes ; #nginx进程数,建议设置为CPU核数2倍. error_log var/log/ ...

  8. smb.conf详解[未完]

    看着玩意看的吐血!!!! baidu\google充斥着一堆错误的文章及翻译,samba.org上动辄就是this document is old and might be incurrent. 不过 ...

  9. 史上最全IO流详解,看着一篇足矣

    一:要了解IO,首先了解File类 File类里面的部分常量,方法 No. 方法或常量 类型 描述 1 public static final String pathSeparator 常量 表示路径 ...

随机推荐

  1. openstack核心组件——keystone身份认证部署服务(5)

    node1主机执行 1.mysql -u root -p 2.create database keystone; 创建数据库 MariaDB [(none)]> show databases; ...

  2. vue 中后台 列表的增删改查同一解决方案

    查看 & 查询 常⻅业务列表⻚都是由 搜索栏 和 数据列表 组成. 其中: 搜索栏包含 搜索条件 . 新增 . 批量xx . 导出 等对 数据列表 全局操作功能项. 数据列表包含 分⻚ 和每条 ...

  3. elasticsearch 索引清理脚本及常用命令

    elastic索引日志清理不及时,很容易产生磁盘紧张,官网给出curl -k -XDELETE可以清理不需要的索引日志. 清理脚本 #!/bin/bash #Author: 648403020@qq. ...

  4. P4742 【[Wind Festival]Running In The Sky】

    相信来做这道题的人肯定都学过\(Tarjan\)缩点吧,如果没有建议先去做P3387 [模板]缩点,如果你忘了,建议也去看看 满足上面要求后,你会惊奇发现,这两道题基本一样,唯一的差别就是这道题需要记 ...

  5. Docker:四、Docker进阶 Windows Docker IIS 部署

    前面的三篇docker 文档大家看的肯定不过瘾,自己可能也已经上手一试了...不知道有没有发现问题... 哈哈... 我来说说我遇到的问题哦 一.windows docker 镜像越来越大 默认的do ...

  6. python3-day3

    一.函数基本语法及特性 重复用到的代码通过def封装起来,用到的时候直接调用函数名字:语法 1 def 函数名字(内容): 2 需要执行的动作 什么是函数: 函数一词来源于数学,但编程中的「函数」概念 ...

  7. LightningChart运行互动示例介绍

    LightningChart.NET完全由GPU加速,并且性能经过优化,可用于实时显示海量数据-超过10亿个数据点. LightningChart包括广泛的2D,高级3D,Polar,Smith,3D ...

  8. 《kubernetes + .net core 》dev ops部分

    目录 1.kubernetes 预备知识 1.1 集群资源 1.1.1 role 1.1.2 namespace 1.1.3 node 1.1.4 persistent volume 1.1.5 st ...

  9. Layman PHP+JavaScript 实现图片无刷新上传

    html文件代码 <!-- ajax文件上传开始 --> <script type="text/javascript" src="/imageuploa ...

  10. sqli-labs第三关 详解

    通过第二关,来到第三关 我们用了前两种方法,都报错,然后自己也不太会别的注入,然后莫名的小知识又增加了.这居然是一个带括号的字符型注入, 这里我们需要闭合前面的括号. $sql=select * fr ...