說句內心話,這篇文章,來來回回修改了不少次,若是認真看完這篇文章,還不會寫fish_redux,請在評論裏噴我。react
來學學難搞的fish_redux框架吧,這個框架,官方的文檔真是一言難盡,比flutter_bloc官網的文檔真是遜色太多了,可是一旦知道怎麼寫,頁面堆起來也是很是爽呀,結構分明,邏輯也會錯落有致。android
其實在當時搞懂這個框架的時候,就一直想寫一篇文章記錄下,可是由於忙(lan),致使一直沒寫,如今以爲仍是必須把使用的過程記錄下,畢竟剛上手這個框架是個蛋痛的過程,必需要把這個過程作個記錄。ios
這不只僅是記錄的文章,文中所給出的示例,也是我從新構思去寫的,過程也是力求闡述清楚且詳細。git
怎麼將Page當作widget使用(BottomNavigationBar,NavigationRail等等導航欄控件會使用到)github
若是你在使用fish_redux的過程當中遇到過上述的問題,那就來看看這篇文章吧!這裏,會解答上面全部的問題點!web
fish_redux相關地址數據庫
我用的是0.3.X的版本,算是第三版,相對於前幾版,改動較大json
fish_redux: ^0.3.4 #演示列表須要用到的庫 dio: ^3.0.9 #網絡請求框架 json_annotation: ^2.4.0 #json序列化和反序列化用的
此處選擇:Page,底下的「Select Fils」所有選擇,這是標準的redux文件結構;這邊命名建議使用大駝峯:Countredux
建立成功的文件結構api
在寫代碼前,先看寫下流程圖,這圖是憑着本身的理解畫的
經過倆個流程圖對比,其中仍是有一些差異的
這邊寫幾個示例,來演示fish_redux的使用
計數器
頁面跳轉
列表文章
全局模塊
全局模式優化
Component使用
開發小技巧
這個例子演示,view中點擊此操做,而後更新頁面數據;下述的流程,在effect中把數據處理好,經過action中轉傳遞給reducer更新數據
main
///須要使用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
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裏面有三個參數
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
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
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
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; }
從上面的例子看到,如此簡單數據變換,僅僅是個state中一個參數自增的過程,effect層就顯得有些多餘;因此,把流程簡化成下面
搞起來
view
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
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; }
從效果圖,很容易看到,倆個頁面相互傳值
main
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); }); }, ); }
先來看看該頁面的一個流程
state
先寫state文件,這邊須要定義倆個變量來
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
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:這裏須要定義倆個枚舉事件
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
/// 使用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
Reducer<FirstState> buildReducer() { return asReducer( <Object, Reducer<FirstState>>{ FirstAction.updateMsg: _updateMsg, }, ); } FirstState _updateMsg(FirstState state, Action action) { return state.clone()..msg = action.payload; }
這個頁面比較簡單,後續不涉及到頁面數據更新,因此reducer模塊能夠不寫,看看該頁面的流程
state
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
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), ], ), ), ); }
enum SecondAction { backFirst } class SecondActionCreator { ///返回到第一個頁面,而後從棧中移除自身,同時傳回去一些數據 static Action backFirst() { return Action(SecondAction.backFirst); } }
effect
///隱藏系統包中的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}); }
理解了上面倆個案例,相信你可使用fish_redux實現一部分頁面了;可是,咱們堆頁面的過程當中,能體會列表模塊是很是重要的一部分,如今就來學學,在fish_redux中怎麼使用ListView吧!
效果圖對於列表的滾動,作了倆個操做:一個是拖拽列表;另外一個是滾動鼠標的滾輪。flutter對鼠標觸發的相關事件也支持的愈來愈好了!
main
這邊改動很是小,只在路由裏,新增了:GuidePage,ListPage;同時將home字段中的默認頁面,改爲了:GuidePage頁面;導航頁面代碼就不貼在文章裏了,下面貼下該頁面連接
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模塊 ---> 列表模塊邏輯完善
初始化列表模塊
item模塊
列表模塊邏輯完善:倆地方分倆步(adapter建立及其綁定,正常page頁面編輯)
按照流程走
準備工做
建立bean實體
根據api返回的json數據,生成相應的實體
json轉實體
這地方生成了:ItemDetailBean;代碼倆百多行就不貼了,具體的內容,點擊下面連接
建立item模塊
文件結構
OK,bean文件搞定了,再來看看,item文件中的文件,這裏component文件不須要改動,因此這地方,咱們只須要看:state.dart,view.dart
state
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),分左右倆大塊
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建立及其綁定
建立adapter
class ListItemAdapter extends SourceFlowAdapter<ListState> { static const String item_style = "project_tab_item"; ListItemAdapter() : super( pool: <String, Component<Object>>{ ///定義item的樣式 item_style: ItemComponent(), }, ); }
state調整
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
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
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
enum ListAction { updateItem } class ListActionCreator { static Action updateItem(var list) { return Action(ListAction.updateItem, payload: list); } }
effect
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; }
此次列表模塊是很是的簡單,基本不涉及什麼流程,就是最基本初始化的一個過程,將state裏初始化的數據在view中展現
state
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
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
class ListItemAdapter extends SourceFlowAdapter<ListEditState> { static const String itemName = "item"; ListItemAdapter() : super( pool: <String, Component<Object>>{itemName: ItemComponent()}, ); }
page
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模塊的流程,也是很是的清晰
state
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
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
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,是徹底能夠的。
假設一種狀況,在index是奇數時展現:OneComponent;在index是奇數時展現:TwoComponent;
下述代碼可作思路參考
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狀態被重置到未選狀態,具體效果看下方效果圖
下述代碼爲總體流程
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)); } ), ), ); }
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<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裏面代碼
文件結構
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
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()); }
這裏面將PageRoutes裏面的visitor字段使用起來,狀態更新操做代碼有點多,就單獨提出來了;因此main文件裏面,增長了:
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
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
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), ), ); }
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());
在上面的全局模式裏說了,使用全局模塊,前期須要規劃好字段,否則項目進行到中期的時候,想添加字段,多個模塊的State會出現大範圍爆紅,提示去實現你添加的字段;項目開始規劃好全部的字段,顯然這須要全面的考慮好大部分場景,可是人的靈感老是無限的,不改代碼是不可能,這輩子都不可能。只能想辦法看能不能添加一次字段後,後期添加字段,並不會引發其餘模塊爆紅,試了屢次,成功的使用中間實體,來解決該問題
這裏優化倆個方面
使用通用的全局實體
將路由模塊和全局模塊封裝
由於使用中間實體,有一些地方會出現空指針問題,我都在流程裏面寫清楚了,你們能夠把優化流程完整看一遍哈,都配置好,後面拓展使用就不會報空指針了
main:大改
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()); } } }
下面倆個模塊是須要改動代碼的模塊
state
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
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; }
下面倆個模塊代碼沒有改動,可是爲了思路完整,一樣貼出來
enum GlobalAction { changeThemeColor } class GlobalActionCreator{ static Action onChangeThemeColor(){ return const Action(GlobalAction.changeThemeColor); } }
class GlobalStore{ static Store<GlobalState> _globalStore; static Store<GlobalState> get store => _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer()); }
state
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
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的使用總的來講是比較簡單了,比較關鍵的是在State中創建起鏈接。
這地方寫了一個Component,代碼很簡單,來看看吧
這地方代碼是自動生成了,沒有任何改動,就不貼了
state
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; } }
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), ), ); }
CompPage中,沒用到effete這層,就沒建立該文件,老規矩,先看看state
state
computed():該方法是必須實現的,這個相似直接的get()方法,可是切記不能像get()直接返回state.leftAreaState()或state.rightAreaState,某些場景初始化沒法刷新,由於是同一個對象,會被判斷未更改,因此會不刷新控件
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
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
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("改變"), ), )) ], ), ); }
enum CompAction { change } class CompActionCreator { static Action change() { return const Action(CompAction.change); } }
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內部本身去處理了
廣播在複雜的業務場景,可以起到很是巨大的做用,能很是輕鬆使用跨頁面交互,跨Component交互!
fish_redux中是帶有廣播的通訊方式,使用的方式很簡單,這本是effect層,ctx參數自帶的一個api,這裏介紹一下
說明:請注意廣播能夠通知任何頁面的枚舉方法,你能夠單獨寫一個枚舉事件,也能夠不寫,直接使用某個頁面的枚舉事件,是徹底能夠
action
enum BroadcastAction { toNotify } class BroadcastActionCreator { ///廣播通知 static Action toNotify(String msg) { return Action(BroadcastAction.toNotify, payload: msg); } }
發送廣播
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是處於Page或Component都能進行通訊交互,不少狀況下,咱們在一個頁面進行了操做,其餘頁面也須要同步作一些處理,使用廣播就很簡單了
注意: 廣播發送和接受是一對多的關係,一處發送,能夠在多處接受;和dispatch發送事件,若是在effect裏面接受,在reducer就沒法接受的狀況是不同的(被攔截了)
無限弱化了reducer層做用
Reducer<TestState> buildReducer() { return asReducer( <Object, Reducer<TestState>>{ TestAction.onRefresh: _onRefresh, }, ); } TestState _onRefresh(TreeState state, Action action) { return state.clone(); }
說明
這種開發形式,能夠說是個慣例,在android裏面是封裝一個個View,View裏有對應的一套,邏輯自洽的功能,而後在主xm裏面組合這些View;這種思想徹底能夠引伸到Flutter裏,並且,開發體驗更上幾百層樓,讓你的widget組合能夠更加靈活百變,百變星君
爲何用widget組合方式構造頁面?
組合widget關鍵點
解決方案
狀態管理