前言
如果過于依賴思維慣性與經(jīng)驗主義同欠,會在學(xué)習(xí)與接觸新事物時造成很大的阻礙遏插。它可能會蒙蔽我們的認(rèn)知缸榄、減緩我們消化吸收的速度忱反,更有甚者會憑白產(chǎn)生很多根本不存在的對立矛盾來消耗我們的精力泛释,使得我們愈加抵觸新事物。
所以温算,放下過去怜校,以空杯心態(tài)去學(xué)習(xí)、認(rèn)知新事物注竿,才能更客觀全面的了解消化茄茁。
fish_redux的相關(guān)鏈接:
本文適合那些對 Flutter 知識體系有初步了解的朋友裙顽,例如
- Flutter 中 State 是什么? StatelessWidget 與 StatefulWidget 之間的區(qū)別是啥宣谈?
- Flutter 的 Navigator 如何進(jìn)行頁面跳轉(zhuǎn)愈犹?Flutter 略微復(fù)雜的頁面開發(fā),包含 UI 更新與數(shù)據(jù)處理等闻丑。
- 能流程使用 Dart 進(jìn)行開發(fā)漩怎,了解 Dart 的異步 Future API。
本文作為 fish_redux 入門文章梆掸,并未涉及到 fish_redux 的高級用法扬卷。但是它能幫你對 fish_redux 的狀態(tài)管理、事件分發(fā)等有個初步了解酸钦,并了解因何而用怪得,如何用。
1卑硫、fish_redux
為什么要用 fish_redux ?
一個最簡單的使用場景徒恋,你在一個 State 中使用耗時操作,例如網(wǎng)絡(luò)交互欢伏、數(shù)據(jù)庫查詢等入挣,在 then((){})
處理回調(diào)并調(diào)用 setState((){}})
更新UI,但是在回調(diào)時頁面處于 deactivate
或者 dispose
狀態(tài)硝拧,結(jié)果你的 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().
引用泄漏或錯誤狀態(tài)回調(diào)的解決辦法有很多葛假,例如在回調(diào)中判斷 State 狀態(tài)(State 中的 UI 與 邏輯代碼耦合導(dǎo)致,仍然泄露)滋恬、監(jiān)聽生命周期及時釋放回調(diào)等聊训。專業(yè)方案有諸如 Provider 、 scoped_model 恢氯、 Bloc 等带斑,而 fish_redux
則是最為出色的解決方案之一。在這里順便推薦一下 flutter_boost 混合開發(fā)管理框架勋拟,阿里牛比比比Q摹!敢靡!
fish_redux 功能雖然強(qiáng)大挂滓,但其 API 與設(shè)計思路較為復(fù)雜,官方介紹說其延續(xù)了前端 Redux 框架的思想啸胧。對于很多不熟悉 Redux 的朋友來說杂彭,還需要去了解 Redux ,但是框架這種東西沒實際用過是很難了解的(需要一定的代碼量)吓揪。為了幫助不熟悉 fish_redux 的朋友快速上手,于是就有了這篇文章所计,只要具有前言中提到的對 Flutter 知識體系有一定了解的朋友應(yīng)該都可以快速上手 fish_redux柠辞。
2、<a id="mockLeaked">Mock 泄漏</a>
之后的小節(jié)主胧,我會在每處都打個 tag 叭首,在操作處與結(jié)尾處都會備注 tag 名稱。本小節(jié)的 tag 為 mockLeaked
踪栋。
- 使用 Flutter 命令創(chuàng)建一個 Application 項目焙格,在 yaml 中依賴fish_redux。
創(chuàng)建 fish_
建議使用 IDEA 打開項目夷都,需要裝有$ flutter create fish_redux_demo
Flutter
Dart
與FishReduxTemplate
這三個插件眷唉。編輯pubspec.yaml
文件:
在 terminal 中輸入# 建議 sdk 版本 2.6.0 及其以上《诠伲可以使用 擴(kuò)展函數(shù)冬阳,真香。 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( '點擊跳轉(zhuǎn)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('調(diào)用異步函數(shù)'), onPressed: () { Timer(Duration(seconds: 3), () { _content = '3秒延時已到'; setState(() {}); }); }, ) ], ), ), ); } } ///--------------------mock_leaked_demo.dart 代碼------------------------------
運行 App刑顺,點擊跳到模擬泄漏頁面氯窍,點擊 調(diào)用異步函數(shù)
按鈕立馬退出頁面饲常,就可以在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>
本小節(jié) tag 是 fixLeaked
狼讨。
對于不熟悉 fish_redux 的朋友贝淤,強(qiáng)烈推薦安裝 FishReduxTemplate
插件,用于生成 fish_redux 的相關(guān) API 文件熊楼。
- 安裝好
FishReduxTemplate
插件之后霹娄,創(chuàng)建一個文件夾fixleaked
,右鍵該文件夾選擇 New -> FishReduxTemplate 創(chuàng)建模板文件鲫骗,選擇 Page 類型犬耻,輸入名稱 FixLeaked 。生成文件如下执泰。生成的dart
文件名稱是固定的枕磁,但是類名根據(jù)輸入的名稱變化,所以命名請盡量做到見名知意术吝。
創(chuàng)建fish_redux模板之一
創(chuàng)建fish_redux模板之二
創(chuàng)建fish_redux模板之三 - 這里暫不介紹這六個文件的作用计济,直接擼代碼。關(guān)注數(shù)據(jù)源 排苍,編輯 state.dart 文件沦寂,F(xiàn)ishRedux 要求提供一個 State 類,該類我們可以理解為 MVC 淘衙、 MVP 或 MVVM 中的 Model 传藏,它的作用就是承載數(shù)據(jù)。我們在
FixLeakedState
類中創(chuàng)建一個公開的成員變量content
彤守,需要注意在clone
函數(shù)中拷貝FixLeakedState
的成員屬性值毯侦。該類的initState(Map<String, dynamic> args)
頂級函數(shù),根據(jù)傳遞的參數(shù)創(chuàng)建FixLeakedState
初始對象來決定頁面的初始狀態(tài)具垫。import 'package:fish_redux/fish_redux.dart'; class FixLeakedState implements Cloneable<FixLeakedState> { String content; @override FixLeakedState clone() { //級聯(lián)語法給 clone 對象賦值 return FixLeakedState()..content = this.content; } } FixLeakedState initState(Map<String, dynamic> args) { return FixLeakedState()..content = "MockLeakedPage"; }
-
繪制 UI侈离,修改 view.dart 文件 ,該文件用于創(chuàng)建 Widget 對象筝蚕,提供一個頂級函數(shù)
Widget buildView(FixLeakedState state, Dispatch dispatch, ViewService viewService)
卦碾。參數(shù)列表:-
FixLeakedState
:FishRedux 在合適的時機(jī)會重新調(diào)用buildView
函數(shù),我們根據(jù) state 的狀態(tài)去構(gòu)造不同 UI 效果即可饰及。至于合適的時機(jī)是啥時候蔗坯,后文再說。 -
Dispatch
:一個派發(fā)函數(shù)對象燎含,調(diào)用該對象宾濒,我們可以分發(fā)出不同的事件出去,F(xiàn)ishRedux 會在 effect 或者 reducer 中注冊監(jiān)聽事件屏箍。也就是說我們想把事件從 view 中派發(fā)出去绘梦,使用 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)
函數(shù)通過 state 對象來決定 UI 展示效果,而 Dispatch 對象用于幫助在頂級函數(shù)派發(fā)事件(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('調(diào)用異步函數(shù)'), onPressed: () { //dispatch something }, ) ], ), ), ); }
-
-
在
buildView
我們已經(jīng)知道通過 Dispatch來分發(fā)事件了,那么事件如何定義與創(chuàng)建呢疹鳄?答案在action.dart
中拧略。action.dart
文件定義有兩個類,一個枚舉類和一個構(gòu)造器類瘪弓。
需要什么事件垫蛆,我們可以定義在枚舉類中,構(gòu)造器類可以傳入import 'package:fish_redux/fish_redux.dart'; enum FixLeakedAction { action } class FixLeakedActionCreator { static Action onAction() { return const Action(FixLeakedAction.action); } }
dynamic payload
負(fù)載來生成對應(yīng)的枚舉對象腺怯。///介紹一下 FishRedux 定義的 Action 類袱饭,該類看看就行了 ///需要一個 type 來區(qū)分事件, ///通過 dynamic 類型的 payload 來傳遞數(shù)據(jù)呛占。 class Action { const Action(this.type, {this.payload}); final Object type; final dynamic payload; } ///實際上 action.dart 編輯后的內(nèi)容虑乖, ///刪除默認(rèn)生成的 action 定義之后,注意 effect.dart 與 reduce.dart 使用到了默認(rèn) action ///注意刪除掉它晾虑。 import 'package:fish_redux/fish_redux.dart'; enum FixLeakedAction { delay, modifyContent, } class FixLeakedActionCreator { ///創(chuàng)建 delay action决左,模擬耗時任務(wù) static Action delay() { return const Action(FixLeakedAction.delay); } ///創(chuàng)建修改 content 的 action static Action modifyContent(String content) { return Action(FixLeakedAction.delay, payload: content); } }
- 回到
buildView
方法中,通過按鈕RaisedButton
將模擬延時的事件通過 Dispatch 對象派發(fā)出去走贪。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('調(diào)用異步函數(shù)'), onPressed: () { dispatch(FixLeakedActionCreator.delay()); }, ) ], ), ), ); }
-
事件分發(fā)之后,在
effect.dart
與reducer.dart
來處理事件惑芭。 那么effect
與reducer
有啥區(qū)別勒坠狡?- effect:直譯成 n.影響,作用 vt. 產(chǎn)生遂跟;達(dá)到目的 等逃沿,在此我覺得 翻譯為動詞產(chǎn)生更符合其定義 。 FishRedux 將 effect 設(shè)計為一個 UI 無關(guān)的任務(wù)觸發(fā)器幻锁,我們可以通過 Action 與其 payload 來進(jìn)行一些與 State 凯亮、buildView 等均毫無關(guān)系的工作任務(wù),例如網(wǎng)絡(luò)交互哄尔、數(shù)據(jù)庫讀寫假消、IO操作等等。
- reducer:直譯成 n. [助劑] 減速器; 縮減者岭接,減壓器富拗,還原劑; 臼予,相信很多朋友剛開始看到這個文件肯定是一臉蒙蔽
問號臉
我覺得,在直譯這方面啃沪,也許只有還原劑能搭上邊吧粘拾。reducer 接受 Action 的事件之后,會更改 State 對象的狀態(tài)创千,而 State 對象的狀態(tài)變化之后缰雇,F(xiàn)ishRedux 會觸發(fā) buildView 函數(shù),重新構(gòu)建 UI 追驴。UI 構(gòu)建時會對比 Widget 對象械哟,所以如果你在這里遇到 UI 沒有刷新最好看看生成的新舊 Widget 對象比對結(jié)果如何。
///--------------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('耗時操作結(jié)束')); }); } ///--------------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-------------------
-
萬事俱備氯檐,就等跳轉(zhuǎn)到該頁面了戒良,這么多個類是如何聯(lián)系在一起的呢?
答案在page.dart
中冠摄,該文件中的FixLeakedPage
類會把除了action.dart
文件之外的四個文件串起來糯崎,構(gòu)成一個頁面。我們只需要調(diào)用FixLeakedPage().buildPage(args)
生成一個 Widget 對象給 Navigator 跳轉(zhuǎn)即可河泳,這里的 args 暫時傳遞 null 即可沃呢。
另外在state.dart
文件中的初始化函數(shù):initState(Map<String, dynamic> args)
就是經(jīng)由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( '點擊跳轉(zhuǎn)mockLeakedPage', ), RaisedButton( child: Text('跳轉(zhuǎn) 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小節(jié)中泄漏的問題都不會再泄漏啦。舉一反三纸兔,在許許多多用到異步的地方惰瓜,我們使用 fish_redux 就可以愉快搞定他們啦。
注意:effect 中接受的 Action 汉矿,在 reducer 中不能接受到崎坊。也就是同一個 Action 被 Dispatch 出去,effect 先于 reducer 接受洲拇,并且 effect 接受之后不會再向后派發(fā)奈揍。所以 Action 的定義需要注意消費順序。同樣的赋续,fish_redux 的全局事件派發(fā)也有同樣的事項需要注意男翰。但這不是本文的重點,之后有時間寫 fish_redux 全局狀態(tài)管理的筆記再注明吧纽乱。
4蛾绎、小結(jié)
本文中的源碼地址:fish_redux_demo。tag 列表如下:
- 第二小節(jié),tag 名稱
mockLeaked
秘通。模擬耗時操作的泄漏現(xiàn)象为严。 - 第三小節(jié),tag 名稱
fixLeaked
肺稀。使用 fish_redux 來解決泄漏問題第股,并介紹 fish_redux 簡單使用。
關(guān)于 fish_redux 的更多知識大家可參考下列內(nèi)容话原。另外fish_redux 還有很多好用的用法夕吻,例如全局狀態(tài)管理、 Adapter 繁仁、middleware 等涉馅,本文由于篇幅原因只介紹 fish_redux 的簡單應(yīng)用,力求大家在看完本文章之后能對 fish_redux 的作用與工作流程有個簡單的了解黄虱。如有錯漏稚矿,還煩請指出,十分感謝哦捻浦!
- Flutter高內(nèi)聚組件怎么做晤揣?阿里閑魚打造開源高效方案!
- 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 對象的創(chuàng)建朱灿、渲染昧识、派發(fā) Action 的活動圖,本圖不涉及到開發(fā)中看不見的核心 API盗扒,這部分在學(xué)習(xí)源碼之后有空再補充吧跪楞。