說句心里話,這篇文章,來來回回修改了很多次,如果認真看完這篇文章,還不會寫fish_redux,請在評論里噴我。
前言
來學學難搞的fish_redux框架吧,這個框架,官方的文檔真是一言難盡,比flutter_bloc官網的文檔真是遜色太多了,但是一旦知道怎么寫,頁面堆起來也是非常爽呀,結構分明,邏輯也會錯落有致。
其實在當時搞懂這個框架的時候,就一直想寫一篇文章記錄下,但是因為忙(lan),導致一直沒寫,現在覺得還是必須把使用的過程記錄下,畢竟剛上手這個框架是個蛋痛的過程,必須要把這個過程做個記錄。
這不僅僅是記錄的文章,文中所給出的示例,也是我重新構思去寫的,過程也是力求闡述清楚且詳細。
幾個問題點
- 頁面切換的轉場動畫
- 頁面怎么更新數據
- fish_redux各個模塊之間,怎么傳遞數據
- 頁面跳轉傳值,及其接受下個頁面回傳的值
- 怎么配合ListView使用
- ListView怎么使用adapter,數據怎么和item綁定
- 怎么將Page當做widget使用(BottomNavigationBar,NavigationRail等等導航欄控件會使用到)
- 這個直接使用:XxxPage.buildPage(null) 即可
如果你在使用fish_redux的過程中遇到過上述的問題,那就來看看這篇文章吧!這里,會解答上面所有的問題點!
准備
引入
fish_redux相關地址
我用的是0.3.X的版本,算是第三版,相對於前幾版,改動較大
- 引入fish_redux插件,想用最新版插件,可進入pub地址里面查看
fish_redux: ^0.3.4
#演示列表需要用到的庫
dio: ^3.0.9 #網絡請求框架
json_annotation: ^2.4.0 #json序列化和反序列化用的
開發插件
-
此處我們需要安裝代碼生成插件,可以幫我們生成大量文件和模板代碼
-
在Android Studio里面搜索”fish“就能搜出插件了,插件名叫:FishReduxTemplate
-
BakerJQ編寫:Android Studio的Fish Redux模板。
-
huangjianke編寫:VSCode的Fish Redux模板
創建
- 這里我在新建的count文件夾上,選擇新建文件,選擇:New ---> FishReduxTemplate
- 此處選擇:Page,底下的“Select Fils”全部選擇,這是標准的redux文件結構;這邊命名建議使用大駝峰:Count
- Component:這個一般是可復用的相關的組件;列表的item,也可以選擇這個
- Adapter:這里有三個Adapter,都可以不用了;fish_redux第三版推出了功能更強大的adapter,更加靈活的綁定方式
- 創建成功后,記得在創建的文件夾上右擊,選擇:Reload From Disk;把創建的文件刷新出來
- 創建成功的文件結構
- page:總頁面,注冊effect,reducer,component,adapter的功能,相關的配置都在此頁面操作
- state:這地方就是我們存放子模塊變量的地方;初始化變量和接受上個頁面參數,也在此處,是個很重要的模塊
- view:主要是我們寫頁面的模塊
- action:這是一個非常重要的模塊,所有的事件都在此處定義和中轉
- effect:相關的業務邏輯,網絡請求等等的“副作用”操作,都可以寫在該模塊
- reducer:該模塊主要是用來更新數據的,也可以寫一些簡單的邏輯或者和數據有關的邏輯操作
- OK,至此就把所有的准備工作搞定了,下面可以開搞代碼了
開發流程
redux流程
- 下圖是阮一峰老師博客上放的redux流程圖
fish_redux流程
-
在寫代碼前,先看寫下流程圖,這圖是憑着自己的理解畫的
- 可以發現,事件的傳遞,都是通過dispatch這個方法,而且action這層很明顯是非常關鍵的一層,事件的傳遞,都是在該層定義和中轉的
- 這圖在語雀上調了半天,就在上面加了個自己的github水印地址
-
通過倆個流程圖對比,其中還是有一些差別的
- redux里面的store是全局的。fish_redux里面也有這個全局store的概念,放在子模塊里面理解store,react;對應fish_redux里的就是:state,view
- fish_redux里面多了effect層:這層主要是處理邏輯,和相關網絡請求之類
- reducer里面,理論上也是可以處理一些和數據相關,簡單的邏輯;但是復雜的,會產生相應較大的“副作用”的業務邏輯,還是需要在effect中寫
范例說明
這邊寫幾個示例,來演示fish_redux的使用
- 計數器
- fish_redux正常情況下的流轉過程
- fish_redux各模塊怎么傳遞數據
- 頁面跳轉
- A ---> B(A跳轉到B,並傳值給B頁面)
- B ---> A(B返回到A,並返回值給A頁面)
- 列表文章
- 列表展示-網絡請求
- 列表修改-單item刷新
- 多樣式列表
- 列表存在的問題+解決方案
- 全局模塊
- 全局切換主題
- 全局模式優化
- 大幅度提升開發體驗
- Component使用
- page中使用component
- 廣播
- 開發小技巧
- 弱化reducer
- widget組合式開發
計數器
效果圖
- 這個例子演示,view中點擊此操作,然后更新頁面數據;下述的流程,在effect中把數據處理好,通過action中轉傳遞給reducer更新數據
- view ---> action ---> effect ---> reducer(更新數據)
- 注意:該流程將展示,怎么將數據在各流程中互相傳遞
標准模式
- main
- 這地方需要注意,cupertino,material這類系統包和fish_redux里包含的“Page”類名重復了,需要在這類系統包上使用hide,隱藏系統包里的Page類
- 關於頁面的切換風格,可以在MaterialApp中的onGenerateRoute方法中,使用相應頁面切換風格,這邊使用通用風格:Material
///需要使用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) {
return MaterialPageRoute(
builder: (BuildContext context) {
return routes.buildPage(settings.name, settings.arguments);
},
settings: settings,
);
},
);
}
- state
- 定義我們在頁面展示的一些變量,initState中可以初始化變量;clone方法的賦值寫法是必須的
class CountState implements Cloneable<CountState> {
int count;
@override
CountState clone() {
return CountState()..count = count;
}
}
CountState initState(Map<String, dynamic> args) {
return CountState()..count = 0;
}
- view:這里面就是寫界面的模塊,buildView里面有三個參數
- state:這個就是我們的數據層,頁面需要的變量都寫在state層
- dispatch:類似調度器,調用action層中的方法,從而去回調effect,reducer層的方法
- viewService:這個參數,我們可以使用其中的方法:buildComponent("組件名"),調用我們封裝的相關組件
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state, dispatch);
}
Widget _bodyWidget(CountState state, Dispatch dispatch) {
return Scaffold(
appBar: AppBar(
title: Text("FishRedux"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
///使用state中的變量,控住數據的變換
Text(state.count.toString()),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
///點擊事件,調用action 計數自增方法
dispatch(CountActionCreator.countIncrease());
},
child: Icon(Icons.add),
),
);
}
- action
- 該層是非常重要的模塊,頁面所有的行為都可以在本層直觀的看到
- XxxxAction中的枚舉字段是必須的,一個事件對應有一個枚舉字段,枚舉字段是:effect,reducer層標識的入口
- XxxxActionCreator類中的方法是中轉方法,方法中可以傳參數,參數類型可任意;方法中的參數放在Action類中的payload字段中,然后在effect,reducer中的action參數中拿到payload值去處理就行了
- 這地方需要注意下,默認生成的模板代碼,return的Action類加了const修飾,如果使用Action的payload字段賦值並攜帶數據,是會報錯的;所以這里如果需要攜帶參數,請去掉const修飾關鍵字
enum CountAction { increase, updateCount }
class CountActionCreator {
///去effect層去處理自增數據
static Action countIncrease() {
return Action(CountAction.increase);
}
///去reducer層更新數據,傳參可以放在Action類中的payload字段中,payload是dynamic類型,可傳任何類型
static Action updateCount(int count) {
return Action(CountAction.updateCount, payload: count);
}
}
- effect
- 如果在調用action里面的XxxxActionCreator類中的方法,相應的枚舉字段,會在combineEffects中被調用,在這里,我們就能寫相應的方法處理邏輯,方法中帶倆個參數:action,ctx
- action:該對象中,我們可以拿到payload字段里面,在action里面保存的值
- ctx:該對象中,可以拿到state的參數,還可以通過ctx調用dispatch方法,調用action中的方法,在這里調用dispatch方法,一般是把處理好的數據,通過action中轉到reducer層中更新數據
- 如果在調用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
- 該層是更新數據的,action中調用的XxxxActionCreator類中的方法,相應的枚舉字段,會在asReducer方法中回調,這里就可以寫個方法,克隆state數據進行一些處理,這里面有倆個參數:state,action
- state參數經常使用的是clone方法,clone一個新的state對象;action參數基本就是拿到其中的payload字段,將其中的值,賦值給state
Reducer<CountState> buildReducer() {
return asReducer(
<Object, Reducer<CountState>>{
CountAction.updateCount: _updateCount,
},
);
}
///通知View層更新界面
CountState _updateCount(CountState state, Action action) {
final CountState newState = state.clone();
newState..count = action.payload;
return newState;
}
- page模塊不需要改動,這邊就不貼代碼了
優化
-
從上面的例子看到,如此簡單數據變換,僅僅是個state中一個參數自增的過程,effect層就顯得有些多余;所以,把流程簡化成下面
- view ---> action ---> reducer
-
注意:這邊把effect層刪掉,該層可以舍棄了;然后對view,action,reducer層代碼進行一些小改動
搞起來
- view
- 這邊僅僅把點擊事件的方法,微微改了下:CountActionCreator.countIncrease()改成CountActionCreator.updateCount()
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state, dispatch);
}
Widget _bodyWidget(CountState state, Dispatch dispatch) {
return Scaffold(
appBar: AppBar(
title: Text("FishRedux"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Text(state.count.toString()),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
///點擊事件,調用action 計數自增方法
dispatch(CountActionCreator.updateCount());
},
child: Icon(Icons.add),
),
);
}
- action
- 這里只使用一個枚舉字段,和一個方法就行了,也不用傳啥參數了
enum CountAction { updateCount }
class CountActionCreator {
///去reducer層更新數據,傳參可以放在Action類中的payload字段中,payload是dynamic類型,可傳任何類型
static Action updateCount() {
return Action(CountAction.updateCount);
}
}
- reducer
- 這里直接在:_updateCount方法中處理下簡單的自增邏輯
Reducer<CountState> buildReducer() {
return asReducer(
<Object, Reducer<CountState>>{
CountAction.updateCount: _updateCount,
},
);
}
///通知View層更新界面
CountState _updateCount(CountState state, Action action) {
final CountState newState = state.clone();
newState..count = state.count + 1;
return newState;
}
搞定
- 可以看見優化了后,代碼量減少了很多,對待不同的業務場景,可以靈活的變動,使用框架,但不要拘泥框架;但是如果有網絡請求,很復雜的業務邏輯,就萬萬不能寫在reducer里面了,一定要寫在effect中,這樣才能保證一個清晰的解耦結構,保證處理數據和更新數據過程分離
頁面跳轉
效果圖
- 從效果圖,很容易看到,倆個頁面相互傳值
- FirstPage ---> SecondPage(FirstPage跳轉到SecondPage,並傳值給SecondPage頁面)
- SecondPage ---> FirstPage(SecondPage返回到FirstPage,並返回值給FirstPage頁面)
實現
- 從上面效果圖上看,很明顯,這邊需要實現倆個頁面,先看看main頁面的改動
- main
- 這里只增加了倆個頁面:FirstPage和SecondPage;並將主頁面入口換成了:FirstPage
Widget createApp() {
///定義路由
final AbstractRoutes routes = PageRoutes(
pages: <String, Page<Object, dynamic>>{
///計數器模塊演示
"CountPage": CountPage(),
///頁面傳值跳轉模塊演示
"FirstPage": FirstPage(),
"SecondPage": SecondPage(),
},
);
return MaterialApp(
title: 'FishRedux',
home: routes.buildPage("FirstPage", null), //作為默認頁面
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) {
return routes.buildPage(settings.name, settings.arguments);
},
settings: settings,
);
},
);
}
FirstPage
-
先來看看該頁面的一個流程
- view ---> action ---> effect(跳轉到SecondPage頁面)
- effect(拿到SecondPage返回的數據) ---> action ---> reducer(更新頁面數據)
-
state
- 先寫state文件,這邊需要定義倆個變量來
- fixedMsg:這個是傳給下個頁面的值
- msg:在頁面上展示傳值得變量
- initState方法是初始化變量和接受頁面傳值的,這邊我們給他賦個初始值
- 先寫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
- 該頁面邏輯相當簡單,主要的僅僅是在onPressed方法中處理邏輯
Widget buildView(FirstState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state, dispatch);
}
Widget _bodyWidget(FirstState state, Dispatch dispatch) {
return Scaffold(
appBar: AppBar(
title: Text("FirstPage"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('下方數據是SecondPage頁面傳遞過來的:'),
Text(state.msg),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
///跳轉到Second頁面
dispatch(FirstActionCreator.toSecond());
},
child: Icon(Icons.arrow_forward),
),
);
}
- action:這里需要定義倆個枚舉事件
- toSecond:跳轉到SecondPage頁面
- updateMsg:拿到SecondPage頁面返回的數據,然后更新頁面數據
enum FirstAction { toSecond , updateMsg}
class FirstActionCreator {
///跳轉到第二個頁面
static Action toSecond() {
return const Action(FirstAction.toSecond);
}
///拿到第二個頁面返回的數據,執行更新數據操作
static Action updateMsg(String msg) {
return Action(FirstAction.updateMsg, payload: msg);
}
}
- effect
- 此處需要注意:fish_redux 框架中的Action類和系統包中的重名了,需要把系統包中Action類隱藏掉
- 傳值直接用pushNamed方法即可,攜帶的參數可以寫在arguments字段中;pushNamed返回值是Future類型,如果想獲取他的返回值,跳轉方法就需要寫成異步的,等待從SecondPage頁面獲取返回的值,
/// 使用hide方法,隱藏系統包里面的Action類
import 'package:flutter/cupertino.dart' hide Action;
Effect<FirstState> buildEffect() {
return combineEffects(<Object, Effect<FirstState>>{
FirstAction.toSecond: _toSecond,
});
}
void _toSecond(Action action, Context<FirstState> ctx) async{
///頁面之間傳值;這地方必須寫個異步方法,等待上個頁面回傳過來的值;as關鍵字是類型轉換
var result = await Navigator.of(ctx.context).pushNamed("SecondPage", arguments: {"firstValue": FirstState.fixedMsg});
///獲取到數據,更新頁面上的數據
ctx.dispatch(FirstActionCreator.updateMsg( (result as Map)["secondValue"]) );
}
- reducer
- 這里就是從action里面獲取傳遞的值,賦值給克隆對象中msg字段即可
Reducer<FirstState> buildReducer() {
return asReducer(
<Object, Reducer<FirstState>>{
FirstAction.updateMsg: _updateMsg,
},
);
}
FirstState _updateMsg(FirstState state, Action action) {
return state.clone()..msg = action.payload;
}
SecondPage
- 這個頁面比較簡單,后續不涉及到頁面數據更新,所以reducer模塊可以不寫,看看該頁面的流程
- view ---> action ---> effect(pop當前頁面,並攜帶值返回)
- state
- 該模塊的變量和FirstPage類型,就不闡述了
- initState里面通過args變量獲取上個頁面傳遞的值,上個頁面傳值需要傳遞Map類型,這邊通過key獲取相應的value
class SecondState implements Cloneable<SecondState> {
///傳遞給下個頁面的值
static const String fixedMsg = "\n我是SecondPage頁面傳遞過來的數據:SecondValue";
///展示傳遞過來的值
String msg;
@override
SecondState clone() {
return SecondState()..msg = msg;
}
}
SecondState initState(Map<String, dynamic> args) {
///獲取上個頁面傳遞過來的數據
return SecondState()..msg = args["firstValue"];
}
- view
- 這邊需要注意的就是:WillPopScope控件接管AppBar的返回事件
Widget buildView(SecondState state, Dispatch dispatch, ViewService viewService) {
return WillPopScope(
child: _bodyWidget(state),
onWillPop: () {
dispatch(SecondActionCreator.backFirst());
///true:表示執行頁面返回 false:表示不執行返回頁面操作,這里因為要傳值,所以接管返回操作
return Future.value(false);
},
);
}
Widget _bodyWidget(SecondState state) {
return Scaffold(
appBar: AppBar(
title: Text("SecondPage"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('下方數據是FirstPage頁面傳遞過來的:'),
Text(state.msg),
],
),
),
);
}
- action
enum SecondAction { backFirst }
class SecondActionCreator {
///返回到第一個頁面,然后從棧中移除自身,同時傳回去一些數據
static Action backFirst() {
return Action(SecondAction.backFirst);
}
}
- effect
- 此處同樣需要隱藏系統包中的Action類
- 這邊直接在pop方法的第二個參數,寫入返回數據
///隱藏系統包中的Action類
import 'package:flutter/cupertino.dart' hide Action;
Effect<SecondState> buildEffect() {
return combineEffects(<Object, Effect<SecondState>>{
SecondAction.backFirst: _backFirst,
});
}
void _backFirst(Action action, Context<SecondState> ctx) {
///pop當前頁面,並且返回相應的數據
Navigator.pop(ctx.context, {"secondValue": SecondState.fixedMsg});
}
搞定
- 因為page模塊不需要改動,所以就沒必要將page模塊代碼附上了哈
- OK,到這里,咱們也已經把倆個頁面相互傳值的方式get到了!
列表文章
-
理解了上面倆個案例,相信你可以使用fish_redux實現一部分頁面了;但是,我們堆頁面的過程中,能體會列表模塊是非常重要的一部分,現在就來學學,在fish_redux中怎么使用ListView吧!
- 廢話少說,上號!
列表展示-網絡請求
效果圖
- 效果圖對於列表的滾動,做了倆個操作:一個是拖拽列表;另一個是滾動鼠標的滾輪。flutter對鼠標觸發的相關事件也支持的越來越好了!
- 這邊我們使用的是玩Android的api,這個api有個坑的地方,沒設置開啟跨域,所以運行在web上,這個api使用會報錯,我在玩Android的github上提了issue,哎,也不知道作者啥時候解決,,,
- 這地方只能曲線救國,關閉瀏覽器跨域限制,設置看這里:https://www.jianshu.com/p/56b1e01e6b6a
- 如果運行在虛擬機上,就完全不會出現這個問題!
准備
- 先看下文件結構
- main
- 這邊改動非常小,只在路由里,新增了:GuidePage,ListPage;同時將home字段中的默認頁面,改成了:GuidePage頁面;導航頁面代碼就不貼在文章里了,下面貼下該頁面鏈接
- ListPage才是重點,下文會詳細說明
void main() {
runApp(createApp());
}
Widget createApp() {
///定義路由
final AbstractRoutes routes = PageRoutes(
pages: <String, Page<Object, dynamic>>{
///導航頁面
"GuidePage": GuidePage(),
///計數器模塊演示
"CountPage": CountPage(),
///頁面傳值跳轉模塊演示
"FirstPage": FirstPage(),
"SecondPage": SecondPage(),
///列表模塊演示
"ListPage": ListPage(),
},
);
return MaterialApp(
title: 'FishRedux',
home: routes.buildPage("GuidePage", null), //作為默認頁面
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) {
return routes.buildPage(settings.name, settings.arguments);
},
settings: settings,
);
},
);
}
流程
- Adapter實現的流程
- 創建item(Component) ---> 創建adapter文件 ---> state集成相應的Source ---> page里面綁定adapter
- 通過以上四步,就能在fish_redux使用相應列表里面的adapter了,過程有點麻煩,但是熟能生巧,多用用就能很快搭建一個復雜的列表了
- 總流程:初始化列表模塊 ---> item模塊 ---> 列表模塊邏輯完善
- 初始化列表模塊
- 這個就是正常的創建fish_redux模板代碼和文件
- item模塊
- 根據接口返回json,創建相應的bean ---> 創建item模塊 ---> 編寫state ---> 編寫view界面
- 列表模塊邏輯完善:倆地方分倆步(adapter創建及其綁定,正常page頁面編輯)
- 創建adapter文件 ---> state調整 ---> page中綁定adapter
- view模塊編寫 ---> action添加更新數據事件 ---> effect初始化時獲取數據並處理 ---> reducer更新數據
- 初始化列表模塊
- 整體流程確實有些多,但是咱們按照整體三步流程流程走,保證思路清晰就行了
初始化列表模塊
- 此處新建個文件夾,在文件夾上新建fis_redux文件就行了;這地方,我們選擇page,整體的五個文件:action,effect,reducer,state,view;全部都要用到,所以默認全選,填入Module的名字,點擊OK
item模塊
按照流程走
- 根據接口返回json,創建相應的bean ---> 創建item模塊 ---> 編寫state ---> 編寫view界面
准備工作
-
創建bean實體
- 根據api返回的json數據,生成相應的實體
- json轉實體
- 網站:https://javiercbk.github.io/json_to_dart/
- 插件:AS中可以搜索:FlutterJsonBeanFactory
- 這地方生成了:ItemDetailBean;代碼倆百多行就不貼了,具體的內容,點擊下面鏈接
-
創建item模塊
- 這邊我們實現一個簡單的列表,item僅僅做展示功能;不做點擊,更新ui等操作,所以這邊我們就不需要創建:effect,reducer,action文件;只選擇:state和view就行了
- 創建item,這里選擇component
文件結構
OK,bean文件搞定了,再來看看,item文件中的文件,這里component文件不需要改動,所以這地方,我們只需要看:state.dart,view.dart
- state
- 這地方還是常規的寫法,因為json生成的bean里面,能用到的所有數據,都在Datas類里面,所以,這地方建一個Datas類的變量即可
- 因為,沒用到reducer,實際上clone實現方法都能刪掉,防止后面可能需要clone對象,暫且留着
import 'package:fish_redux/fish_redux.dart';
import 'package:fish_redux_demo/list/bean/item_detail_bean.dart';
class ItemState implements Cloneable<ItemState> {
Datas itemDetail;
ItemState({this.itemDetail});
@override
ItemState clone() {
return ItemState()
..itemDetail = itemDetail;
}
}
ItemState initState(Map<String, dynamic> args) {
return ItemState();
}
- view
- 這里item布局稍稍有點麻煩,整體上采用的是:水平布局(Row),分左右倆大塊
- 左邊:單純的圖片展示
- 右邊:采用了縱向布局(Column),結合Expanded形成比例布局,分別展示三塊東西:標題,內容,作者和時間
- OK,這邊view只是簡單用到了state提供的數據形成的布局,沒有什么要特別注意的地方
- 這里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文件 ---> state調整 ---> page中綁定adapter
adapter創建及其綁定
- 創建adapter
- 首先需要創建adapter文件,然后寫入下面代碼:這地方需要繼承SourceFlowAdapter適配器,里面的泛型需要填入ListState,ListState這地方會報錯,因為我們的ListState沒有繼承MutableSource,下面state的調整就是對這個的處理
- ListItemAdapter的構造函數就是通用的寫法了,在super里面寫入我們上面寫好item樣式,這是個pool應該可以理解為樣式池,這個key最好都提出來,因為在state模塊還需要用到,可以定義多個不同的item,很容易做成多樣式item的列表;目前,我們這邊只需要用一個,填入:ItemComponent()
class ListItemAdapter extends SourceFlowAdapter<ListState> {
static const String item_style = "project_tab_item";
ListItemAdapter()
: super(
pool: <String, Component<Object>>{
///定義item的樣式
item_style: ItemComponent(),
},
);
}
- state調整
- state文件中的代碼需要做一些調整,需要繼承相應的類,和adapter建立起關聯
- ListState需要繼承MutableSource;還必須定義一個泛型是item的ItemState類型的List,這倆個是必須的;然后實現相應的抽象方法就行了
- 這里只要向items里寫入ItemState的數據,列表就會更新了
class ListState extends MutableSource implements Cloneable<ListState> {
///這地方一定要注意,List里面的泛型,需要定義為ItemState
///怎么更新列表數據,只需要更新這個items里面的數據,列表數據就會相應更新
///使用多樣式,請寫出 List<Object> items;
List<ItemState> items;
@override
ListState clone() {
return ListState()..items = items;
}
///使用上面定義的List,繼承MutableSource,就把列表和item綁定起來了
@override
Object getItemData(int index) => items[index];
@override
String getItemType(int index) => ListItemAdapter.item_style;
@override
int get itemCount => items.length;
@override
void setItemData(int index, Object data) {
items[index] = data;
}
}
ListState initState(Map<String, dynamic> args) {
return ListState();
}
- page中綁定adapter
- 這里就是將我們的ListSate和ListItemAdapter適配器建立起連接
class ListPage extends Page<ListState, Map<String, dynamic>> {
ListPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<ListState>(
///綁定Adapter
adapter: NoneConn<ListState>() + ListItemAdapter(),
slots: <String, Dependent<ListState>>{}),
middleware: <Middleware<ListState>>[],
);
}
正常page頁面編輯
整體流程
-
view模塊編寫 ---> action添加更新數據事件 ---> effect初始化時獲取數據並處理 ---> reducer更新數據
-
view
- 這里面的列表使用就相當簡單了,填入itemBuilder和itemCount參數就行了,這里就需要用viewService參數了哈
Widget buildView(ListState state, Dispatch dispatch, ViewService viewService) {
return Scaffold(
appBar: AppBar(
title: Text("ListPage"),
),
body: _itemWidget(state, viewService),
);
}
Widget _itemWidget(ListState state, ViewService viewService) {
if (state.items != null) {
///使用列表
return ListView.builder(
itemBuilder: viewService.buildAdapter().itemBuilder,
itemCount: viewService.buildAdapter().itemCount,
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
}
- action
- 只需要寫個更新items的事件就ok了
enum ListAction { updateItem }
class ListActionCreator {
static Action updateItem(var list) {
return Action(ListAction.updateItem, payload: list);
}
}
- effect
- Lifecycle.initState是進入頁面初始化的回調,這邊可以直接用這個狀態回調,來請求接口獲取相應的數據,然后去更新列表
- 這地方有個坑,dio必須結合json序列號和反序列的庫一起用,不然Dio無法將數據源解析成Response類型
Effect<ListState> buildEffect() {
return combineEffects(<Object, Effect<ListState>>{
///進入頁面就執行的初始化操作
Lifecycle.initState: _init,
});
}
void _init(Action action, Context<ListState> ctx) async {
String apiUrl = "https://www.wanandroid.com/project/list/1/json";
Response response = await Dio().get(apiUrl);
ItemDetailBean itemDetailBean =
ItemDetailBean.fromJson(json.decode(response.toString()));
List<Datas> itemDetails = itemDetailBean.data.datas;
///構建符合要求的列表數據源
List<ItemState> items = List.generate(itemDetails.length, (index) {
return ItemState(itemDetail: itemDetails[index]);
});
///通知更新列表數據源
ctx.dispatch(ListActionCreator.updateItem(items));
}
- reducer
- 最后就是更新操作了哈,這里就是常規寫法了
Reducer<ListState> buildReducer() {
return asReducer(
<Object, Reducer<ListState>>{
ListAction.updateItem: _updateItem,
},
);
}
ListState _updateItem(ListState state, Action action) {
return state.clone()..items = action.payload;
}
列表修改-單item刷新
效果圖
- 這次來演示列表的單item更新,沒有網絡請求的操作,所以代碼邏輯就相當簡單了
結構
- 來看看代碼結構
- 這地方很明顯得發現,list_edit主體文件很少,因為這邊直接在state里初始化了數據源,就沒有后期更新數據的操作,所以就不需要:action,effect,reducer這三個文件!item模塊則直接在reducer里更新數據,不涉及相關復雜的邏輯,所以不需要:effect文件。
列表模塊
-
這次列表模塊是非常的簡單,基本不涉及什么流程,就是最基本初始化的一個過程,將state里初始化的數據在view中展示
- state ---> view
-
state
- 老規矩,先來看看state中的代碼
- 這里一些新建了變量,泛型是ItemState(item的State),items變量初始化了一組數據;然后,同樣繼承了MutableSource,實現其相關方法
class ListEditState extends MutableSource implements Cloneable<ListEditState> {
List<ItemState> items;
@override
ListEditState clone() {
return ListEditState()..items = items;
}
@override
Object getItemData(int index) => items[index];
@override
String getItemType(int index) => ListItemAdapter.itemName;
@override
int get itemCount => items.length;
@override
void setItemData(int index, Object data) {
items[index] = data;
}
}
ListEditState initState(Map<String, dynamic> args) {
return ListEditState()
..items = [
ItemState(id: 1, title: "列表Item-1", itemStatus: false),
ItemState(id: 2, title: "列表Item-2", itemStatus: false),
ItemState(id: 3, title: "列表Item-3", itemStatus: false),
ItemState(id: 4, title: "列表Item-4", itemStatus: false),
ItemState(id: 5, title: "列表Item-5", itemStatus: false),
ItemState(id: 6, title: "列表Item-6", itemStatus: false),
];
}
- view
- view的代碼主體僅僅是個ListView.builder,沒有什么額外Widget
Widget buildView(ListEditState state, Dispatch dispatch, ViewService viewService) {
return Scaffold(
appBar: AppBar(
title: Text("ListEditPage"),
),
body: ListView.builder(
itemBuilder: viewService.buildAdapter().itemBuilder,
itemCount: viewService.buildAdapter().itemCount,
),
);
}
- adapter
- 和上面類型,adapter繼承SourceFlowAdapter適配器
class ListItemAdapter extends SourceFlowAdapter<ListEditState> {
static const String itemName = "item";
ListItemAdapter()
: super(
pool: <String, Component<Object>>{itemName: ItemComponent()},
);
}
- page
- 在page里面綁定adapter
class ListEditPage extends Page<ListEditState, Map<String, dynamic>> {
ListEditPage()
: super(
initState: initState,
view: buildView,
dependencies: Dependencies<ListEditState>(
///綁定適配器
adapter: NoneConn<ListEditState>() + ListItemAdapter(),
slots: <String, Dependent<ListEditState>>{}),
middleware: <Middleware<ListEditState>>[],
);
}
item模塊
- 接下就是比較重要的item模塊了,item模塊的流程,也是非常的清晰
- view ---> action ---> reducer
- state
- 老規矩,先來看看state里面的代碼;此處就是寫常規變量的定義,這些在view中都能用得着
class ItemState implements Cloneable<ItemState> {
int id;
String title;
bool itemStatus;
ItemState({this.id, this.title, this.itemStatus});
@override
ItemState clone() {
return ItemState()
..title = title
..itemStatus = itemStatus
..id = id;
}
}
ItemState initState(Map<String, dynamic> args) {
return ItemState();
}
- view
- 可以看到Checkbox的內部點擊操作,我們傳遞了一個id參數,注意這個id參數是必須的,在更新item的時候來做區分用的
Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
return Container(
child: InkWell(
onTap: () {},
child: ListTile(
title: Text(state.title),
trailing: Checkbox(
value: state.itemStatus,
///Checkbox的點擊操作:狀態變更
onChanged: (value) => dispatch(ItemActionCreator.onChange(state.id)),
),
),
),
);
}
- action
- 一個狀態改變的事件
enum ItemAction { onChange }
class ItemActionCreator {
//狀態改變
static Action onChange(int id) {
return Action(ItemAction.onChange, payload: id);
}
}
- reducer
- _onChange會回調所有ItemState,所以這地方必須用id或其它唯一標識去界定,我們所操作的item具體是哪一個
- _onChange方法,未操作的item返回的時候要注意,需要返回:state原對象,標明該state對象未變動,其item不需要刷新;不能返回state.clone(),這樣返回的就是個全新的state對象,每個item都會刷新,還會造成一個很奇怪的bug,會造成后續點擊item操作失靈
Reducer<ItemState> buildReducer() {
return asReducer(
<Object, Reducer<ItemState>>{
ItemAction.onChange: _onChange,
},
);
}
ItemState _onChange(ItemState state, Action action) {
if (state.id == action.payload) {
return state.clone()..itemStatus = !state.itemStatus;
}
///這地方一定要注意,要返回:state;不能返回:state.clone(),否則會造成后續更新失靈
return state;
}
多樣式列表
注意:如果使用多樣式,items的列表泛型不要寫成ItemState,寫成Object就行了;在下面代碼,我們可以看到,實現的getItemData()方法返回的類型是Object,所以Items的列表泛型寫成Object,是完全可以的。
- 我們定義數據源的時候把泛型寫成Object是完全可以的,但是初始化數據的時候一定要注意,寫成對應adapter類型里面的state
- 假設一種情況,在index是奇數時展示:OneComponent;在index是奇數時展示:TwoComponent;
- getItemType:這個重寫方法里面,在index為奇偶數時分別返回:OneComponent和TwoComponent的標識
- 數據賦值時也一定要在index為奇偶數時賦值泛型分別為:OneState和TwoState
- 也可以這樣優化去做,在getItemType里面判斷當前泛型是什么數據類型,然后再返回對應的XxxxComponent的標識
- 數據源的數據類型必須和getItemType返回的XxxxComponent的標識相對應,如果數據源搞成Object類型,映射到對應位置的item數據時,會報類型不適配的錯誤
下述代碼可做思路參考
class ListState extends MutableSource implements Cloneable<PackageCardState> {
List<Object> items;
@override
ListState clone() {
return PackageCardState()..items = items;
}
@override
Object getItemData(int index) => items[index];
@override
String getItemType(int index) {
if(items[index] is OneState) {
return PackageCardAdapter.itemStyleOne;
}else{
return PackageCardAdapter.itemStyleTwo;
}
}
@override
int get itemCount => items.length;
@override
void setItemData(int index, Object data) => items[index] = data;
}
列表存在的問題+解決方案
列表多item刷新問題
這里搞定了單item刷新場景,還存在一種多item刷新的場景
- 說明下,列表item是沒辦法一次刷新多個item的,只能一次刷新一個item(一個clone對應着一次刷新),一個事件對應着刷新一個item;這邊是打印多個日志分析出來了
- 解決:解決辦法是,多個事件去處理刷新操作
舉例:假設一種場景,對於上面的item只能單選,一個item項被選中,其它item狀態被重置到未選狀態,具體效果看下方效果圖
- 效果圖
-
這種效果的實現非常簡單,但是如果思路不對,會掉進坑里出不來
-
還原被選的狀態,不能在同一個事件里寫,需要新寫一個清除事件
下述代碼為整體流程
- view
Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
return InkWell(
onTap: () {},
child: ListTile(
title: Text(state.title),
trailing: Checkbox(
value: state.itemStatus,
///CheckBox的點擊操作:狀態變更
onChanged: (value) {
//單選模式,清除選中的item,以便做單選
dispatch(ItemActionCreator.clear());
//刷新選中item
dispatch(ItemActionCreator.onChange(state.id));
}
),
),
);
}
- action
enum ItemAction {
onChange,
clear,
}
class ItemActionCreator {
//狀態改變
static Action onChange(int id) {
return Action(ItemAction.onChange, payload: id);
}
//清除改變的狀態
static Action clear() {
return Action(ItemAction.clear);
}
}
- reducer
Reducer<ItemState> buildReducer() {
return asReducer(
<Object, Reducer<ItemState>>{
ItemAction.onChange: _onChange,
ItemAction.clear: _clear,
},
);
}
ItemState _onChange(ItemState state, Action action) {
if (state.id == action.payload) {
return state.clone()..itemStatus = !state.itemStatus;
}
///這地方一定要注意,要返回:state;不能返回:state.clone(),否則會造成后續更新失靈
return state;
}
///單選模式
ItemState _clear(ItemState state, Action action) {
if (state.itemStatus) {
return state.clone()..itemStatus = false;
}
///這地方一定要注意,要返回:state;不能返回:state.clone(),否則會造成后續更新失靈
return state;
}
這個問題實際上解決起來很簡單,但是如果一直在 _onChange 方法重置狀態,你會發現和你預期的結果一直對不上;完整且詳細的效果,可以去看demo里面代碼
搞定
-
呼,終於將列表這塊寫完,說實話,這個列表的使用確實有點麻煩;實際上,如果大家用心看了的話,麻煩的地方,其實就是在這塊:adapter創建及其綁定;只能多寫寫了,熟能生巧!
-
列表模塊大功告成,以后就能愉快的寫列表了!
全局模式
效果圖
- 理解了上面的是三個例子,相信大部分頁面,對於你來說都不在話下了;現在我們再來看個例子,官方提供的全局主題功能,當然,這不僅僅是全局主題,全局字體樣式,字體大小等等,都是可以全局管理,當然了,寫app之前要做好規划
開搞
store模塊
- 文件結構
- 這地方需要新建一個文件夾,新建四個文件:action,reducer,state,store
- state
- 老規矩,先來看看state,我們這里只在抽象類里面定義了一個主題色,這個抽象類是很重要的,需要做全局模式所有子模塊的state,都必須實現這個抽象類
abstract class GlobalBaseState{
Color themeColor;
}
class GlobalState implements GlobalBaseState, Cloneable<GlobalState>{
@override
Color themeColor;
@override
GlobalState clone() {
return GlobalState();
}
}
- action
- 因為只做切換主題色,這地方只需要定義一個事件即可
enum GlobalAction { changeThemeColor }
class GlobalActionCreator{
static Action onChangeThemeColor(){
return const Action(GlobalAction.changeThemeColor);
}
}
- reducer
- 這里就是處理變色的一些操作,這是咸魚官方demo里面代碼;這說明簡單的邏輯,是可以放在reducer里面寫的
import 'package:flutter/material.dart' hide Action;
Reducer<GlobalState> buildReducer(){
return asReducer(
<Object, Reducer<GlobalState>>{
GlobalAction.changeThemeColor: _onChangeThemeColor,
},
);
}
List<Color> _colors = <Color>[
Colors.green,
Colors.red,
Colors.black,
Colors.blue
];
GlobalState _onChangeThemeColor(GlobalState state, Action action) {
final Color next =
_colors[((_colors.indexOf(state.themeColor) + 1) % _colors.length)];
return state.clone()..themeColor = next;
}
- store
- 切換全局狀態的時候,就需要調用這個類了
/// 建立一個AppStore
/// 目前它的功能只有切換主題
class GlobalStore{
static Store<GlobalState> _globalStore;
static Store<GlobalState> get store => _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer());
}
main改動
- 這里面將PageRoutes里面的visitor字段使用起來,狀態更新操作代碼有點多,就單獨提出來了;所以main文件里面,增加了:
- visitor字段使用
- 增加_updateState方法
void main() {
runApp(createApp());
}
Widget createApp() {
///全局狀態更新
_updateState() {
return (Object pageState, GlobalState appState) {
final GlobalBaseState p = pageState;
if (pageState is Cloneable) {
final Object copy = pageState.clone();
final GlobalBaseState newState = copy;
if (p.themeColor != appState.themeColor) {
newState.themeColor = appState.themeColor;
}
/// 返回新的 state 並將數據設置到 ui
return newState;
}
return pageState;
};
}
final AbstractRoutes routes = PageRoutes(
///全局狀態管理:只有特定的范圍的Page(State繼承了全局狀態),才需要建立和 AppStore 的連接關系
visitor: (String path, Page<Object, dynamic> page) {
if (page.isTypeof<GlobalBaseState>()) {
///建立AppStore驅動PageStore的單向數據連接: 參數1 AppStore 參數2 當AppStore.state變化時,PageStore.state該如何變化
page.connectExtraStore<GlobalState>(GlobalStore.store, _updateState());
}
},
///定義路由
pages: <String, Page<Object, dynamic>>{
///導航頁面
"GuidePage": GuidePage(),
///計數器模塊演示
"CountPage": CountPage(),
///頁面傳值跳轉模塊演示
"FirstPage": FirstPage(),
"SecondPage": SecondPage(),
///列表模塊演示
"ListPage": ListPage(),
},
);
return MaterialApp(
title: 'FishRedux',
home: routes.buildPage("GuidePage", null), //作為默認頁面
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) {
return routes.buildPage(settings.name, settings.arguments);
},
settings: settings,
);
},
);
}
子模塊使用
- 這里就用計數器模塊的來舉例,因為僅僅只需要改動少量代碼,且只涉及state和view,所以其它模塊代碼也不重復貼出了
- state
- 這地方,僅僅讓CountState多實現了GlobalBaseState類,很小的改動
class CountState implements Cloneable<CountState>,GlobalBaseState {
int count;
@override
CountState clone() {
return CountState()
..count = count
..themeColor = themeColor;
}
@override
Color themeColor;
}
CountState initState(Map<String, dynamic> args) {
return CountState()..count = 0;
}
- view
- 這里面僅僅改動了一行,在AppBar里面加了backgroundColor,然后使用state里面的全局主題色
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state, dispatch);
}
Widget _bodyWidget(CountState state, Dispatch dispatch) {
return Scaffold(
appBar: AppBar(
title: Text("FishRedux"),
///全局主題,僅僅在此處改動了一行
backgroundColor: state.themeColor,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Text(state.count.toString()),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
///點擊事件,調用action 計數自增方法
dispatch(CountActionCreator.updateCount());
},
child: Icon(Icons.add),
),
);
}
- 如果其他模塊也需要做主題色,也按照此處邏輯改動即可
調用
- 調用狀態更新就非常簡單了,和正常模塊更新View一樣,這里我們調用全局的就行了,一行代碼搞定,在需要的地方調用就OK了
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());
搞定
- 經過上面的的三步,我們就可以使用全局狀態了;從上面子模塊的使用,可以很明顯的感受到,全局狀態,必須前期做好字段的規划,確定之后,最好不要再增加字段,不然繼承抽象類的多個模塊都會爆紅,提示去實現xxx變量
全局模塊優化
反思
在上面的全局模式里說了,使用全局模塊,前期需要規划好字段,不然項目進行到中期的時候,想添加字段,多個模塊的State會出現大范圍爆紅,提示去實現你添加的字段;項目開始規划好所有的字段,顯然這需要全面的考慮好大部分場景,但是人的靈感總是無限的,不改代碼是不可能,這輩子都不可能。只能想辦法看能不能添加一次字段后,后期添加字段,並不會引起其他模塊爆紅,試了多次,成功的使用中間實體,來解決該問題
這里優化倆個方面
- 使用通用的全局實體
- 這樣后期添加字段,就不會影響其他模塊,這樣我們就能一個個模塊的去整改,不會出現整個項目不能運行的情況
- 將路由模塊和全局模塊封裝
- 路由模塊后期頁面多了,代碼會很多,放在主入口,真的不好管理;全局模塊同理
因為使用中間實體,有一些地方會出現空指針問題,我都在流程里面寫清楚了,大家可以把優化流程完整看一遍哈,都配置好,后面拓展使用就不會報空指針了
優化
入口模塊
- main:大改
- 從下面代碼可以看到,這里將路由模塊和全局模塊單獨提出來了,這地方為了方便觀看,就寫在一個文件里;說明下,RouteConfig和StoreConfig這倆個類,可以放在倆個不同的文件里,這樣管理路由和全局字段更新就會很方便了!
- RouteConfig:這里將頁面標識和頁面映射分開寫,這樣我們跳轉頁面的時候,就可以直接引用RouteConfig里面的頁面標識
- StoreConfig:全局模塊里最重要的就是狀態的判斷,注釋寫的很清楚了,可以看看注釋哈
void main() {
runApp(createApp());
}
Widget createApp() {
return MaterialApp(
title: 'FishRedux',
home: RouteConfig.routes.buildPage(RouteConfig.guidePage, null), //作為默認頁面
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) {
return RouteConfig.routes.buildPage(settings.name, settings.arguments);
},
settings: settings,
);
},
);
}
///路由管理
class RouteConfig {
///定義你的路由名稱比如 static final String routeHome = 'page/home';
///導航頁面
static const String guidePage = 'page/guide';
///計數器頁面
static const String countPage = 'page/count';
///頁面傳值跳轉模塊演示
static const String firstPage = 'page/first';
static const String secondPage = 'page/second';
///列表模塊演示
static const String listPage = 'page/list';
static const String listEditPage = 'page/listEdit';
static final AbstractRoutes routes = PageRoutes(
pages: <String, Page<Object, dynamic>>{
///將你的路由名稱和頁面映射在一起,比如:RouteConfig.homePage : HomePage(),
RouteConfig.guidePage: GuidePage(),
RouteConfig.countPage: CountPage(),
RouteConfig.firstPage: FirstPage(),
RouteConfig.secondPage: SecondPage(),
RouteConfig.listPage: ListPage(),
RouteConfig.listEditPage: ListEditPage(),
},
visitor: StoreConfig.visitor,
);
}
///全局模式
class StoreConfig {
///全局狀態管理
static _updateState() {
return (Object pageState, GlobalState appState) {
final GlobalBaseState p = pageState;
if (pageState is Cloneable) {
final Object copy = pageState.clone();
final GlobalBaseState newState = copy;
if (p.store == null) {
///這地方的判斷是必須的,判斷第一次store對象是否為空
newState.store = appState.store;
} else {
/// 這地方增加字段判斷,是否需要更新
if ((p.store.themeColor != appState.store.themeColor)) {
newState.store.themeColor = appState.store.themeColor;
}
/// 如果增加字段,同理上面的判斷然后賦值...
}
/// 返回新的 state 並將數據設置到 ui
return newState;
}
return pageState;
};
}
static visitor(String path, Page<Object, dynamic> page) {
if (page.isTypeof<GlobalBaseState>()) {
///建立AppStore驅動PageStore的單向數據連接
///參數1 AppStore 參數2 當AppStore.state變化時,PageStore.state該如何變化
page.connectExtraStore<GlobalState>(GlobalStore.store, _updateState());
}
}
}
Store模塊
下面倆個模塊是需要改動代碼的模塊
- state
- 這里使用了StoreModel中間實體,注意,這地方實體字段store,初始化是必須的,不然在子模塊引用該實體下的字段會報空指針
abstract class GlobalBaseState{
StoreModel store;
}
class GlobalState implements GlobalBaseState, Cloneable<GlobalState>{
@override
GlobalState clone() {
return GlobalState();
}
@override
StoreModel store = StoreModel(
/// store這個變量,在這必須示例化,不然引用該變量中的字段,會報空指針
/// 下面的字段,賦初值,就是初始時展示的全局狀態
/// 這地方初值,理應從緩存或數據庫中取,表明用戶選擇的全局狀態
themeColor: Colors.lightBlue
);
}
///中間全局實體
///需要增加字段就在這個實體里面添加就行了
class StoreModel {
Color themeColor;
StoreModel({this.themeColor});
}
- reducer
- 這地方改動非常小,將state.themeColor改成state.store.themeColor
Reducer<GlobalState> buildReducer(){
return asReducer(
<Object, Reducer<GlobalState>>{
GlobalAction.changeThemeColor: _onChangeThemeColor,
},
);
}
List<Color> _colors = <Color>[
Colors.green,
Colors.red,
Colors.black,
Colors.blue
];
GlobalState _onChangeThemeColor(GlobalState state, Action action) {
final Color next =
_colors[((_colors.indexOf(state.store.themeColor) + 1) % _colors.length)];
return state.clone()..store.themeColor = next;
}
下面倆個模塊代碼沒有改動,但是為了思路完整,同樣貼出來
- action
enum GlobalAction { changeThemeColor }
class GlobalActionCreator{
static Action onChangeThemeColor(){
return const Action(GlobalAction.changeThemeColor);
}
}
- store
class GlobalStore{
static Store<GlobalState> _globalStore;
static Store<GlobalState> get store => _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer());
}
子模塊使用
- 這里就用計數器模塊的來舉例,因為僅僅只需要改動少量代碼,且只涉及state和view,所以其它模塊代碼也不重復貼出了
- state
- 因為是用中間實體,所以在clone方法里面必須將實現的store字段加上,不然會報空指針
class CountState implements Cloneable<CountState>, GlobalBaseState {
int count;
@override
CountState clone() {
return CountState()
..count = count
..store = store;
}
@override
StoreModel store;
}
CountState initState(Map<String, dynamic> args) {
return CountState()..count = 0;
}
- view
- 這里面僅僅改動了一行,在AppBar里面加了backgroundColor,然后使用state里面的全局主題色
Widget buildView(CountState state, Dispatch dispatch, ViewService viewService) {
return _bodyWidget(state, dispatch);
}
Widget _bodyWidget(CountState state, Dispatch dispatch) {
return Scaffold(
appBar: AppBar(
title: Text("FishRedux"),
///全局主題,僅僅在此處改動了一行
backgroundColor: state.store.themeColor,
),
///下面其余代碼省略....
}
- 如果其他模塊也需要做主題色,也按照此處邏輯改動即可
調用
- 調用和上面說的一樣,用下述全局方式在合適的地方調用
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());
體驗
通過上面的優化,使用體驗提升不是一個級別,大大提升的全局模式的擴展性,我們就算后期增加了大量的全局字段,也可以一個個模塊慢慢改,不用一次爆肝全改完,猝死的概率又大大減少了!
Component使用
Component是個比較常用的模塊,上面使用列表的時候,就使用到了Component,這次我們來看看,在頁面中直接使用Component,可插拔式使用!Component的使用總的來說是比較簡單了,比較關鍵的是在State中建立起連接。
效果圖
- 上圖的效果是在頁面中嵌入了倆個Component,改變子Component的操作是在頁面中完成的
- 先看下頁面結構
Component
這地方寫了一個Component,代碼很簡單,來看看吧
- component
這地方代碼是自動生成了,沒有任何改動,就不貼了
- state
- initState():我們需要注意,Component中的initState()方法在內部沒有調用,雖然自動生成的代碼有這個方法,但是無法起到初始化作用,可以刪掉該方法
class AreaState implements Cloneable<AreaState> {
String title;
String text;
Color color;
AreaState({
this.title = "",
this.color = Colors.blue,
this.text = "",
});
@override
AreaState clone() {
return AreaState()
..color = color
..text = text
..title = title;
}
}
- view
Widget buildView(
AreaState state, Dispatch dispatch, ViewService viewService) {
return Scaffold(
appBar: AppBar(
title: Text(state.title),
automaticallyImplyLeading: false,
),
body: Container(
height: double.infinity,
width: double.infinity,
alignment: Alignment.center,
color: state.color,
child: Text(state.text),
),
);
}
Page
CompPage中,沒用到effete這層,就沒創建該文件,老規矩,先看看state
- state
- 這地方是非常重要的地方,XxxxConnecto的實現形式是看官方代碼寫的
- computed():該方法是必須實現的,這個類似直接的get()方法,但是切記不能像get()直接返回state.leftAreaState()或state.rightAreaState,某些場景初始化無法刷新,因為是同一個對象,會被判斷未更改,所以會不刷新控件
- 注意了注意了,這邊做了優化,直接返回clone方法,這是對官方賦值寫法的一個優化,也可以避免上面說的問題,大家可以思考思考
- set():該方法是Component數據流回推到頁面的state,保持倆者state數據一致;如果Component模塊更新了自己的State,不寫這個方法會報錯的
class CompState implements Cloneable<CompState> {
AreaState leftAreaState;
AreaState rightAreaState;
@override
CompState clone() {
return CompState()
..rightAreaState = rightAreaState
..leftAreaState = leftAreaState;
}
}
CompState initState(Map<String, dynamic> args) {
///初始化數據
return CompState()
..rightAreaState = AreaState(
title: "LeftAreaComponent",
text: "LeftAreaComponent",
color: Colors.indigoAccent,
)
..leftAreaState = AreaState(
title: "RightAreaComponent",
text: "RightAreaComponent",
color: Colors.blue,
);
}
///左邊Component連接器
class LeftAreaConnector extends ConnOp<CompState, AreaState>
with ReselectMixin<CompState, AreaState> {
@override
AreaState computed(CompState state) {
return state.leftAreaState.clone();
}
@override
void set(CompState state, AreaState subState) {
state.leftAreaState = subState;
}
}
///右邊Component連接器
class RightAreaConnector extends ConnOp<CompState, AreaState>
with ReselectMixin<CompState, AreaState> {
@override
AreaState computed(CompState state) {
return state.rightAreaState.clone();
}
@override
void set(CompState state, AreaState subState) {
state.rightAreaState = subState;
}
}
- page
- 寫完連接器后,我們在Page里面綁定下,就能使用Component了
class CompPage extends Page<CompState, Map<String, dynamic>> {
CompPage()
: super(
initState: initState,
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<CompState>(
adapter: null,
slots: <String, Dependent<CompState>>{
//綁定Component
"leftArea": LeftAreaConnector() + AreaComponent(),
"rightArea": RightAreaConnector() + AreaComponent(),
}),
middleware: <Middleware<CompState>>[],
);
}
- view
- 使用Component就非常簡單了:viewService.buildComponent("xxxxxx")
Widget buildView(CompState state, Dispatch dispatch, ViewService viewService) {
return Container(
color: Colors.white,
child: Column(
children: [
///Component組件部分
Expanded(
flex: 3,
child: Row(
children: [
Expanded(child: viewService.buildComponent("leftArea")),
Expanded(child: viewService.buildComponent("rightArea")),
],
),
),
///按鈕
Expanded(
flex: 1,
child: Center(
child: RawMaterialButton(
fillColor: Colors.blue,
shape: StadiumBorder(),
onPressed: () => dispatch(CompActionCreator.change()),
child: Text("改變"),
),
))
],
),
);
}
- action
enum CompAction { change }
class CompActionCreator {
static Action change() {
return const Action(CompAction.change);
}
}
- reducer
Reducer<CompState> buildReducer() {
return asReducer(
<Object, Reducer<CompState>>{
CompAction.change: _change,
},
);
}
CompState _change(CompState state, Action action) {
final CompState newState = state.clone();
//改變leftAreaComponent中state
newState.leftAreaState.text = "LeftAreaState:${Random().nextInt(1000)}";
newState.leftAreaState.color =
Color.fromRGBO(randomColor(), randomColor(), randomColor(), 1);
//改變rightAreaComponent中state
newState.rightAreaState.text = "RightAreaState:${Random().nextInt(1000)}";
newState.rightAreaState.color =
Color.fromRGBO(randomColor(), randomColor(), randomColor(), 1);
return newState;
}
int randomColor() {
return Random().nextInt(255);
}
總結下
總的來說,Component的使用還是比較簡單的;如果我們把某個復雜的列表提煉出一個Component的,很明顯有個初始化的過程,這里我們需要將:請求參數調體或列表詳情操作,在page頁面處理好,然后再更新給我們綁定的子Component的State,這樣就能起到初始化某個模塊的作用;至於刷新,下拉等后續操作,就讓Component內部自己去處理了
廣播
廣播在復雜的業務場景,能夠起到非常巨大的作用,能非常輕松使用跨頁面交互,跨Component交互!
fish_redux中是帶有廣播的通信方式,使用的方式很簡單,這本是effect層,ctx參數自帶的一個api,這里介紹一下
使用
說明:請注意廣播可以通知任何頁面的枚舉方法,你可以單獨寫一個枚舉事件,也可以不寫,直接使用某個頁面的枚舉事件,是完全可以
- action
- 廣播事件單獨寫了一個action文件,僅方便演示,也可以不單獨新建一個廣播枚舉Action
enum BroadcastAction { toNotify }
class BroadcastActionCreator {
///廣播通知
static Action toNotify(String msg) {
return Action(BroadcastAction.toNotify, payload: msg);
}
}
- 發送廣播
- 這是頁面跳轉的方法,就在此處寫了,如果想看詳細代碼的話,可以去demo地址里面看下
void _backFirst(Action action, Context<SecondState> ctx) {
//廣播通信
ctx.broadcast(BroadcastActionCreator.toNotify("頁面二發送廣播通知"));
}
- 接受廣播
Effect<FirstState> buildEffect() {
return combineEffects(<Object, Effect<FirstState>>{
//接受發送的廣播消息
BroadcastAction.toNotify: _receiveNotify,
});
}
void _receiveNotify(Action action, Context<FirstState> ctx) async {
///接受廣播
print("跳轉一頁面:${action.payload}");
}
說明
廣播的使用還是挺簡單的,基本和dispatch的使用是一致的,dispatch是模塊的,而broadcast是處於Page或Component都能進行通信交互,很多情況下,我們在一個頁面進行了操作,其他頁面也需要同步做一些處理,使用廣播就很簡單了
注意: 廣播發送和接受是一對多的關系,一處發送,可以在多處接受;和dispatch發送事件,如果在effect里面接受,在reducer就無法接受的情況是不一樣的(被攔截了)
開發小技巧
弱化reducer
無限弱化了reducer層作用
- 在日常使用fish_redux和flutter_bloc后,實際能深刻體會reducer層實際上只是相當於bloc中yield
或emit關鍵字的作用,職能完全可以弱化為,僅僅作為狀態刷新;這樣可以大大簡化開發流程,只需要關注
view -> action -> effect (reducer:使用統一的刷新事件) - 下面范例代碼,處理數據的操作直接在effect層處理,如需要更改數據,直接對ctx.state進行操作,涉及刷新頁面的操作,統一調用onRefresh事件;對於一個頁面有幾十個表單的情況,這種操作,能大大提升你的開發速度和體驗,親身體驗,大家可以嘗試下
Reducer<TestState> buildReducer() {
return asReducer(
<Object, Reducer<TestState>>{
TestAction.onRefresh: _onRefresh,
},
);
}
TestState _onRefresh(TreeState state, Action action) {
return state.clone();
}
- 具體可以查看 玩android 項目代碼;花了一些時間,把玩android項目代碼所有模塊全部重構了,肝痛
widget組合式開發
說明
這種開發形式,可以說是個慣例,在android里面是封裝一個個View,View里有對應的一套,邏輯自洽的功能,然后在主xm里面組合這些View;這種思想完全可以引申到Flutter里,而且,開發體驗更上幾百層樓,讓你的widget組合可以更加靈活百變,百變星君
-
view模塊中,頁面使用widget組合的方式去構造的,只傳入必要的數據源和保留一些點擊回調
-
為什么用widget組合方式構造頁面?
- 非常復雜的界面,必須將頁面分成一個個小模塊,然后再將其組合, 每個小模塊Widget內部應當對自身的的職能,能邏輯自洽的去處理;這種組合的方式呈現的代碼,會非常的層次分明,不會讓你的代碼寫着寫着,突然就變成shit
-
組合widget關鍵點
- 一般來說,我們並不關注widget內部頁面的實現,只需要關心的是widget需要的數據源, 以及widget對交互的反饋;例如:我點擊widget后,widget回調事件,並傳達一些數據給我;至於內部怎么實現, 外部並不關心,請勿將dispatch傳遞到封裝的widget內部,這會使我們關注的事件被封裝在內部
-
具體請查看 玩android 項目代碼
最后
Demo地址
- 這片文章,說實話,花了不少精力去寫的,也花了不少時間構思;主要是例子,必須要自己重寫下,反復思考例子是否合理等等,頭皮微涼。
- 代碼地址:代碼demo地址
- fish_redux版-玩Android:fish_redux版-玩android
- 大家如果覺得有收獲,就給我點個贊吧!你的點贊,是我碼字的最大動力!
系列文章
- 解決方案
- 改善嵌套問題:Flutter 改善套娃地獄問題(仿喜馬拉雅PC頁面舉例)
- Dialog解決方案,牆裂推薦:一種更優雅的Flutter Dialog解決方案
- 狀態管理