1. 程式人生 > >fish_redux使用詳解---看完就會用!

fish_redux使用詳解---看完就會用!

**說句心裡話,這篇文章,來來回回修改了很多次,如果認真看完這篇文章,還不會寫fish_redux,請在評論裡噴我。** ## 前言 來學學難搞的fish_redux框架吧,這個框架,官方的文件真是一言難盡,比flutter_bloc官網的文件真是遜色太多了,但是一旦知道怎麼寫,頁面堆起來也是非常爽呀,結構分明,邏輯也會錯落有致。 其實在當時搞懂這個框架的時候,就一直想寫一篇文章記錄下,但是因為忙(lan),導致一直沒寫,現在覺得還是必須把使用的過程記錄下,畢竟剛上手這個框架是個蛋痛的過程,必須要把這個過程做個記錄。 這不僅僅是記錄的文章,文中所給出的示例,也是我重新構思去寫的,過程也是力求闡述清楚且詳細。 ![img](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808161402.jpg) ### 幾個問題點 - 頁面切換的轉場動畫 - 頁面怎麼更新資料 - fish_redux各個模組之間,怎麼傳遞資料 - 頁面跳轉傳值,及其接受下個頁面回傳的值 - 怎麼配合ListView使用 - ListView怎麼使用adapter,資料怎麼和item繫結 - 怎麼將Page當做widget使用(BottomNavigationBar,NavigationRail等等導航欄控制元件會使用到) - 這個直接使用:XxxPage.buildPage(null) 即可 如果你在使用fish_redux的過程中遇到過上述的問題,那就來看看這篇文章吧!這裡,會解答上面所有的問題點! ## 準備 ### 引入 **fish_redux相關地址** - GitHub地址:[https://github.com/alibaba/fish-redux](https://github.com/alibaba/fish-redux) - Pub地址:[https://pub.dev/packages/fish_redux](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 ![image-20200808181112391](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808233038.png) - BakerJQ編寫:[Android Studio的Fish Redux模板](https://github.com/BakerJQ/FishReduxTemplateForAS)。 - huangjianke編寫:[VSCode的Fish Redux模板](https://github.com/huangjianke/fish-redux-template) ### 建立 - 這裡我在新建的count資料夾上,選擇新建檔案,選擇:New ---> FishReduxTemplate ![image-20200808181242775](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808233050.png) - 此處選擇:Page,底下的“Select Fils”全部選擇,這是標準的redux檔案結構;這邊命名建議使用大駝峰:Count - Component:這個一般是可複用的相關的元件;列表的item,也可以選擇這個 - Adapter:這裡有三個Adapter,都可以不用了;fish_redux第三版推出了功能更強大的adapter,更加靈活的繫結方式 ![image-20200808181325258](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808233056.png) - 建立成功後,記得在建立的資料夾上右擊,選擇:Reload From Disk;把建立的檔案刷新出來 ![image-20200808181410600](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808233101.png) - 建立成功的檔案結構 - page:總頁面,註冊effect,reducer,component,adapter的功能,相關的配置都在此頁面操作 - state:這地方就是我們存放子模組變數的地方;初始化變數和接受上個頁面引數,也在此處,是個很重要的模組 - view:主要是我們寫頁面的模組 - action:這是一個非常重要的模組,所有的事件都在此處定義和中轉 - effect:相關的業務邏輯,網路請求等等的“副作用”操作,都可以寫在該模組 - reducer:該模組主要是用來更新資料的,也可以寫一些簡單的邏輯或者和資料有關的邏輯操作 ![image-20200808181550186](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808233107.png) - OK,至此就把所有的準備工作搞定了,下面可以開搞程式碼了 ![img](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808233113.jpg) ## 開發流程 ### redux流程 - 下圖是阮一峰老師部落格上放的redux流程圖 ![img](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808233125.jpeg) ### fish_redux流程 - 在寫程式碼前,先看寫下流程圖,這圖是憑著自己的理解畫的 - 可以發現,事件的傳遞,都是通過dispatch這個方法,而且action這層很明顯是非常關鍵的一層,事件的傳遞,都是在該層定義和中轉的 - 這圖在語雀上調了半天,就在上面加了個自己的github水印地址 ![fish_redux流程](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808233137.jpg) - 通過倆個流程圖對比,其中還是有一些差別的 - 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組合式開發 ## 計數器 ### 效果圖 ![fish_redux中count](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808233256.gif) - 這個例子演示,view中點選此操作,然後更新頁面資料;下述的流程,在effect中把資料處理好,通過action中轉傳遞給reducer更新資料 - view ---> action ---> effect ---> reducer(更新資料) - 注意:該流程將展示,怎麼將資料在各流程中互相傳遞 ### 標準模式 - main - 這地方需要注意,cupertino,material這類系統包和fish_redux裡包含的“Page”類名重複了,需要在這類系統包上使用hide,隱藏系統包裡的Page類 - 關於頁面的切換風格,可以在MaterialApp中的onGenerateRoute方法中,使用相應頁面切換風格,這邊使用ios的頁面切換風格:cupertino ```dart ///需要使用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: >{ "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(builder: (BuildContext context) { // return routes.buildPage(settings.name, settings.arguments); // }); }, ); } ``` - state - 定義我們在頁面展示的一些變數,initState中可以初始化變數;clone方法的賦值寫法是必須的 ```dart class CountState implements Cloneable { int count; @override CountState clone() { return CountState()..count = count; } } CountState initState(Map args) { return CountState()..count = 0; } ``` - view:這裡面就是寫介面的模組,buildView裡面有三個引數 - state:這個就是我們的資料層,頁面需要的變數都寫在state層 - dispatch:類似排程器,呼叫action層中的方法,從而去回撥effect,reducer層的方法 - viewService:這個引數,我們可以使用其中的方法:buildComponent("元件名"),呼叫我們封裝的相關元件 ```dart 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: [ 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修飾關鍵字 ```dart 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層中更新資料 ```dart Effect buildEffect() { return combineEffects(>{ CountAction.increase: _onIncrease, }); } ///自增數 void _onIncrease(Action action, Context 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 ```dart Reducer buildReducer() { return asReducer( >{ 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() ```dart 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: [ 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 - 這裡只使用一個列舉欄位,和一個方法就行了,也不用傳啥引數了 ```dart enum CountAction { updateCount } class CountActionCreator { ///去reducer層更新資料,傳參可以放在Action類中的payload欄位中,payload是dynamic型別,可傳任何型別 static Action updateCount() { return Action(CountAction.updateCount); } } ``` - reducer - 這裡直接在:_updateCount方法中處理下簡單的自增邏輯 ```dart Reducer buildReducer() { return asReducer( >{ CountAction.updateCount: _updateCount, }, ); } ///通知View層更新介面 CountState _updateCount(CountState state, Action action) { final CountState newState = state.clone(); newState..count = state.count + 1; return newState; } ``` ### 搞定 - 可以看見優化了後,程式碼量減少了很多,對待不同的業務場景,可以靈活的變動,使用框架,但不要拘泥框架;但是如果有網路請求,很複雜的業務邏輯,就萬萬不能寫在reducer裡面了,一定要寫在effect中,這樣才能保證一個清晰的解耦結構,保證處理資料和更新資料過程分離 ![img](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808234825.jpg) ## 頁面跳轉 ### 效果圖 ![fish_redux中jump](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200809165108.gif) - 從效果圖,很容易看到,倆個頁面相互傳值 - FirstPage ---> SecondPage(FirstPage跳轉到SecondPage,並傳值給SecondPage頁面) - SecondPage ---> FirstPage(SecondPage返回到FirstPage,並返回值給FirstPage頁面) ### 實現 - 從上面效果圖上看,很明顯,這邊需要實現倆個頁面,先看看main頁面的改動 - main - 這裡只增加了倆個頁面:FirstPage和SecondPage;並將主頁面入口換成了:FirstPage ```dart Widget createApp() { ///定義路由 final AbstractRoutes routes = PageRoutes( pages: >{ ///計數器模組演示 "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方法是初始化變數和接受頁面傳值的,這邊我們給他賦個初始值 ```dart class FirstState implements Cloneable { ///傳遞給下個頁面的值 static const String fixedMsg = "\n我是FirstPage頁面傳遞過來的資料:FirstValue"; ///展示傳遞過來的值 String msg; @override FirstState clone() { return FirstState()..msg = msg; } } FirstState initState(Map args) { return FirstState()..msg = "\n暫無"; } ``` - view - 該頁面邏輯相當簡單,主要的僅僅是在onPressed方法中處理邏輯 ```dart 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: [ Text('下方資料是SecondPage頁面傳遞過來的:'), Text(state.msg), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { ///跳轉到Second頁面 dispatch(FirstActionCreator.toSecond()); }, child: Icon(Icons.arrow_forward), ), ); } ``` - action:這裡需要定義倆個列舉事件 - toSecond:跳轉到SecondPage頁面 - updateMsg:拿到SecondPage頁面返回的資料,然後更新頁面資料 ```dart 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頁面獲取返回的值, ```dart /// 使用hide方法,隱藏系統包裡面的Action類 import 'package:flutter/cupertino.dart' hide Action; Effect buildEffect() { return combineEffects(>{ FirstAction.toSecond: _toSecond, }); } void _toSecond(Action action, Context 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欄位即可 ```dart Reducer buildReducer() { return asReducer( >{ 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 ```dart class SecondState implements Cloneable { ///傳遞給下個頁面的值 static const String fixedMsg = "\n我是SecondPage頁面傳遞過來的資料:SecondValue"; ///展示傳遞過來的值 String msg; @override SecondState clone() { return SecondState()..msg = msg; } } SecondState initState(Map args) { ///獲取上個頁面傳遞過來的資料 return SecondState()..msg = args["firstValue"]; } ``` - view - 這邊需要注意的就是:WillPopScope控制元件接管AppBar的返回事件 ```dart 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: [ Text('下方資料是FirstPage頁面傳遞過來的:'), Text(state.msg), ], ), ), ); } ``` - action ```dart enum SecondAction { backFirst } class SecondActionCreator { ///返回到第一個頁面,然後從棧中移除自身,同時傳回去一些資料 static Action backFirst() { return Action(SecondAction.backFirst); } } ``` - effect - 此處同樣需要隱藏系統包中的Action類 - 這邊直接在pop方法的第二個引數,寫入返回資料 ```dart ///隱藏系統包中的Action類 import 'package:flutter/cupertino.dart' hide Action; Effect buildEffect() { return combineEffects(>{ SecondAction.backFirst: _backFirst, }); } void _backFirst(Action action, Context ctx) { ///pop當前頁面,並且返回相應的資料 Navigator.pop(ctx.context, {"secondValue": SecondState.fixedMsg}); } ``` ### 搞定 - 因為page模組不需要改動,所以就沒必要將page模組程式碼附上了哈 - OK,到這裡,咱們也已經把倆個頁面相互傳值的方式get到了! ![img](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200809165135.jpg) ## 列表文章 - 理解了上面倆個案例,相信你可以使用fish_redux實現一部分頁面了;但是,我們堆頁面的過程中,能體會列表模組是非常重要的一部分,現在就來學學,在fish_redux中怎麼使用ListView吧! - 廢話少說,上號! ![00685430](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200808234813.png) ### 列表展示-網路請求 #### 效果圖 ![fish_redux中list](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200812171610.gif) - 效果圖對於列表的滾動,做了倆個操作:一個是拖拽列表;另一個是滾動滑鼠的滾輪。flutter對滑鼠觸發的相關事件也支援的越來越好了! - 這邊我們使用的是玩Android的api,這個api有個坑的地方,沒設定開啟跨域,所以執行在web上,這個api使用會報錯,我在玩Android的github上提了issue,哎,也不知道作者啥時候解決,,, - 這地方只能曲線救國,關閉瀏覽器跨域限制,設定看這裡:https://www.jianshu.com/p/56b1e01e6b6a - 如果執行在虛擬機器上,就完全不會出現這個問題! #### 準備 - 先看下檔案結構 ![image-20200810225418771](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200812171629.png) - main - 這邊改動非常小,只在路由裡,新增了:GuidePage,ListPage;同時將home欄位中的預設頁面,改成了:GuidePage頁面;導航頁面程式碼就不貼在文章裡了,下面貼下該頁面連結 - https://github.com/CNAD666/ExampleCode/tree/master/Flutter/fish_redux_demo/lib/guide - ListPage才是重點,下文會詳細說明 ```dart void main() { runApp(createApp()); } Widget createApp() { ///定義路由 final AbstractRoutes routes = PageRoutes( pages: >{ ///導航頁面 "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 ![image-20200812140314075](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200812171706.png) #### item模組 **按照流程走** - 根據介面返回json,建立相應的bean ---> 建立item模組 ---> 編寫state ---> 編寫view介面 **準備工作** - 建立bean實體 - 根據api返回的json資料,生成相應的實體 - api:https://www.wanandroid.com/project/list/1/json - json轉實體 - 網站:https://javiercbk.github.io/json_to_dart/ - 外掛:AS中可以搜尋:FlutterJsonBeanFactory - 這地方生成了:ItemDetailBean;程式碼倆百多行就不貼了,具體的內容,點選下面連結 - ItemDetailBean程式碼:https://github.com/CNAD666/ExampleCode/blob/master/Flutter/fish_redux_demo/lib/list/bean/item_detail_bean.dart - 建立item模組 - 這邊我們實現一個簡單的列表,item僅僅做展示功能;不做點選,更新ui等操作,所以這邊我們就不需要建立:effect,reducer,action檔案;只選擇:state和view就行了 - 建立item,這裡選擇component ![image-20200810225523628](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200812171718.png) **檔案結構** ![image-20200812141416796](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200812171729.png) OK,bean檔案搞定了,再來看看,item檔案中的檔案,這裡component檔案不需要改動,所以這地方,我們只需要看:state.dart,view.dart - state - 這地方還是常規的寫法,因為json生成的bean裡面,能用到的所有資料,都在Datas類裡面,所以,這地方建一個Datas類的變數即可 - 因為,沒用到reducer,實際上clone實現方法都能刪掉,防止後面可能需要clone物件,暫且留著 ```dart import 'package:fish_redux/fish_redux.dart'; import 'package:fish_redux_demo/list/bean/item_detail_bean.dart'; class ItemState implements Cloneable { Datas itemDetail; ItemState({this.itemDetail}); @override ItemState clone() { return ItemState() ..itemDetail = itemDetail; } } ItemState initState(Map args) { return ItemState(); } ``` - view - 這裡item佈局稍稍有點麻煩,整體上採用的是:水平佈局(Row),分左右倆大塊 - 左邊:單純的圖片展示 - 右邊:採用了縱向佈局(Column),結合Expanded形成比例佈局,分別展示三塊東西:標題,內容,作者和時間 - OK,這邊view只是簡單用到了state提供的資料形成的佈局,沒有什麼要特別注意的地方 ```dart 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: [ //左邊圖片 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: [ //標題 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: [ //作者 Row( children: [ Text("作者:", style: TextStyle(fontSize: 12)), Expanded( child: Text(state.itemDetail.author, style: TextStyle(color: Colors.blue, fontSize: 12), overflow: TextOverflow.ellipsis), ) ], ), //時間 Row(children: [ 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() ```dart class ListItemAdapter extends SourceFlowAdapter { static const String item_style = "project_tab_item"; ListItemAdapter() : super( pool: >{ ///定義item的樣式 item_style: ItemComponent(), }, ); } ``` - state調整 - state檔案中的程式碼需要做一些調整,需要繼承相應的類,和adapter建立起關聯 - ListState需要繼承MutableSource;還必須定義一個泛型是item的ItemState型別的List,這倆個是必須的;然後實現相應的抽象方法就行了 - 這裡只要向items裡寫入ItemState的資料,列表就會更新了 ```dart class ListState extends MutableSource implements Cloneable { ///這地方一定要注意,List裡面的泛型,需要定義為ItemState ///怎麼更新列表資料,只需要更新這個items裡面的資料,列表資料就會相應更新 ///使用多樣式,請寫出 List items; List 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 args) { return ListState(); } ``` - page中繫結adapter - 這裡就是將我們的ListSate和ListItemAdapter介面卡建立起連線 ```dart class ListPage extends Page> { ListPage() : super( initState: initState, effect: buildEffect(), reducer: buildReducer(), view: buildView, dependencies: Dependencies( ///繫結Adapter adapter: NoneConn() + ListItemAdapter(), slots: >{}), middleware: >[], ); } ``` **正常page頁面編輯** 整體流程 - view模組編寫 ---> action新增更新資料事件 ---> effect初始化時獲取資料並處理 ---> reducer更新資料 - view - 這裡面的列表使用就相當簡單了,填入itemBuilder和itemCount引數就行了,這裡就需要用viewService引數了哈 ```dart 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了 ```dart enum ListAction { updateItem } class ListActionCreator { static Action updateItem(var list) { return Action(ListAction.updateItem, payload: list); } } ``` - effect - Lifecycle.initState是進入頁面初始化的回撥,這邊可以直接用這個狀態回撥,來請求介面獲取相應的資料,然後去更新列表 - 這地方有個坑,dio必須結合json序列號和反序列的庫一起用,不然Dio無法將資料來源解析成Response型別 ```dart Effect buildEffect() { return combineEffects(>{ ///進入頁面就執行的初始化操作 Lifecycle.initState: _init, }); } void _init(Action action, Context 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 itemDetails = itemDetailBean.data.datas; ///構建符合要求的列表資料來源 List items = List.generate(itemDetails.length, (index) { return ItemState(itemDetail: itemDetails[index]); }); ///通知更新列表資料來源 ctx.dispatch(ListActionCreator.updateItem(items)); } ``` - reducer - 最後就是更新操作了哈,這裡就是常規寫法了 ```dart Reducer buildReducer() { return asReducer( >{ ListAction.updateItem: _updateItem, }, ); } ListState _updateItem(ListState state, Action action) { return state.clone()..items = action.payload; } ``` ### 列表修改-單item重新整理 #### 效果圖 ![list_editjump](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200813183830.gif) - 這次來演示列表的單item更新,沒有網路請求的操作,所以程式碼邏輯就相當簡單了 #### 結構 - 來看看程式碼結構 ![image-20200813171905618](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200813182858.png) - 這地方很明顯得發現,list_edit主體檔案很少,因為這邊直接在state裡初始化了資料來源,就沒有後期更新資料的操作,所以就不需要:action,effect,reducer這三個檔案!item模組則直接在reducer裡更新資料,不涉及相關複雜的邏輯,所以不需要:effect檔案。 #### 列表模組 - 這次列表模組是非常的簡單,基本不涉及什麼流程,就是最基本初始化的一個過程,將state裡初始化的資料在view中展示 - state ---> view - state - 老規矩,先來看看state中的程式碼 - 這裡一些新建了變數,泛型是ItemState(item的State),items變數初始化了一組資料;然後,同樣繼承了MutableSource,實現其相關方法 ```dart class ListEditState extends MutableSource implements Cloneable { List 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 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 ```dart 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介面卡 ```dart class ListItemAdapter extends SourceFlowAdapter { static const String itemName = "item"; ListItemAdapter() : super( pool: >{itemName: ItemComponent()}, ); } ``` - page - 在page裡面繫結adapter ```dart class ListEditPage extends Page> { ListEditPage() : super( initState: initState, view: buildView, dependencies: Dependencies( ///繫結介面卡 adapter: NoneConn() + ListItemAdapter(), slots: >{}), middleware: >[], ); } ``` #### item模組 - 接下就是比較重要的item模組了,item模組的流程,也是非常的清晰 - view ---> action ---> reducer - state - 老規矩,先來看看state裡面的程式碼;此處就是寫常規變數的定義,這些在view中都能用得著 ```dart class ItemState implements Cloneable { 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 args) { return ItemState(); } ``` - view - 可以看到Checkbox的內部點選操作,我們傳遞了一個id引數,注意這個id引數是必須的,在更新item的時候來做區分用的 ```dart 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 - 一個狀態改變的事件 ```dart 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操作失靈 ```dart Reducer buildReducer() { return asReducer( >{ 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資料時,會報型別不適配的錯誤 **下述程式碼可做思路參考** ```dart class ListState extends MutableSource implements Cloneable { List 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狀態被重置到未選狀態,具體效果看下方效果圖 - 效果圖 ![單選模式](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200928134810.gif) - 這種效果的實現非常簡單,但是如果思路不對,會掉進坑裡出不來 - 還原被選的狀態,不能在同一個事件裡寫,需要新寫一個清除事件 **下述程式碼為整體流程** - view ```dart 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 ```dart 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 ```dart Reducer buildReducer() { return asReducer( >{ 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建立及其繫結**;只能多寫寫了,熟能生巧! - 列表模組大功告成,以後就能愉快的寫列表了! ![img](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200812171752.jpg) ## 全域性模式 ### 效果圖 ![fish_redux_switch](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200812171758.gif) - 理解了上面的是三個例子,相信大部分頁面,對於你來說都不在話下了;現在我們再來看個例子,官方提供的全域性主題功能,當然,這不僅僅是全域性主題,全域性字型樣式,字型大小等等,都是可以全域性管理,當然了,寫app之前要做好規劃 ### 開搞 #### store模組 - 檔案結構 - 這地方需要新建一個資料夾,新建四個檔案:action,reducer,state,store ![image-20200812162317741](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200812171806.png) - state - 老規矩,先來看看state,我們這裡只在抽象類裡面定義了一個主題色,這個抽象類是很重要的,需要做全域性模式所有子模組的state,都必須實現這個抽象類 ```dart abstract class GlobalBaseState{ Color themeColor; } class GlobalState implements GlobalBaseState, Cloneable{ @override Color themeColor; @override GlobalState clone() { return GlobalState(); } } ``` - action - 因為只做切換主題色,這地方只需要定義一個事件即可 ```dart enum GlobalAction { changeThemeColor } class GlobalActionCreator{ static Action onChangeThemeColor(){ return const Action(GlobalAction.changeThemeColor); } } ``` - reducer - 這裡就是處理變色的一些操作,這是鹹魚官方demo裡面程式碼;這說明簡單的邏輯,是可以放在reducer裡面寫的 ```dart import 'package:flutter/material.dart' hide Action; Reducer buildReducer(){ return asReducer( >{ GlobalAction.changeThemeColor: _onChangeThemeColor, }, ); } List _colors = [ 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 - 切換全域性狀態的時候,就需要呼叫這個類了 ```dart /// 建立一個AppStore /// 目前它的功能只有切換主題 class GlobalStore{ static Store _globalStore; static Store get store => _globalStore ??= createStore(GlobalState(), buildReducer()); } ``` #### main改動 - 這裡面將PageRoutes裡面的visitor欄位使用起來,狀態更新操作程式碼有點多,就單獨提出來了;所以main檔案裡面,增加了: - visitor欄位使用 - 增加_updateState方法 ```dart 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 page) { if (page.isTypeof()) { ///建立AppStore驅動PageStore的單向資料連線: 引數1 AppStore 引數2 當AppStore.state變化時,PageStore.state該如何變化 page.connectExtraStore(GlobalStore.store, _updateState()); } }, ///定義路由 pages: >{ ///導航頁面 "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類,很小的改動 ```dart class CountState implements Cloneable,GlobalBaseState { int count; @override CountState clone() { return CountState()..count = count; } @override Color themeColor; } CountState initState(Map args) { return CountState()..count = 0; } ``` - view - 這裡面僅僅改動了一行,在AppBar裡面加了backgroundColor,然後使用state裡面的全域性主題色 ```dart 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: [ 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了 ```dart GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor()); ``` ### 搞定 - 經過上面的的三步,我們就可以使用全域性狀態了;從上面子模組的使用,可以很明顯的感受到,全域性狀態,必須前期做好欄位的規劃,確定之後,最好不要再增加欄位,不然繼承抽象類的多個模組都會爆紅,提示去實現xxx變數 ## 全域性模組優化 ### 反思 在上面的全域性模式裡說了,使用全域性模組,前期需要規劃好欄位,不然專案進行到中期的時候,想新增欄位,多個模組的State會出現大範圍爆紅,提示去實現你新增的欄位;專案開始規劃好所有的欄位,顯然這需要全面的考慮好大部分場景,但是人的靈感總是無限的,不改程式碼是不可能,這輩子都不可能。只能想辦法看能不能新增一次欄位後,後期新增欄位,並不會引起其他模組爆紅,試了多次,成功的使用中間實體,來解決該問題 這裡優化倆個方面 - 使用通用的全域性實體 - 這樣後期新增欄位,就不會影響其他模組,這樣我們就能一個個模組的去整改,不會出現整個專案不能執行的情況 - 將路由模組和全域性模組封裝 - 路由模組後期頁面多了,程式碼會很多,放在主入口,真的不好管理;全域性模組同理 因為使用中間實體,有一些地方會出現空指標問題,我都在流程裡面寫清楚了,大家可以把優化流程完整看一遍哈,都配置好,後面拓展使用就不會報空指標了 ### 優化 #### 入口模組 - main:大改 - 從下面程式碼可以看到,這裡將路由模組和全域性模組單獨提出來了,這地方為了方便觀看,就寫在一個檔案裡;說明下,RouteConfig和StoreConfig這倆個類,可以放在倆個不同的檔案裡,這樣管理路由和全域性欄位更新就會很方便了! - RouteConfig:這裡將頁面標識和頁面對映分開寫,這樣我們跳轉頁面的時候,就可以直接引用RouteConfig裡面的頁面標識 - StoreConfig:全域性模組裡最重要的就是狀態的判斷,註釋寫的很清楚了,可以看看註釋哈 ```dart 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: >{ ///將你的路由名稱和頁面對映在一起,比如: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 page) { if (page.isTypeof()) { ///建立AppStore驅動PageStore的單向資料連線 ///引數1 AppStore 引數2 當AppStore.state變化時,PageStore.state該如何變化 page.connectExtraStore(GlobalStore.store, _updateState()); } } } ``` #### Store模組 **下面倆個模組是需要改動程式碼的模組** - state - 這裡使用了StoreModel中間實體,注意,這地方實體欄位store,初始化是必須的,不然在子模組引用該實體下的欄位會報空指標 ```dart abstract class GlobalBaseState{ StoreModel store; } class GlobalState implements GlobalBaseState, Cloneable{ @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 ```dart Reducer buildReducer(){ return asReducer( >{ GlobalAction.changeThemeColor: _onChangeThemeColor, }, ); } List _colors = [ 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 ```dart enum GlobalAction { changeThemeColor } class GlobalActionCreator{ static Action onChangeThemeColor(){ return const Action(GlobalAction.changeThemeColor); } } ``` - store ```dart class GlobalStore{ static Store _globalStore; static Store get store => _globalStore ??= createStore(GlobalState(), buildReducer()); } ``` #### 子模組使用 - 這裡就用計數器模組的來舉例,因為僅僅只需要改動少量程式碼,且只涉及state和view,所以其它模組程式碼也不重複貼出了 - state - 因為是用中間實體,所以在clone方法裡面必須將實現的store欄位加上,不然會報空指標 ```dart class CountState implements Cloneable, GlobalBaseState { int count; @override CountState clone() { return CountState() ..count = count ..store = store; } @override StoreModel store; } CountState initState(Map args) { return CountState()..count = 0; } ``` - view - 這裡面僅僅改動了一行,在AppBar裡面加了backgroundColor,然後使用state裡面的全域性主題色 ```dart 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, ), ///下面其餘程式碼省略.... } ``` - 如果其他模組也需要做主題色,也按照此處邏輯改動即可 #### 呼叫 - 呼叫和上面說的一樣,用下述全域性方式在合適的地方呼叫 ```dart GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor()); ``` ### 體驗 通過上面的優化,使用體驗提升不是一個級別,大大提升的全域性模式的擴充套件性,我們就算後期增加了大量的全域性欄位,也可以一個個模組慢慢改,不用一次爆肝全改完,猝死的概率又大大減少了! ![img](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200829194220.jpg) ## Component使用 Component是個比較常用的模組,上面使用列表的時候,就使用到了Component,這次我們來看看,在頁面中直接使用Component,可插拔式使用!Component的使用總的來說是比較簡單了,比較關鍵的是在State中建立起連線。 ### 效果圖 ![fish_redux中component](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/bookWeb/20200905190609.gif) - 上圖的效果是在頁面中嵌入了倆個Component,改變子Component的操作是在頁面中完成的 - 先看下頁面結構 ![image-20200905183821129](https://cdn.jsdelivr.net/gh/CNAD666/MyData/pic/flutter/blog/20200905190707.png) ### Component 這地方寫了一個Component,程式碼很簡單,來看看吧 - component 這地方程式碼是自動生成了,沒有任何改動,就不貼了 - state - initState():我們需要注意,Component中的initState()方法在內部沒有呼叫,雖然自動生成的程式碼有這個方法,但是無法起到初始化作用,可以刪掉該方法 ```dart class AreaState implements Cloneable { 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 ```dart 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 Comp