关于flutter:fishredux使用详解看完就会用

2次阅读

共计 40105 个字符,预计需要花费 101 分钟才能阅读完成。

说句心里话,这篇文章,来来回回批改了很屡次,如果认真看完这篇文章,还不会写 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 暗藏 Page
import 'package:flutter/cupertino.dart'hide Page;
import 'package:flutter/material.dart' hide Page;

void main() {runApp(MyApp());
}

Widget createApp() {
  /// 定义路由
  final AbstractRoutes routes = PageRoutes(
    pages: <String, Page<Object, dynamic>>{"CountPage": CountPage(),
    },
  );

  return MaterialApp(
    title: 'FishDemo',
    home: routes.buildPage("CountPage", null), // 作为默认页面
    onGenerateRoute: (RouteSettings settings) {
      //ios 页面切换格调
      return CupertinoPageRoute(builder: (BuildContext context) {return routes.buildPage(settings.name, settings.arguments);
      })
//      Material 页面切换格调
//      return MaterialPageRoute<Object>(builder: (BuildContext context) {//        return routes.buildPage(settings.name, settings.arguments);
//      });
    },
  );
}
  • state

    • 定义咱们在页面展现的一些变量,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
  • 大家如果感觉有播种,就给我点个赞吧!你的点赞,是我码字的最大能源!

正文完
 0