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

說句內心話,這篇文章,來來回回修改了不少次,若是認真看完這篇文章,還不會寫fish_redux,請在評論裏噴我。react

前言

來學學難搞的fish_redux框架吧,這個框架,官方的文檔真是一言難盡,比flutter_bloc官網的文檔真是遜色太多了,可是一旦知道怎麼寫,頁面堆起來也是很是爽呀,結構分明,邏輯也會錯落有致。android

其實在當時搞懂這個框架的時候,就一直想寫一篇文章記錄下,可是由於忙(lan),致使一直沒寫,如今以爲仍是必須把使用的過程記錄下,畢竟剛上手這個框架是個蛋痛的過程,必需要把這個過程作個記錄。ios

這不只僅是記錄的文章,文中所給出的示例,也是我從新構思去寫的,過程也是力求闡述清楚且詳細。git

img

幾個問題點

  • 頁面切換的轉場動畫
  • 頁面怎麼更新數據
  • fish_redux各個模塊之間,怎麼傳遞數據
  • 頁面跳轉傳值,及其接受下個頁面回傳的值
  • 怎麼配合ListView使用
  • ListView怎麼使用adapter,數據怎麼和item綁定
  • 怎麼將Page當作widget使用(BottomNavigationBar,NavigationRail等等導航欄控件會使用到)github

    • 這個直接使用:XxxPage.buildPage(null) 便可

若是你在使用fish_redux的過程當中遇到過上述的問題,那就來看看這篇文章吧!這裏,會解答上面全部的問題點!web

準備

引入

fish_redux相關地址數據庫

我用的是0.3.X的版本,算是第三版,相對於前幾版,改動較大json

  • 引入fish_redux插件,想用最新版插件,可進入pub地址裏面查看
fish_redux: ^0.3.4
#演示列表須要用到的庫
dio: ^3.0.9    #網絡請求框架
json_annotation: ^2.4.0 #json序列化和反序列化用的

開發插件

  • 此處咱們須要安裝代碼生成插件,能夠幫咱們生成大量文件和模板代碼
  • 在Android Studio裏面搜索」fish「就能搜出插件了,插件名叫:FishReduxTemplate

    image-20200808181112391

  • BakerJQ編寫:Android Studio的Fish Redux模板
  • huangjianke編寫:VSCode的Fish Redux模板

建立

  • 這裏我在新建的count文件夾上,選擇新建文件,選擇:New ---> FishReduxTemplate

image-20200808181242775

  • 此處選擇:Page,底下的「Select Fils」所有選擇,這是標準的redux文件結構;這邊命名建議使用大駝峯:Countredux

    • Component:這個通常是可複用的相關的組件;列表的item,也能夠選擇這個
    • Adapter:這裏有三個Adapter,均可以不用了;fish_redux第三版推出了功能更強大的adapter,更加靈活的綁定方式

image-20200808181325258

  • 建立成功後,記得在建立的文件夾上右擊,選擇:Reload From Disk;把建立的文件刷新出來

image-20200808181410600

  • 建立成功的文件結構api

    • page:總頁面,註冊effect,reducer,component,adapter的功能,相關的配置都在此頁面操做
    • state:這地方就是咱們存放子模塊變量的地方;初始化變量和接受上個頁面參數,也在此處,是個很重要的模塊
    • view:主要是咱們寫頁面的模塊
    • action:這是一個很是重要的模塊,全部的事件都在此處定義和中轉
    • effect:相關的業務邏輯,網絡請求等等的「反作用」操做,均可以寫在該模塊
    • reducer:該模塊主要是用來更新數據的,也能夠寫一些簡單的邏輯或者和數據有關的邏輯操做

image-20200808181550186

  • OK,至此就把全部的準備工做搞定了,下面能夠開搞代碼了

img

開發流程

redux流程

  • 下圖是阮一峯老師博客上放的redux流程圖

img

fish_redux流程

  • 在寫代碼前,先看寫下流程圖,這圖是憑着本身的理解畫的

    • 能夠發現,事件的傳遞,都是經過dispatch這個方法,並且action這層很明顯是很是關鍵的一層,事件的傳遞,都是在該層定義和中轉的
    • 這圖在語雀上調了半天,就在上面加了個本身的github水印地址

fish_redux流程

  • 經過倆個流程圖對比,其中仍是有一些差異的

    • 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

  • 這個例子演示,view中點擊此操做,而後更新頁面數據;下述的流程,在effect中把數據處理好,經過action中轉傳遞給reducer更新數據

    • view ---> action ---> effect ---> reducer(更新數據)
  • 注意:該流程將展現,怎麼將數據在各流程中互相傳遞

