fish_redux 入门:助你快速上手 fish_redux
前言
如果过于依赖思维惯性与经验主义,会在学习与接触新事物时造成很大的阻碍。它可能会蒙蔽我们的认知、减缓我们消化吸收的速度,更有甚者会凭白产生很多根本不存在的对立矛盾来消耗我们的精力,使得我们愈加抵触新事物。
所以,放下过去,以空杯心态去学习、认知新事物,才能更客观全面的了解消化。
fish_redux的相关链接:
Fish Redux本文适合那些对 Flutter 知识体系有初步了解的朋友,例如
- Flutter 中 State 是什么? StatelessWidget 与 StatefulWidget 之间的区别是啥?
- Flutter 的 Navigator 如何进行页面跳转?Flutter 略微复杂的页面开发,包含 UI 更新与数据处理等。
- 能流程使用 Dart 进行开发,了解 Dart 的异步 Future API。
本文作为 fish_redux 入门文章,并未涉及到 fish_redux 的高级用法。但是它能帮你对 fish_redux 的状态管理、事件分发等有个初步了解,并了解因何而用,如何用。
1、fish_redux
为什么要用 fish_redux ?
一个最简单的使用场景,你在一个 State 中使用耗时操作,例如网络交互、数据库查询等,在 then((){})
处理回调并调用 setState((){}})
更新UI,但是在回调时页面处于 deactivate
或者 dispose
状态,结果你的 Flutter 项目报错了,提示如下:
Unhandled Exception: setState() called after dispose(): _MockLeakedState#24ffc(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
引用泄漏或错误状态回调的解决办法有很多,例如在回调中判断 State 状态(State 中的 UI 与 逻辑代码耦合导致,仍然泄露)、监听生命周期及时释放回调等。专业方案有诸如 Provider 、 scoped_model 、 Bloc 等,而 fish_redux
则是最为出色的解决方案之一。在这里顺便推荐一下 flutter_boost 混合开发管理框架,阿里牛比比比!!!
fish_redux 功能虽然强大,但其 API 与设计思路较为复杂,官方介绍说其延续了前端 Redux 框架的思想。对于很多不熟悉 Redux 的朋友来说,还需要去了解 Redux ,但是框架这种东西没实际用过是很难了解的(需要一定的代码量)。为了帮助不熟悉 fish_redux 的朋友快速上手,于是就有了这篇文章,只要具有前言中提到的对 Flutter 知识体系有一定了解的朋友应该都可以快速上手 fish_redux。
2、<a id="mockLeaked">Mock 泄漏</a>
之后的小节,我会在每处都打个 tag ,在操作处与结尾处都会备注 tag 名称。本小节的 tag 为 mockLeaked
。
- 使用 Flutter 命令创建一个 Application 项目,在 yaml 中依赖fish_redux。
创建 fish_
建议使用 IDEA 打开项目,需要装有$ flutter create fish_redux_demo
Flutter
Dart
与FishReduxTemplate
这三个插件。编辑pubspec.yaml
文件:
在 terminal 中输入# 建议 sdk 版本 2.6.0 及其以上。可以使用 扩展函数,真香。 environment: sdk: ">=2.6.0 <3.0.0" dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 # 依赖 fish_redx fish_redux: ^0.3.3
flutter pub get
更新项目配置。 - 编辑页面,模拟在销毁时遇到的泄漏问题。
///--------------------main.dart 代码------------------------------ import 'package:fish_redux_demo/page/leaked_demo.dart'; import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'FishReduxDemo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'FishReduxDemo'), routes: { '/page/mockLeakedPage':(_)=>MockLeakedPage() }, ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { void _incrementCounter() { Navigator.pushNamed(context, '/page/mockLeakedPage'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( '点击跳转mockLeakedPage', ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'GoToMockLeakedPage', child: Icon(Icons.more_horiz), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } ///--------------------main.dart 代码------------------------------ ///--------------------main.dart 代码------------------------------ ///--------------------mock_leaked_demo.dart 代码------------------------------ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; class MockLeakedPage extends StatefulWidget { @override State<StatefulWidget> createState() => _MockLeakedState(); } class _MockLeakedState extends State<MockLeakedPage> { String _content = "MockLeakedPage"; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( '模拟泄漏', style: TextStyle(fontSize: 18), ), centerTitle: true, ), body: Container( alignment: Alignment(0, 0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text( _content, style: TextStyle(fontSize: 16), ), RaisedButton( child: Text('调用异步函数'), onPressed: () { Timer(Duration(seconds: 3), () { _content = '3秒延时已到'; setState(() {}); }); }, ) ], ), ), ); } } ///--------------------mock_leaked_demo.dart 代码------------------------------
运行 App,点击跳到模拟泄漏页面,点击 调用异步函数
按钮立马退出页面,就可以在logcat看到该异常
Unhandled Exception: setState() called after dispose(): _MockLeakedState#24ffc(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
3、<a id="fixLeaked">使用 fish_redux 解决泄漏</a>
本小节 tag 是 fixLeaked
。
对于不熟悉 fish_redux 的朋友,强烈推荐安装 FishReduxTemplate
插件,用于生成 fish_redux 的相关 API 文件。
- 安装好
FishReduxTemplate
插件之后,创建一个文件夹fixleaked
,右键该文件夹选择 New -> FishReduxTemplate 创建模板文件,选择 Page 类型,输入名称 FixLeaked 。生成文件如下。生成的dart
文件名称是固定的,但是类名根据输入的名称变化,所以命名请尽量做到见名知意。
创建fish_redux模板之一
创建fish_redux模板之二
创建fish_redux模板之三 - 这里暂不介绍这六个文件的作用,直接撸代码。关注数据源 ,编辑 state.dart 文件,FishRedux 要求提供一个 State 类,该类我们可以理解为 MVC 、 MVP 或 MVVM 中的 Model ,它的作用就是承载数据。我们在
FixLeakedState
类中创建一个公开的成员变量content
,需要注意在clone
函数中拷贝FixLeakedState
的成员属性值。该类的initState(Map<String, dynamic> args)
顶级函数,根据传递的参数创建FixLeakedState
初始对象来决定页面的初始状态。import 'package:fish_redux/fish_redux.dart'; class FixLeakedState implements Cloneable<FixLeakedState> { String content; @override FixLeakedState clone() { //级联语法给 clone 对象赋值 return FixLeakedState()..content = this.content; } } FixLeakedState initState(Map<String, dynamic> args) { return FixLeakedState()..content = "MockLeakedPage"; }
-
绘制 UI,修改 view.dart 文件 ,该文件用于创建 Widget 对象,提供一个顶级函数
Widget buildView(FixLeakedState state, Dispatch dispatch, ViewService viewService)
。参数列表:-
FixLeakedState
:FishRedux 在合适的时机会重新调用buildView
函数,我们根据 state 的状态去构造不同 UI 效果即可。至于合适的时机是啥时候,后文再说。 -
Dispatch
:一个派发函数对象,调用该对象,我们可以分发出不同的事件出去,FishRedux 会在 effect 或者 reducer 中注册监听事件。也就是说我们想把事件从 view 中派发出去,使用 Dispatch 对象就好啦。事件的 API 定义在 action 文件中。
import 'package:fish_redux/fish_redux.dart'; class FixLeakedState implements Cloneable<FixLeakedState> { String content; @override FixLeakedState clone() { return FixLeakedState()..content = this.content; } } FixLeakedState initState(Map<String, dynamic> args) { return FixLeakedState()..content = "MockLeakedPage"; }
-
ViewService
:带有 BuildContext 上下文对象的对象。 - 从该文件:我们就可知,
buildView(FixLeakedState, Dispatch, ViewService)
函数通过 state 对象来决定 UI 展示效果,而 Dispatch 对象用于帮助在顶级函数派发事件(Action),ViewService 提供了我们需要用到的 BuildContext 上下文对象。
import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action; import 'action.dart'; import 'state.dart'; Widget buildView(FixLeakedState state, Dispatch dispatch, ViewService viewService) { return Scaffold( appBar: AppBar( title: Text( '模拟泄漏', style: TextStyle(fontSize: 18), ), centerTitle: true, ), body: Container( alignment: Alignment(0, 0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text( state.content, style: TextStyle(fontSize: 16), ), RaisedButton( child: Text('调用异步函数'), onPressed: () { //dispatch something }, ) ], ), ), ); }
-
-
在
buildView
我们已经知道通过 Dispatch来分发事件了,那么事件如何定义与创建呢?答案在action.dart
中。action.dart
文件定义有两个类,一个枚举类和一个构造器类。
需要什么事件,我们可以定义在枚举类中,构造器类可以传入import 'package:fish_redux/fish_redux.dart'; enum FixLeakedAction { action } class FixLeakedActionCreator { static Action onAction() { return const Action(FixLeakedAction.action); } }
dynamic payload
负载来生成对应的枚举对象。///介绍一下 FishRedux 定义的 Action 类,该类看看就行了 ///需要一个 type 来区分事件, ///通过 dynamic 类型的 payload 来传递数据。 class Action { const Action(this.type, {this.payload}); final Object type; final dynamic payload; } ///实际上 action.dart 编辑后的内容, ///删除默认生成的 action 定义之后,注意 effect.dart 与 reduce.dart 使用到了默认 action ///注意删除掉它。 import 'package:fish_redux/fish_redux.dart'; enum FixLeakedAction { delay, modifyContent, } class FixLeakedActionCreator { ///创建 delay action,模拟耗时任务 static Action delay() { return const Action(FixLeakedAction.delay); } ///创建修改 content 的 action static Action modifyContent(String content) { return Action(FixLeakedAction.delay, payload: content); } }
- 回到
buildView
方法中,通过按钮RaisedButton
将模拟延时的事件通过 Dispatch 对象派发出去。import 'package:fish_redux/fish_redux.dart'; import 'package:flutter/material.dart' hide Action; import 'action.dart'; import 'state.dart'; Widget buildView(FixLeakedState state, Dispatch dispatch, ViewService viewService) { return Scaffold( appBar: AppBar( title: Text( '模拟泄漏', style: TextStyle(fontSize: 18), ), centerTitle: true, ), body: Container( alignment: Alignment(0, 0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text( state.content, style: TextStyle(fontSize: 16), ), RaisedButton( child: Text('调用异步函数'), onPressed: () { dispatch(FixLeakedActionCreator.delay()); }, ) ], ), ), ); }
-
事件分发之后,在
effect.dart
与reducer.dart
来处理事件。 那么effect
与reducer
有啥区别勒?- effect:直译成 n.影响,作用 vt. 产生;达到目的 等,在此我觉得 翻译为动词产生更符合其定义 。 FishRedux 将 effect 设计为一个 UI 无关的任务触发器,我们可以通过 Action 与其 payload 来进行一些与 State 、buildView 等均毫无关系的工作任务,例如网络交互、数据库读写、IO操作等等。
- reducer:直译成 n. [助剂] 减速器; 缩减者,减压器,还原剂; ,相信很多朋友刚开始看到这个文件肯定是一脸蒙蔽
问号脸
我觉得,在直译这方面,也许只有还原剂能搭上边吧。reducer 接受 Action 的事件之后,会更改 State 对象的状态,而 State 对象的状态变化之后,FishRedux 会触发 buildView 函数,重新构建 UI 。UI 构建时会对比 Widget 对象,所以如果你在这里遇到 UI 没有刷新最好看看生成的新旧 Widget 对象比对结果如何。
///--------------effect.dart------------------- import 'dart:async'; import 'package:fish_redux/fish_redux.dart'; import 'action.dart'; import 'state.dart'; Effect<FixLeakedState> buildEffect() { return combineEffects(<Object, Effect<FixLeakedState>>{ FixLeakedAction.delay: _delay, }); } void _delay(Action action, Context<FixLeakedState> ctx) { Timer(Duration(seconds: 3), () { ctx.dispatch(FixLeakedActionCreator.modifyContent('耗时操作结束')); }); } ///--------------effect.dart------------------- ///--------------reducer.dart------------------- import 'package:fish_redux/fish_redux.dart'; import 'action.dart'; import 'state.dart'; Reducer<FixLeakedState> buildReducer() { return asReducer( <Object, Reducer<FixLeakedState>>{ FixLeakedAction.modifyContent: _modifyContent, }, ); } FixLeakedState _modifyContent(FixLeakedState state, Action action) { return state.clone()..content = action.payload; } ///--------------reducer.dart-------------------
-
万事俱备,就等跳转到该页面了,这么多个类是如何联系在一起的呢?
答案在page.dart
中,该文件中的FixLeakedPage
类会把除了action.dart
文件之外的四个文件串起来,构成一个页面。我们只需要调用FixLeakedPage().buildPage(args)
生成一个 Widget 对象给 Navigator 跳转即可,这里的 args 暂时传递 null 即可。
另外在state.dart
文件中的初始化函数:initState(Map<String, dynamic> args)
就是经由FixLeakedPage().buildPage(args)
传递赋值的。///--------------page.dart------------------- import 'package:fish_redux/fish_redux.dart'; import 'effect.dart'; import 'reducer.dart'; import 'state.dart'; import 'view.dart'; class FixLeakedPage extends Page<FixLeakedState, Map<String, dynamic>> { FixLeakedPage() : super( initState: initState, effect: buildEffect(), reducer: buildReducer(), view: buildView, dependencies: Dependencies<FixLeakedState>( adapter: null, slots: <String, Dependent<FixLeakedState>>{ }), middleware: <Middleware<FixLeakedState>>[ ],); } ///--------------page.dart------------------- ///改造一下main.dart文件 ///--------------main.dart------------------- import 'package:fish_redux_demo/page/fixleaked/page.dart'; import 'package:fish_redux_demo/page/mock_leaked_demo.dart'; import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'FishReduxDemo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'FishReduxDemo'), routes: { '/page/mockLeakedPage': (_) => MockLeakedPage(), '/page/fixLeakedPage': (_) => FixLeakedPage().buildPage(null), }, ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { void _incrementCounter() { Navigator.pushNamed(context, '/page/mockLeakedPage'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( '点击跳转mockLeakedPage', ), RaisedButton( child: Text('跳转 FishRedux FixLeaked 页面'), onPressed: () { Navigator.pushNamed(context, '/page/fixLeakedPage'); }, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'GoToMockLeakedPage', child: Icon(Icons.more_horiz), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } ///--------------main.dart-------------------
重新运行 App ,无论你咋退出页面,在第2小节中泄漏的问题都不会再泄漏啦。举一反三,在许许多多用到异步的地方,我们使用 fish_redux 就可以愉快搞定他们啦。
注意:effect 中接受的 Action ,在 reducer 中不能接受到。也就是同一个 Action 被 Dispatch 出去,effect 先于 reducer 接受,并且 effect 接受之后不会再向后派发。所以 Action 的定义需要注意消费顺序。同样的,fish_redux 的全局事件派发也有同样的事项需要注意。但这不是本文的重点,之后有时间写 fish_redux 全局状态管理的笔记再注明吧。
4、小结
本文中的源码地址:fish_redux_demo。tag 列表如下:
- 第二小节,tag 名称
mockLeaked
。模拟耗时操作的泄漏现象。 - 第三小节,tag 名称
fixLeaked
。使用 fish_redux 来解决泄漏问题,并介绍 fish_redux 简单使用。
关于 fish_redux 的更多知识大家可参考下列内容。另外fish_redux 还有很多好用的用法,例如全局状态管理、 Adapter 、middleware 等,本文由于篇幅原因只介绍 fish_redux 的简单应用,力求大家在看完本文章之后能对 fish_redux 的作用与工作流程有个简单的了解。如有错漏,还烦请指出,十分感谢哦!
- Flutter高内聚组件怎么做?阿里闲鱼打造开源高效方案!
- https://github.com/alibaba/fish-redux/issues
- https://github.com/alibaba/fish-redux
- https://github.com/alibaba/fish-redux/blob/master/doc/README-cn.md
从我的角度绘制了一下文中 fish_redux Page 对象的创建、渲染、派发 Action 的活动图,本图不涉及到开发中看不见的核心 API,这部分在学习源码之后有空再补充吧。