標準模式

  • main

    • 這地方須要注意,cupertino,material這類系統包和fish_redux裏包含的「Page」類名重複了,須要在這類系統包上使用hide,隱藏系統包裏的Page類
    • 關於頁面的切換風格,能夠在MaterialApp中的onGenerateRoute方法中,使用相應頁面切換風格,這邊使用ios的頁面切換風格:cupertino
///須要使用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

    • 定義咱們在頁面展現的一些變量,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層中更新數據
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中,這樣才能保證一個清晰的解耦結構,保證處理數據和更新數據過程分離

img

頁面跳轉

效果圖

fish_redux中jump

  • 從效果圖,很容易看到,倆個頁面相互傳值

    • 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) {
      //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方法是初始化變量和接受頁面傳值的,這邊咱們給他賦個初始值
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到了!

img

列表文章

  • 理解了上面倆個案例,相信你可使用fish_redux實現一部分頁面了;可是,咱們堆頁面的過程當中,能體會列表模塊是很是重要的一部分,如今就來學學,在fish_redux中怎麼使用ListView吧!

    • 廢話少說,上號!

00685430

列表展現-網絡請求

效果圖

fish_redux中list

  • 效果圖對於列表的滾動,作了倆個操做:一個是拖拽列表;另外一個是滾動鼠標的滾輪。flutter對鼠標觸發的相關事件也支持的愈來愈好了!

    • 這邊咱們使用的是玩Android的api,這個api有個坑的地方,沒設置開啓跨域,因此運行在web上,這個api使用會報錯,我在玩Android的github上提了issue,哎,也不知道做者啥時候解決,,,
  • 這地方只能曲線救國,關閉瀏覽器跨域限制,設置看這裏:https://www.jianshu.com/p/56b...
  • 若是運行在虛擬機上,就徹底不會出現這個問題!

準備

  • 先看下文件結構

image-20200810225418771

  • 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) {
      //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

item模塊

按照流程走

  • 根據接口返回json,建立相應的bean ---> 建立item模塊 ---> 編寫state ---> 編寫view界面

準備工做

  • 建立bean實體

  • 建立item模塊

    • 這邊咱們實現一個簡單的列表,item僅僅作展現功能;不作點擊,更新ui等操做,因此這邊咱們就不須要建立:effect,reducer,action文件;只選擇:state和view就好了
    • 建立item,這裏選擇component

image-20200810225523628

文件結構

image-20200812141416796

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提供的數據造成的佈局,沒有什麼要特別注意的地方
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刷新

效果圖

list_editjump

  • 此次來演示列表的單item更新,沒有網絡請求的操做,因此代碼邏輯就至關簡單了

結構

  • 來看看代碼結構

image-20200813171905618

  • 這地方很明顯得發現,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建立及其綁定;只能多寫寫了,熟能生巧!
  • 列表模塊大功告成,之後就能愉快的寫列表了!

img

全局模式

效果圖

fish_redux_switch

  • 理解了上面的是三個例子,相信大部分頁面,對於你來講都不在話下了;如今咱們再來看個例子,官方提供的全局主題功能,固然,這不只僅是全局主題,全局字體樣式,字體大小等等,都是能夠全局管理,固然了,寫app以前要作好規劃

開搞

store模塊

  • 文件結構

    • 這地方須要新建一個文件夾,新建四個文件:action,reducer,state,store

image-20200812162317741

  • 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) {
      //ios頁面切換風格
      return CupertinoPageRoute(builder: (BuildContext context) {
        return routes.buildPage(settings.name, settings.arguments);
      });
    },
  );
}

子模塊使用

  • 這裏就用計數器模塊的來舉例,由於僅僅只須要改動少許代碼,且只涉及state和view,因此其它模塊代碼也不重複貼出了
  • state

    • 這地方,僅僅讓CountState多實現了GlobalBaseState類,很小的改動
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

    • 這裏面僅僅改動了一行,在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) {
      //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());
    }
  }
}

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());

體驗

經過上面的優化,使用體驗提高不是一個級別,大大提高的全局模式的擴展性,咱們就算後期增長了大量的全局字段,也能夠一個個模塊慢慢改,不用一次爆肝全改完,猝死的機率又大大減小了!

img

Component使用

Component是個比較經常使用的模塊,上面使用列表的時候,就使用到了Component,此次咱們來看看,在頁面中直接使用Component,可插拔式使用!Component的使用總的來講是比較簡單了,比較關鍵的是在State中創建起鏈接。

效果圖

fish_redux中component

  • 上圖的效果是在頁面中嵌入了倆個Component,改變子Component的操做是在頁面中完成的
  • 先看下頁面結構

image-20200905183821129

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
  • 你們若是以爲有收穫,就給我點個贊吧!你的點贊,是我碼字的最大動力!

img

系列文章

相關文章
相關標籤/搜索