fish_redux使用詳解---看完就會(huì)用!

說句心里話就谜,這篇文章怪蔑,來來回回修改了很多次,如果認(rèn)真看完這篇文章丧荐,還不會(huì)寫fish_redux缆瓣,請(qǐng)?jiān)谠u(píng)論里噴我。

前言

來學(xué)學(xué)難搞的fish_redux框架吧虹统,這個(gè)框架弓坞,官方的文檔真是一言難盡,比flutter_bloc官網(wǎng)的文檔真是遜色太多了车荔,但是一旦知道怎么寫渡冻,頁面堆起來也是非常爽呀,結(jié)構(gòu)分明忧便,邏輯也會(huì)錯(cuò)落有致幕帆。

其實(shí)在當(dāng)時(shí)搞懂這個(gè)框架的時(shí)候田盈,就一直想寫一篇文章記錄下隘击,但是因?yàn)槊Γ╨an)宋下,導(dǎo)致一直沒寫,現(xiàn)在覺得還是必須把使用的過程記錄下,畢竟剛上手這個(gè)框架是個(gè)蛋痛的過程,必須要把這個(gè)過程做個(gè)記錄。

這不僅僅是記錄的文章辐董,文中所給出的示例,也是我重新構(gòu)思去寫的禀综,過程也是力求闡述清楚且詳細(xì)。

img

幾個(gè)問題點(diǎn)

  • 頁面切換的轉(zhuǎn)場(chǎng)動(dòng)畫
  • 頁面怎么更新數(shù)據(jù)
  • fish_redux各個(gè)模塊之間定枷,怎么傳遞數(shù)據(jù)
  • 頁面跳轉(zhuǎn)傳值孤澎,及其接受下個(gè)頁面回傳的值
  • 怎么配合ListView使用
  • ListView怎么使用adapter,數(shù)據(jù)怎么和item綁定
  • 怎么將Page當(dāng)做widget使用(BottomNavigationBar欠窒,NavigationRail等等導(dǎo)航欄控件會(huì)使用到)
    • 這個(gè)直接使用:XxxPage.buildPage(null) 即可

如果你在使用fish_redux的過程中遇到過上述的問題覆旭,那就來看看這篇文章吧!這里岖妄,會(huì)解答上面所有的問題點(diǎn)型将!

準(zhǔn)備

引入

fish_redux相關(guān)地址

我用的是0.3.X的版本,算是第三版荐虐,相對(duì)于前幾版七兜,改動(dòng)較大

  • 引入fish_redux插件,想用最新版插件福扬,可進(jìn)入pub地址里面查看
fish_redux: ^0.3.4
#演示列表需要用到的庫
dio: ^3.0.9    #網(wǎng)絡(luò)請(qǐng)求框架
json_annotation: ^2.4.0 #json序列化和反序列化用的

開發(fā)插件

  • 此處我們需要安裝代碼生成插件腕铸,可以幫我們生成大量文件和模板代碼

  • 在Android Studio里面搜索”fish“就能搜出插件了,插件名叫:FishReduxTemplate

    image-20200808181112391
  • BakerJQ編寫:Android Studio的Fish Redux模板铛碑。

  • huangjianke編寫:VSCode的Fish Redux模板

創(chuàng)建

  • 這里我在新建的count文件夾上狠裹,選擇新建文件,選擇:New ---> FishReduxTemplate
image-20200808181242775
  • 此處選擇:Page汽烦,底下的“Select Fils”全部選擇涛菠,這是標(biāo)準(zhǔn)的redux文件結(jié)構(gòu);這邊命名建議使用大駝峰:Count
    • Component:這個(gè)一般是可復(fù)用的相關(guān)的組件撇吞;列表的item俗冻,也可以選擇這個(gè)
    • Adapter:這里有三個(gè)Adapter,都可以不用了梢夯;fish_redux第三版推出了功能更強(qiáng)大的adapter言疗,更加靈活的綁定方式
image-20200808181325258
  • 創(chuàng)建成功后晴圾,記得在創(chuàng)建的文件夾上右擊颂砸,選擇:Reload From Disk;把創(chuàng)建的文件刷新出來
image-20200808181410600
  • 創(chuàng)建成功的文件結(jié)構(gòu)
    • page:總頁面,注冊(cè)effect人乓,reducer勤篮,component,adapter的功能色罚,相關(guān)的配置都在此頁面操作
    • state:這地方就是我們存放子模塊變量的地方碰缔;初始化變量和接受上個(gè)頁面參數(shù),也在此處戳护,是個(gè)很重要的模塊
    • view:主要是我們寫頁面的模塊
    • action:這是一個(gè)非常重要的模塊金抡,所有的事件都在此處定義和中轉(zhuǎn)
    • effect:相關(guān)的業(yè)務(wù)邏輯,網(wǎng)絡(luò)請(qǐng)求等等的“副作用”操作腌且,都可以寫在該模塊
    • reducer:該模塊主要是用來更新數(shù)據(jù)的梗肝,也可以寫一些簡(jiǎn)單的邏輯或者和數(shù)據(jù)有關(guān)的邏輯操作
image-20200808181550186
  • OK,至此就把所有的準(zhǔn)備工作搞定了铺董,下面可以開搞代碼了
img

開發(fā)流程

redux流程

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

fish_redux流程

  • 在寫代碼前巫击,先看寫下流程圖,這圖是憑著自己的理解畫的

    • 可以發(fā)現(xiàn)精续,事件的傳遞坝锰,都是通過dispatch這個(gè)方法,而且action這層很明顯是非常關(guān)鍵的一層重付,事件的傳遞顷级,都是在該層定義和中轉(zhuǎn)的
    • 這圖在語雀上調(diào)了半天,就在上面加了個(gè)自己的github水印地址
    fish_redux流程
  • 通過倆個(gè)流程圖對(duì)比堪夭,其中還是有一些差別的

    • redux里面的store是全局的愕把。fish_redux里面也有這個(gè)全局store的概念,放在子模塊里面理解store森爽,react恨豁;對(duì)應(yīng)fish_redux里的就是:state,view
    • fish_redux里面多了effect層:這層主要是處理邏輯爬迟,和相關(guān)網(wǎng)絡(luò)請(qǐng)求之類
    • reducer里面橘蜜,理論上也是可以處理一些和數(shù)據(jù)相關(guān),簡(jiǎn)單的邏輯付呕;但是復(fù)雜的计福,會(huì)產(chǎn)生相應(yīng)較大的“副作用”的業(yè)務(wù)邏輯,還是需要在effect中寫

范例說明

這邊寫幾個(gè)示例徽职,來演示fish_redux的使用

  • 計(jì)數(shù)器
    • fish_redux正常情況下的流轉(zhuǎn)過程
    • fish_redux各模塊怎么傳遞數(shù)據(jù)
  • 頁面跳轉(zhuǎn)
    • A ---> B(A跳轉(zhuǎn)到B象颖,并傳值給B頁面)
    • B ---> A(B返回到A,并返回值給A頁面)
  • 列表文章
    • 列表展示-網(wǎng)絡(luò)請(qǐng)求
    • 列表修改-單item刷新
    • 多樣式列表
    • 列表存在的問題+解決方案
  • 全局模塊
    • 全局切換主題
  • 全局模式優(yōu)化
    • 大幅度提升開發(fā)體驗(yàn)
  • Component使用
    • page中使用component
  • 廣播
  • 開發(fā)小技巧
    • 弱化reducer
    • widget組合式開發(fā)

計(jì)數(shù)器

效果圖

fish_redux中count
  • 這個(gè)例子演示姆钉,view中點(diǎn)擊此操作说订,然后更新頁面數(shù)據(jù)抄瓦;下述的流程,在effect中把數(shù)據(jù)處理好陶冷,通過action中轉(zhuǎn)傳遞給reducer更新數(shù)據(jù)
    • view ---> action ---> effect ---> reducer(更新數(shù)據(jù))
  • 注意:該流程將展示钙姊,怎么將數(shù)據(jù)在各流程中互相傳遞

標(biāo)準(zhǔn)模式

  • main
    • 這地方需要注意,cupertino埂伦,material這類系統(tǒng)包和fish_redux里包含的“Page”類名重復(fù)了煞额,需要在這類系統(tǒng)包上使用hide,隱藏系統(tǒng)包里的Page類
    • 關(guān)于頁面的切換風(fēng)格沾谜,可以在MaterialApp中的onGenerateRoute方法中膊毁,使用相應(yīng)頁面切換風(fēng)格,這邊使用通用風(fēng)格: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), //作為默認(rèn)頁面
    onGenerateRoute: (RouteSettings settings) {
      return MaterialPageRoute(
        builder: (BuildContext context) {
          return routes.buildPage(settings.name, settings.arguments);
        },
        settings: settings,
      );
    },
  );
}
  • state
    • 定義我們?cè)陧撁嬲故镜囊恍┳兞炕埽琲nitState中可以初始化變量媚媒;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里面有三個(gè)參數(shù)
    • state:這個(gè)就是我們的數(shù)據(jù)層涩僻,頁面需要的變量都寫在state層
    • dispatch:類似調(diào)度器缭召,調(diào)用action層中的方法,從而去回調(diào)effect逆日,reducer層的方法
    • viewService:這個(gè)參數(shù)嵌巷,我們可以使用其中的方法:buildComponent("組件名"),調(diào)用我們封裝的相關(guān)組件
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中的變量室抽,控住數(shù)據(jù)的變換
          Text(state.count.toString()),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () {
        ///點(diǎn)擊事件搪哪,調(diào)用action 計(jì)數(shù)自增方法
        dispatch(CountActionCreator.countIncrease());
      },
      child: Icon(Icons.add),
    ),
  );
}
  • action
    • 該層是非常重要的模塊,頁面所有的行為都可以在本層直觀的看到
    • XxxxAction中的枚舉字段是必須的坪圾,一個(gè)事件對(duì)應(yīng)有一個(gè)枚舉字段晓折,枚舉字段是:effect,reducer層標(biāo)識(shí)的入口
    • XxxxActionCreator類中的方法是中轉(zhuǎn)方法兽泄,方法中可以傳參數(shù)漓概,參數(shù)類型可任意;方法中的參數(shù)放在Action類中的payload字段中病梢,然后在effect胃珍,reducer中的action參數(shù)中拿到payload值去處理就行了
    • 這地方需要注意下,默認(rèn)生成的模板代碼蜓陌,return的Action類加了const修飾觅彰,如果使用Action的payload字段賦值并攜帶數(shù)據(jù),是會(huì)報(bào)錯(cuò)的钮热;所以這里如果需要攜帶參數(shù)填抬,請(qǐng)去掉const修飾關(guān)鍵字
enum CountAction { increase, updateCount }

class CountActionCreator {
  ///去effect層去處理自增數(shù)據(jù)
  static Action countIncrease() {
    return Action(CountAction.increase);
  }
  ///去reducer層更新數(shù)據(jù),傳參可以放在Action類中的payload字段中隧期,payload是dynamic類型飒责,可傳任何類型
  static Action updateCount(int count) {
    return Action(CountAction.updateCount, payload: count);
  }
}
  • effect
    • 如果在調(diào)用action里面的XxxxActionCreator類中的方法蛀骇,相應(yīng)的枚舉字段,會(huì)在combineEffects中被調(diào)用读拆,在這里,我們就能寫相應(yīng)的方法處理邏輯鸵闪,方法中帶倆個(gè)參數(shù):action檐晕,ctx
      • action:該對(duì)象中,我們可以拿到payload字段里面蚌讼,在action里面保存的值
      • ctx:該對(duì)象中辟灰,可以拿到state的參數(shù),還可以通過ctx調(diào)用dispatch方法篡石,調(diào)用action中的方法芥喇,在這里調(diào)用dispatch方法,一般是把處理好的數(shù)據(jù)凰萨,通過action中轉(zhuǎn)到reducer層中更新數(shù)據(jù)
Effect<CountState> buildEffect() {
  return combineEffects(<Object, Effect<CountState>>{
    CountAction.increase: _onIncrease,
  });
}
///自增數(shù)
void _onIncrease(Action action, Context<CountState> ctx) {
  ///處理自增數(shù)邏輯
  int count = ctx.state.count + 1;
  ctx.dispatch(CountActionCreator.updateCount(count));
}
  • reducer
    • 該層是更新數(shù)據(jù)的继控,action中調(diào)用的XxxxActionCreator類中的方法,相應(yīng)的枚舉字段胖眷,會(huì)在asReducer方法中回調(diào)武通,這里就可以寫個(gè)方法,克隆state數(shù)據(jù)進(jìn)行一些處理珊搀,這里面有倆個(gè)參數(shù):state冶忱,action
    • state參數(shù)經(jīng)常使用的是clone方法,clone一個(gè)新的state對(duì)象境析;action參數(shù)基本就是拿到其中的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模塊不需要改動(dòng)劳淆,這邊就不貼代碼了

優(yōu)化

  • 從上面的例子看到链沼,如此簡(jiǎn)單數(shù)據(jù)變換,僅僅是個(gè)state中一個(gè)參數(shù)自增的過程沛鸵,effect層就顯得有些多余忆植;所以,把流程簡(jiǎn)化成下面

    • view ---> action ---> reducer
  • 注意:這邊把effect層刪掉谒臼,該層可以舍棄了朝刊;然后對(duì)view,action蜈缤,reducer層代碼進(jìn)行一些小改動(dòng)

搞起來

  • view
    • 這邊僅僅把點(diǎn)擊事件的方法拾氓,微微改了下: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: () {
        ///點(diǎn)擊事件,調(diào)用action 計(jì)數(shù)自增方法
        dispatch(CountActionCreator.updateCount());
      },
      child: Icon(Icons.add),
    ),
  );
}
  • action
    • 這里只使用一個(gè)枚舉字段底哥,和一個(gè)方法就行了咙鞍,也不用傳啥參數(shù)了
enum CountAction { updateCount }

class CountActionCreator {
  ///去reducer層更新數(shù)據(jù)房官,傳參可以放在Action類中的payload字段中,payload是dynamic類型续滋,可傳任何類型
  static Action updateCount() {
    return Action(CountAction.updateCount);
  }
}
  • reducer
    • 這里直接在:_updateCount方法中處理下簡(jiǎn)單的自增邏輯
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;
}

搞定

  • 可以看見優(yōu)化了后翰守,代碼量減少了很多,對(duì)待不同的業(yè)務(wù)場(chǎng)景疲酌,可以靈活的變動(dòng)蜡峰,使用框架,但不要拘泥框架朗恳;但是如果有網(wǎng)絡(luò)請(qǐng)求湿颅,很復(fù)雜的業(yè)務(wù)邏輯,就萬萬不能寫在reducer里面了粥诫,一定要寫在effect中油航,這樣才能保證一個(gè)清晰的解耦結(jié)構(gòu),保證處理數(shù)據(jù)和更新數(shù)據(jù)過程分離
img

頁面跳轉(zhuǎn)

效果圖

fish_redux中jump
  • 從效果圖怀浆,很容易看到谊囚,倆個(gè)頁面相互傳值
    • FirstPage ---> SecondPage(FirstPage跳轉(zhuǎn)到SecondPage,并傳值給SecondPage頁面)
    • SecondPage ---> FirstPage(SecondPage返回到FirstPage执赡,并返回值給FirstPage頁面)

實(shí)現(xiàn)

  • 從上面效果圖上看秒啦,很明顯,這邊需要實(shí)現(xiàn)倆個(gè)頁面搀玖,先看看main頁面的改動(dòng)
  • main
    • 這里只增加了倆個(gè)頁面:FirstPage和SecondPage余境;并將主頁面入口換成了:FirstPage
Widget createApp() {
  ///定義路由
  final AbstractRoutes routes = PageRoutes(
    pages: <String, Page<Object, dynamic>>{
      ///計(jì)數(shù)器模塊演示
      "CountPage": CountPage(),
      ///頁面?zhèn)髦堤D(zhuǎn)模塊演示
      "FirstPage": FirstPage(),
      "SecondPage": SecondPage(),
    },
  );

  return MaterialApp(
    title: 'FishRedux',
    home: routes.buildPage("FirstPage", null), //作為默認(rèn)頁面
    onGenerateRoute: (RouteSettings settings) {
      return MaterialPageRoute(
        builder: (BuildContext context) {
          return routes.buildPage(settings.name, settings.arguments);
        },
        settings: settings,
      );
    },
  );
}

FirstPage

  • 先來看看該頁面的一個(gè)流程

    • view ---> action ---> effect(跳轉(zhuǎn)到SecondPage頁面)
    • effect(拿到SecondPage返回的數(shù)據(jù)) ---> action ---> reducer(更新頁面數(shù)據(jù))
  • state

    • 先寫state文件,這邊需要定義倆個(gè)變量來
      • fixedMsg:這個(gè)是傳給下個(gè)頁面的值
      • msg:在頁面上展示傳值得變量
    • initState方法是初始化變量和接受頁面?zhèn)髦档墓嘧纾@邊我們給他賦個(gè)初始值
class FirstState implements Cloneable<FirstState> {
  ///傳遞給下個(gè)頁面的值
  static const String fixedMsg = "\n我是FirstPage頁面?zhèn)鬟f過來的數(shù)據(jù):FirstValue";
  ///展示傳遞過來的值
  String msg;

  @override
  FirstState clone() {
    return FirstState()..msg = msg;
  }
}

FirstState initState(Map<String, dynamic> args) {
  return FirstState()..msg = "\n暫無";
}
  • view
    • 該頁面邏輯相當(dāng)簡(jiǎn)單芳来,主要的僅僅是在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('下方數(shù)據(jù)是SecondPage頁面?zhèn)鬟f過來的:'),
          Text(state.msg),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () {
        ///跳轉(zhuǎn)到Second頁面
        dispatch(FirstActionCreator.toSecond());
      },
      child: Icon(Icons.arrow_forward),
    ),
  );
}
  • action:這里需要定義倆個(gè)枚舉事件
    • toSecond:跳轉(zhuǎn)到SecondPage頁面
    • updateMsg:拿到SecondPage頁面返回的數(shù)據(jù),然后更新頁面數(shù)據(jù)
enum FirstAction { toSecond , updateMsg}

class FirstActionCreator {
  ///跳轉(zhuǎn)到第二個(gè)頁面
  static Action toSecond() {
    return const Action(FirstAction.toSecond);
  }
  ///拿到第二個(gè)頁面返回的數(shù)據(jù),執(zhí)行更新數(shù)據(jù)操作
  static Action updateMsg(String msg) {
    return Action(FirstAction.updateMsg, payload: msg);
  }
}
  • effect
    • 此處需要注意:fish_redux 框架中的Action類和系統(tǒng)包中的重名了猜拾,需要把系統(tǒng)包中Action類隱藏掉
    • 傳值直接用pushNamed方法即可即舌,攜帶的參數(shù)可以寫在arguments字段中;pushNamed返回值是Future類型挎袜,如果想獲取他的返回值顽聂,跳轉(zhuǎn)方法就需要寫成異步的,等待從SecondPage頁面獲取返回的值盯仪,
/// 使用hide方法紊搪,隱藏系統(tǒng)包里面的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{
  ///頁面之間傳值;這地方必須寫個(gè)異步方法全景,等待上個(gè)頁面回傳過來的值耀石;as關(guān)鍵字是類型轉(zhuǎn)換
  var result = await Navigator.of(ctx.context).pushNamed("SecondPage", arguments: {"firstValue": FirstState.fixedMsg});
  ///獲取到數(shù)據(jù),更新頁面上的數(shù)據(jù)
  ctx.dispatch(FirstActionCreator.updateMsg( (result as Map)["secondValue"]) );
}
  • reducer
    • 這里就是從action里面獲取傳遞的值爸黄,賦值給克隆對(duì)象中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

  • 這個(gè)頁面比較簡(jiǎn)單滞伟,后續(xù)不涉及到頁面數(shù)據(jù)更新揭鳞,所以reducer模塊可以不寫,看看該頁面的流程
    • view ---> action ---> effect(pop當(dāng)前頁面梆奈,并攜帶值返回)
  • state
    • 該模塊的變量和FirstPage類型野崇,就不闡述了
    • initState里面通過args變量獲取上個(gè)頁面?zhèn)鬟f的值,上個(gè)頁面?zhèn)髦敌枰獋鬟fMap類型亩钟,這邊通過key獲取相應(yīng)的value
class SecondState implements Cloneable<SecondState> {
  ///傳遞給下個(gè)頁面的值
  static const String fixedMsg = "\n我是SecondPage頁面?zhèn)鬟f過來的數(shù)據(jù):SecondValue";
  ///展示傳遞過來的值
  String msg;

  @override
  SecondState clone() {
    return SecondState()..msg = msg;
  }
}

SecondState initState(Map<String, dynamic> args) {
  ///獲取上個(gè)頁面?zhèn)鬟f過來的數(shù)據(jù)
  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:表示執(zhí)行頁面返回    false:表示不執(zhí)行返回頁面操作乓梨,這里因?yàn)橐獋髦担越庸芊祷夭僮?      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('下方數(shù)據(jù)是FirstPage頁面?zhèn)鬟f過來的:'),
          Text(state.msg),
        ],
      ),
    ),
  );
}
  • action
enum SecondAction { backFirst }

class SecondActionCreator {
  ///返回到第一個(gè)頁面径荔,然后從棧中移除自身,同時(shí)傳回去一些數(shù)據(jù)
  static Action backFirst() {
    return Action(SecondAction.backFirst);
  }
}
  • effect
    • 此處同樣需要隱藏系統(tǒng)包中的Action類
    • 這邊直接在pop方法的第二個(gè)參數(shù)脆霎,寫入返回?cái)?shù)據(jù)
///隱藏系統(tǒng)包中的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當(dāng)前頁面总处,并且返回相應(yīng)的數(shù)據(jù)
  Navigator.pop(ctx.context, {"secondValue": SecondState.fixedMsg});
}

搞定

  • 因?yàn)閜age模塊不需要改動(dòng),所以就沒必要將page模塊代碼附上了哈
  • OK睛蛛,到這里鹦马,咱們也已經(jīng)把倆個(gè)頁面相互傳值的方式get到了!
img

列表文章

  • 理解了上面?zhèn)z個(gè)案例忆肾,相信你可以使用fish_redux實(shí)現(xiàn)一部分頁面了荸频;但是,我們堆頁面的過程中客冈,能體會(huì)列表模塊是非常重要的一部分旭从,現(xiàn)在就來學(xué)學(xué),在fish_redux中怎么使用ListView吧场仲!

    • 廢話少說和悦,上號(hào)!
00685430

列表展示-網(wǎng)絡(luò)請(qǐng)求

效果圖

fish_redux中l(wèi)ist
  • 效果圖對(duì)于列表的滾動(dòng)渠缕,做了倆個(gè)操作:一個(gè)是拖拽列表鸽素;另一個(gè)是滾動(dòng)鼠標(biāo)的滾輪。flutter對(duì)鼠標(biāo)觸發(fā)的相關(guān)事件也支持的越來越好了亦鳞!
    • 這邊我們使用的是玩Android的api馍忽,這個(gè)api有個(gè)坑的地方,沒設(shè)置開啟跨域燕差,所以運(yùn)行在web上遭笋,這個(gè)api使用會(huì)報(bào)錯(cuò),我在玩Android的github上提了issue徒探,哎坐梯,也不知道作者啥時(shí)候解決,刹帕,吵血,
  • 這地方只能曲線救國谎替,關(guān)閉瀏覽器跨域限制,設(shè)置看這里:http://www.reibang.com/p/56b1e01e6b6a
  • 如果運(yùn)行在虛擬機(jī)上蹋辅,就完全不會(huì)出現(xiàn)這個(gè)問題钱贯!

準(zhǔn)備

  • 先看下文件結(jié)構(gòu)
image-20200810225418771
  • main
    • 這邊改動(dòng)非常小殿托,只在路由里,新增了:GuidePage篮赢,ListPage;同時(shí)將home字段中的默認(rèn)頁面贬派,改成了:GuidePage頁面;導(dǎo)航頁面代碼就不貼在文章里了澎媒,下面貼下該頁面鏈接
    • ListPage才是重點(diǎn)搞乏,下文會(huì)詳細(xì)說明
void main() {
  runApp(createApp());
}

Widget createApp() {
  ///定義路由
  final AbstractRoutes routes = PageRoutes(
    pages: <String, Page<Object, dynamic>>{
      ///導(dǎo)航頁面
      "GuidePage": GuidePage(),
      ///計(jì)數(shù)器模塊演示
      "CountPage": CountPage(),
      ///頁面?zhèn)髦堤D(zhuǎn)模塊演示
      "FirstPage": FirstPage(),
      "SecondPage": SecondPage(),
      ///列表模塊演示
      "ListPage": ListPage(),
    },
  );

  return MaterialApp(
    title: 'FishRedux',
    home: routes.buildPage("GuidePage", null), //作為默認(rèn)頁面
    onGenerateRoute: (RouteSettings settings) {
      return MaterialPageRoute(
        builder: (BuildContext context) {
          return routes.buildPage(settings.name, settings.arguments);
        },
        settings: settings,
      );
    },
  );
}

流程

  • Adapter實(shí)現(xiàn)的流程
    • 創(chuàng)建item(Component) ---> 創(chuàng)建adapter文件 ---> state集成相應(yīng)的Source ---> page里面綁定adapter
  • 通過以上四步,就能在fish_redux使用相應(yīng)列表里面的adapter了戒努,過程有點(diǎn)麻煩请敦,但是熟能生巧,多用用就能很快搭建一個(gè)復(fù)雜的列表了
  • 總流程:初始化列表模塊 ---> item模塊 ---> 列表模塊邏輯完善
    • 初始化列表模塊
      • 這個(gè)就是正常的創(chuàng)建fish_redux模板代碼和文件
    • item模塊
      • 根據(jù)接口返回json,創(chuàng)建相應(yīng)的bean ---> 創(chuàng)建item模塊 ---> 編寫state ---> 編寫view界面
    • 列表模塊邏輯完善:倆地方分倆步(adapter創(chuàng)建及其綁定,正常page頁面編輯)
      • 創(chuàng)建adapter文件 ---> state調(diào)整 ---> page中綁定adapter
      • view模塊編寫 ---> action添加更新數(shù)據(jù)事件 ---> effect初始化時(shí)獲取數(shù)據(jù)并處理 ---> reducer更新數(shù)據(jù)
  • 整體流程確實(shí)有些多痪寻,但是咱們按照整體三步流程流程走腥椒,保證思路清晰就行了

初始化列表模塊

  • 此處新建個(gè)文件夾,在文件夾上新建fis_redux文件就行了敌蚜;這地方桥滨,我們選擇page,整體的五個(gè)文件:action弛车,effect齐媒,reducer,state纷跛,view喻括;全部都要用到,所以默認(rèn)全選贫奠,填入Module的名字唬血,點(diǎn)擊OK
image-20200812140314075

item模塊

按照流程走

  • 根據(jù)接口返回json,創(chuàng)建相應(yīng)的bean ---> 創(chuàng)建item模塊 ---> 編寫state ---> 編寫view界面

準(zhǔn)備工作

文件結(jié)構(gòu)

image-20200812141416796

OK,bean文件搞定了噪舀,再來看看魁淳,item文件中的文件,這里component文件不需要改動(dòng)与倡,所以這地方界逛,我們只需要看:state.dart,view.dart

  • state
    • 這地方還是常規(guī)的寫法纺座,因?yàn)閖son生成的bean里面息拜,能用到的所有數(shù)據(jù),都在Datas類里面净响,所以少欺,這地方建一個(gè)Datas類的變量即可
    • 因?yàn)椋瑳]用到reducer馋贤,實(shí)際上clone實(shí)現(xiàn)方法都能刪掉赞别,防止后面可能需要clone對(duì)象,暫且留著
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布局稍稍有點(diǎn)麻煩配乓,整體上采用的是:水平布局(Row)仿滔,分左右倆大塊
      • 左邊:?jiǎn)渭兊膱D片展示
      • 右邊:采用了縱向布局(Column),結(jié)合Expanded形成比例布局犹芹,分別展示三塊東西:標(biāo)題崎页,內(nèi)容,作者和時(shí)間
    • OK腰埂,這邊view只是簡(jiǎn)單用到了state提供的數(shù)據(jù)形成的布局飒焦,沒有什么要特別注意的地方
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>[
        //標(biāo)題
        Expanded(
          flex: 2,
          child: Container(
            alignment: Alignment.centerLeft,
            child: Text(
              state.itemDetail.title,
              style: TextStyle(fontSize: 16),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ),
        //內(nèi)容
        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),
                  )
                ],
              ),
              //時(shí)間
              Row(children: <Widget>[
                Text("時(shí)間:", style: TextStyle(fontSize: 12)),
                Expanded(
                  child: Text(state.itemDetail.niceDate,
                      style: TextStyle(color: Colors.blue, fontSize: 12),
                      overflow: TextOverflow.ellipsis),
                )
              ])
            ],
          ),
        ),
      ],
    ),
  ));
}

item模塊,就這樣寫完了屿笼,不需要改動(dòng)什么了牺荠,接下來看看List模塊

列表模塊邏輯完善

首先最重要的,我們需要將adapter建立起來刁卜,并和page綁定

  • 創(chuàng)建adapter文件 ---> state調(diào)整 ---> page中綁定adapter

adapter創(chuàng)建及其綁定

  • 創(chuàng)建adapter
    • 首先需要?jiǎng)?chuàng)建adapter文件志电,然后寫入下面代碼:這地方需要繼承SourceFlowAdapter適配器,里面的泛型需要填入ListState蛔趴,ListState這地方會(huì)報(bào)錯(cuò)挑辆,因?yàn)槲覀兊腖istState沒有繼承MutableSource,下面state的調(diào)整就是對(duì)這個(gè)的處理
    • ListItemAdapter的構(gòu)造函數(shù)就是通用的寫法了,在super里面寫入我們上面寫好item樣式鱼蝉,這是個(gè)pool應(yīng)該可以理解為樣式池洒嗤,這個(gè)key最好都提出來,因?yàn)樵趕tate模塊還需要用到魁亦,可以定義多個(gè)不同的item渔隶,很容易做成多樣式item的列表;目前洁奈,我們這邊只需要用一個(gè)间唉,填入: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調(diào)整
    • state文件中的代碼需要做一些調(diào)整,需要繼承相應(yīng)的類利术,和adapter建立起關(guān)聯(lián)
    • ListState需要繼承MutableSource呈野;還必須定義一個(gè)泛型是item的ItemState類型的List,這倆個(gè)是必須的印叁;然后實(shí)現(xiàn)相應(yīng)的抽象方法就行了
    • 這里只要向items里寫入ItemState的數(shù)據(jù)被冒,列表就會(huì)更新了
class ListState extends MutableSource implements Cloneable<ListState> {
  ///這地方一定要注意,List里面的泛型,需要定義為ItemState
  ///怎么更新列表數(shù)據(jù),只需要更新這個(gè)items里面的數(shù)據(jù),列表數(shù)據(jù)就會(huì)相應(yīng)更新
  ///使用多樣式,請(qǐng)寫出  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添加更新數(shù)據(jù)事件 ---> effect初始化時(shí)獲取數(shù)據(jù)并處理 ---> reducer更新數(shù)據(jù)

  • view

    • 這里面的列表使用就相當(dāng)簡(jiǎn)單了,填入itemBuilder和itemCount參數(shù)就行了轮蜕,這里就需要用viewService參數(shù)了哈
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
    • 只需要寫個(gè)更新items的事件就ok了
enum ListAction { updateItem }

class ListActionCreator {
  static Action updateItem(var list) {
    return Action(ListAction.updateItem, payload: list);
  }
}
  • effect
    • Lifecycle.initState是進(jìn)入頁面初始化的回調(diào)昨悼,這邊可以直接用這個(gè)狀態(tài)回調(diào),來請(qǐng)求接口獲取相應(yīng)的數(shù)據(jù)跃洛,然后去更新列表
    • 這地方有個(gè)坑率触,dio必須結(jié)合json序列號(hào)和反序列的庫一起用,不然Dio無法將數(shù)據(jù)源解析成Response類型
Effect<ListState> buildEffect() {
  return combineEffects(<Object, Effect<ListState>>{
    ///進(jìn)入頁面就執(zhí)行的初始化操作
    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;
  ///構(gòu)建符合要求的列表數(shù)據(jù)源
  List<ItemState> items = List.generate(itemDetails.length, (index) {
    return ItemState(itemDetail: itemDetails[index]);
  });
  ///通知更新列表數(shù)據(jù)源
  ctx.dispatch(ListActionCreator.updateItem(items));
}
  • reducer
    • 最后就是更新操作了哈税课,這里就是常規(guī)寫法了
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更新闲延,沒有網(wǎng)絡(luò)請(qǐng)求的操作痊剖,所以代碼邏輯就相當(dāng)簡(jiǎn)單了

結(jié)構(gòu)

  • 來看看代碼結(jié)構(gòu)
image-20200813171905618
  • 這地方很明顯得發(fā)現(xiàn)韩玩,list_edit主體文件很少,因?yàn)檫@邊直接在state里初始化了數(shù)據(jù)源陆馁,就沒有后期更新數(shù)據(jù)的操作找颓,所以就不需要:action,effect叮贩,reducer這三個(gè)文件击狮!item模塊則直接在reducer里更新數(shù)據(jù),不涉及相關(guān)復(fù)雜的邏輯益老,所以不需要:effect文件彪蓬。

列表模塊

  • 這次列表模塊是非常的簡(jiǎn)單,基本不涉及什么流程捺萌,就是最基本初始化的一個(gè)過程档冬,將state里初始化的數(shù)據(jù)在view中展示

    • state ---> view
  • state

    • 老規(guī)矩,先來看看state中的代碼
    • 這里一些新建了變量,泛型是ItemState(item的State)酷誓,items變量初始化了一組數(shù)據(jù)披坏;然后,同樣繼承了MutableSource盐数,實(shí)現(xiàn)其相關(guān)方法
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的代碼主體僅僅是個(gè)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
    • 老規(guī)矩,先來看看state里面的代碼漾峡;此處就是寫常規(guī)變量的定義涮阔,這些在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的內(nèi)部點(diǎn)擊操作,我們傳遞了一個(gè)id參數(shù)灰殴,注意這個(gè)id參數(shù)是必須的敬特,在更新item的時(shí)候來做區(qū)分用的
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的點(diǎn)擊操作:狀態(tài)變更
          onChanged: (value) => dispatch(ItemActionCreator.onChange(state.id)),
        ),
      ),
    ),
  );
}
  • action
    • 一個(gè)狀態(tài)改變的事件
enum ItemAction { onChange }

class ItemActionCreator {
  //狀態(tài)改變
  static Action onChange(int id) {
    return Action(ItemAction.onChange, payload: id);
  }
}
  • reducer
    • _onChange會(huì)回調(diào)所有ItemState,所以這地方必須用id或其它唯一標(biāo)識(shí)去界定牺陶,我們所操作的item具體是哪一個(gè)
    • _onChange方法伟阔,未操作的item返回的時(shí)候要注意,需要返回:state原對(duì)象掰伸,標(biāo)明該state對(duì)象未變動(dòng)皱炉,其item不需要刷新;不能返回state.clone()狮鸭,這樣返回的就是個(gè)全新的state對(duì)象合搅,每個(gè)item都會(huì)刷新,還會(huì)造成一個(gè)很奇怪的bug歧蕉,會(huì)造成后續(xù)點(diǎn)擊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()惯退,否則會(huì)造成后續(xù)更新失靈
  return state;
}

多樣式列表

注意:如果使用多樣式赌髓,items的列表泛型不要寫成ItemState,寫成Object就行了催跪;在下面代碼锁蠕,我們可以看到,實(shí)現(xiàn)的getItemData()方法返回的類型是Object懊蒸,所以Items的列表泛型寫成Object荣倾,是完全可以的。

  • 我們定義數(shù)據(jù)源的時(shí)候把泛型寫成Object是完全可以的骑丸,但是初始化數(shù)據(jù)的時(shí)候一定要注意舌仍,寫成對(duì)應(yīng)adapter類型里面的state
  • 假設(shè)一種情況鳖孤,在index是奇數(shù)時(shí)展示:OneComponent;在index是奇數(shù)時(shí)展示:TwoComponent抡笼;
    • getItemType:這個(gè)重寫方法里面苏揣,在index為奇偶數(shù)時(shí)分別返回:OneComponent和TwoComponent的標(biāo)識(shí)
    • 數(shù)據(jù)賦值時(shí)也一定要在index為奇偶數(shù)時(shí)賦值泛型分別為:OneState和TwoState
  • 也可以這樣優(yōu)化去做,在getItemType里面判斷當(dāng)前泛型是什么數(shù)據(jù)類型推姻,然后再返回對(duì)應(yīng)的XxxxComponent的標(biāo)識(shí)
  • 數(shù)據(jù)源的數(shù)據(jù)類型必須和getItemType返回的XxxxComponent的標(biāo)識(shí)相對(duì)應(yīng)平匈,如果數(shù)據(jù)源搞成Object類型,映射到對(duì)應(yīng)位置的item數(shù)據(jù)時(shí)藏古,會(huì)報(bào)類型不適配的錯(cuò)誤

下述代碼可做思路參考

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刷新場(chǎng)景增炭,還存在一種多item刷新的場(chǎng)景

  • 說明下,列表item是沒辦法一次刷新多個(gè)item的拧晕,只能一次刷新一個(gè)item(一個(gè)clone對(duì)應(yīng)著一次刷新)隙姿,一個(gè)事件對(duì)應(yīng)著刷新一個(gè)item;這邊是打印多個(gè)日志分析出來了
  • 解決:解決辦法是厂捞,多個(gè)事件去處理刷新操作

舉例:假設(shè)一種場(chǎng)景输玷,對(duì)于上面的item只能單選,一個(gè)item項(xiàng)被選中靡馁,其它item狀態(tài)被重置到未選狀態(tài)欲鹏,具體效果看下方效果圖

  • 效果圖
單選模式
  • 這種效果的實(shí)現(xiàn)非常簡(jiǎn)單,但是如果思路不對(duì)臭墨,會(huì)掉進(jìn)坑里出不來

  • 還原被選的狀態(tài)赔嚎,不能在同一個(gè)事件里寫,需要新寫一個(gè)清除事件

下述代碼為整體流程

  • view
Widget buildView(ItemState state, Dispatch dispatch, ViewService viewService) {
  return InkWell(
    onTap: () {},
    child: ListTile(
      title: Text(state.title),
      trailing: Checkbox(
        value: state.itemStatus,
        ///CheckBox的點(diǎn)擊操作:狀態(tài)變更
        onChanged: (value) {
          //單選模式,清除選中的item,以便做單選
          dispatch(ItemActionCreator.clear());

          //刷新選中item
          dispatch(ItemActionCreator.onChange(state.id));
        }
      ),
    ),
  );
}
  • action
enum ItemAction {
  onChange,
  clear,
}

class ItemActionCreator {
  //狀態(tài)改變
  static Action onChange(int id) {
    return Action(ItemAction.onChange, payload: id);
  }

  //清除改變的狀態(tài)
  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(),否則會(huì)造成后續(xù)更新失靈
  return state;
}

///單選模式
ItemState _clear(ItemState state, Action action) {
  if (state.itemStatus) {
    return state.clone()..itemStatus = false;
  }

  ///這地方一定要注意结缚,要返回:state损晤;不能返回:state.clone(),否則會(huì)造成后續(xù)更新失靈
  return state;
}

這個(gè)問題實(shí)際上解決起來很簡(jiǎn)單掺冠,但是如果一直在 _onChange 方法重置狀態(tài)沉馆,你會(huì)發(fā)現(xiàn)和你預(yù)期的結(jié)果一直對(duì)不上码党;完整且詳細(xì)的效果德崭,可以去看demo里面代碼

搞定

  • 呼,終于將列表這塊寫完揖盘,說實(shí)話眉厨,這個(gè)列表的使用確實(shí)有點(diǎn)麻煩;實(shí)際上兽狭,如果大家用心看了的話憾股,麻煩的地方鹿蜀,其實(shí)就是在這塊:adapter創(chuàng)建及其綁定;只能多寫寫了服球,熟能生巧茴恰!

  • 列表模塊大功告成,以后就能愉快的寫列表了斩熊!

img

全局模式

效果圖

fish_redux_switch
  • 理解了上面的是三個(gè)例子往枣,相信大部分頁面,對(duì)于你來說都不在話下了粉渠;現(xiàn)在我們?cè)賮砜磦€(gè)例子分冈,官方提供的全局主題功能,當(dāng)然霸株,這不僅僅是全局主題雕沉,全局字體樣式,字體大小等等去件,都是可以全局管理坡椒,當(dāng)然了,寫app之前要做好規(guī)劃

開搞

store模塊

  • 文件結(jié)構(gòu)
    • 這地方需要新建一個(gè)文件夾尤溜,新建四個(gè)文件:action肠牲,reducer,state靴跛,store
image-20200812162317741
  • state
    • 老規(guī)矩缀雳,先來看看state,我們這里只在抽象類里面定義了一個(gè)主題色梢睛,這個(gè)抽象類是很重要的肥印,需要做全局模式所有子模塊的state,都必須實(shí)現(xiàn)這個(gè)抽象類
abstract class GlobalBaseState{
  Color themeColor;
}

class GlobalState implements GlobalBaseState, Cloneable<GlobalState>{
  @override
  Color themeColor;

  @override
  GlobalState clone() {
    return GlobalState();
  }
}
  • action
    • 因?yàn)橹蛔銮袚Q主題色绝葡,這地方只需要定義一個(gè)事件即可
enum GlobalAction { changeThemeColor }

class GlobalActionCreator{
  static Action onChangeThemeColor(){
    return const Action(GlobalAction.changeThemeColor);
  }
}
  • reducer
    • 這里就是處理變色的一些操作深碱,這是咸魚官方demo里面代碼;這說明簡(jiǎn)單的邏輯藏畅,是可以放在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
    • 切換全局狀態(tài)的時(shí)候敷硅,就需要調(diào)用這個(gè)類了
/// 建立一個(gè)AppStore
/// 目前它的功能只有切換主題
class GlobalStore{
  static Store<GlobalState> _globalStore;
  static Store<GlobalState> get store => _globalStore ??= createStore<GlobalState>(GlobalState(), buildReducer());
}

main改動(dòng)

  • 這里面將PageRoutes里面的visitor字段使用起來,狀態(tài)更新操作代碼有點(diǎn)多愉阎,就單獨(dú)提出來了绞蹦;所以main文件里面,增加了:
    • visitor字段使用
    • 增加_updateState方法
void main() {
  runApp(createApp());
}

Widget createApp() {
  ///全局狀態(tài)更新
  _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 并將數(shù)據(jù)設(shè)置到 ui
        return newState;
      }
      return pageState;
    };
  }
  
  final AbstractRoutes routes = PageRoutes(
    ///全局狀態(tài)管理:只有特定的范圍的Page(State繼承了全局狀態(tài)),才需要建立和 AppStore 的連接關(guān)系
    visitor: (String path, Page<Object, dynamic> page) {
      if (page.isTypeof<GlobalBaseState>()) {
        ///建立AppStore驅(qū)動(dòng)PageStore的單向數(shù)據(jù)連接: 參數(shù)1 AppStore  參數(shù)2 當(dāng)AppStore.state變化時(shí),PageStore.state該如何變化
        page.connectExtraStore<GlobalState>(GlobalStore.store, _updateState());
      }
    },

    ///定義路由
    pages: <String, Page<Object, dynamic>>{
      ///導(dǎo)航頁面
      "GuidePage": GuidePage(),
      ///計(jì)數(shù)器模塊演示
      "CountPage": CountPage(),
      ///頁面?zhèn)髦堤D(zhuǎn)模塊演示
      "FirstPage": FirstPage(),
      "SecondPage": SecondPage(),
      ///列表模塊演示
      "ListPage": ListPage(),
    },
  );

  return MaterialApp(
    title: 'FishRedux',
    home: routes.buildPage("GuidePage", null), //作為默認(rèn)頁面
    onGenerateRoute: (RouteSettings settings) {
      return MaterialPageRoute(
        builder: (BuildContext context) {
          return routes.buildPage(settings.name, settings.arguments);
        },
        settings: settings,
      );
    },
  );
}

子模塊使用

  • 這里就用計(jì)數(shù)器模塊的來舉例榜旦,因?yàn)閮H僅只需要改動(dòng)少量代碼幽七,且只涉及state和view,所以其它模塊代碼也不重復(fù)貼出了
  • state
    • 這地方溅呢,僅僅讓CountState多實(shí)現(xiàn)了GlobalBaseState類澡屡,很小的改動(dòng)
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
    • 這里面僅僅改動(dòng)了一行猿挚,在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"),
      ///全局主題驶鹉,僅僅在此處改動(dòng)了一行
      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: () {
        ///點(diǎn)擊事件绩蜻,調(diào)用action 計(jì)數(shù)自增方法
        dispatch(CountActionCreator.updateCount());
      },
      child: Icon(Icons.add),
    ),
  );
}
  • 如果其他模塊也需要做主題色,也按照此處邏輯改動(dòng)即可

調(diào)用

  • 調(diào)用狀態(tài)更新就非常簡(jiǎn)單了室埋,和正常模塊更新View一樣辜羊,這里我們調(diào)用全局的就行了,一行代碼搞定词顾,在需要的地方調(diào)用就OK了
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());

搞定

  • 經(jīng)過上面的的三步八秃,我們就可以使用全局狀態(tài)了;從上面子模塊的使用肉盹,可以很明顯的感受到昔驱,全局狀態(tài),必須前期做好字段的規(guī)劃上忍,確定之后骤肛,最好不要再增加字段,不然繼承抽象類的多個(gè)模塊都會(huì)爆紅窍蓝,提示去實(shí)現(xiàn)xxx變量

全局模塊優(yōu)化

反思

在上面的全局模式里說了腋颠,使用全局模塊,前期需要規(guī)劃好字段吓笙,不然項(xiàng)目進(jìn)行到中期的時(shí)候淑玫,想添加字段,多個(gè)模塊的State會(huì)出現(xiàn)大范圍爆紅面睛,提示去實(shí)現(xiàn)你添加的字段絮蒿;項(xiàng)目開始規(guī)劃好所有的字段,顯然這需要全面的考慮好大部分場(chǎng)景叁鉴,但是人的靈感總是無限的土涝,不改代碼是不可能,這輩子都不可能幌墓。只能想辦法看能不能添加一次字段后但壮,后期添加字段,并不會(huì)引起其他模塊爆紅常侣,試了多次蜡饵,成功的使用中間實(shí)體,來解決該問題

這里優(yōu)化倆個(gè)方面

  • 使用通用的全局實(shí)體
    • 這樣后期添加字段袭祟,就不會(huì)影響其他模塊验残,這樣我們就能一個(gè)個(gè)模塊的去整改,不會(huì)出現(xiàn)整個(gè)項(xiàng)目不能運(yùn)行的情況
  • 將路由模塊和全局模塊封裝
    • 路由模塊后期頁面多了巾乳,代碼會(huì)很多您没,放在主入口,真的不好管理胆绊;全局模塊同理

因?yàn)槭褂弥虚g實(shí)體氨鹏,有一些地方會(huì)出現(xiàn)空指針問題,我都在流程里面寫清楚了压状,大家可以把優(yōu)化流程完整看一遍哈仆抵,都配置好,后面拓展使用就不會(huì)報(bào)空指針了

優(yōu)化

入口模塊

  • main:大改
    • 從下面代碼可以看到种冬,這里將路由模塊和全局模塊單獨(dú)提出來了镣丑,這地方為了方便觀看,就寫在一個(gè)文件里娱两;說明下莺匠,RouteConfig和StoreConfig這倆個(gè)類,可以放在倆個(gè)不同的文件里十兢,這樣管理路由和全局字段更新就會(huì)很方便了趣竣!
    • RouteConfig:這里將頁面標(biāo)識(shí)和頁面映射分開寫旱物,這樣我們跳轉(zhuǎn)頁面的時(shí)候遥缕,就可以直接引用RouteConfig里面的頁面標(biāo)識(shí)
    • StoreConfig:全局模塊里最重要的就是狀態(tài)的判斷,注釋寫的很清楚了宵呛,可以看看注釋哈
void main() {
  runApp(createApp());
}

Widget createApp() {
  return MaterialApp(
    title: 'FishRedux',
    home: RouteConfig.routes.buildPage(RouteConfig.guidePage, null), //作為默認(rèn)頁面
    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';
  ///導(dǎo)航頁面
  static const String guidePage = 'page/guide';

  ///計(jì)數(shù)器頁面
  static const String countPage = 'page/count';

  ///頁面?zhèn)髦堤D(zhuǎn)模塊演示
  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 {
  ///全局狀態(tài)管理
  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對(duì)象是否為空
          newState.store = appState.store;
        } else {
          /// 這地方增加字段判斷宝穗,是否需要更新
          if ((p.store.themeColor != appState.store.themeColor)) {
            newState.store.themeColor = appState.store.themeColor;
          }

          /// 如果增加字段封孙,同理上面的判斷然后賦值...

        }

        /// 返回新的 state 并將數(shù)據(jù)設(shè)置到 ui
        return newState;
      }
      return pageState;
    };
  }

  static visitor(String path, Page<Object, dynamic> page) {
    if (page.isTypeof<GlobalBaseState>()) {
      ///建立AppStore驅(qū)動(dòng)PageStore的單向數(shù)據(jù)連接
      ///參數(shù)1 AppStore  參數(shù)2 當(dāng)AppStore.state變化時(shí),PageStore.state該如何變化
      page.connectExtraStore<GlobalState>(GlobalStore.store, _updateState());
    }
  }
}

Store模塊

下面?zhèn)z個(gè)模塊是需要改動(dòng)代碼的模塊

  • state
    • 這里使用了StoreModel中間實(shí)體,注意讽营,這地方實(shí)體字段store虎忌,初始化是必須的,不然在子模塊引用該實(shí)體下的字段會(huì)報(bào)空指針
abstract class GlobalBaseState{
  StoreModel store;
}

class GlobalState implements GlobalBaseState, Cloneable<GlobalState>{

  @override
  GlobalState clone() {
    return GlobalState();
  }

  @override
  StoreModel store = StoreModel(
    /// store這個(gè)變量,在這必須示例化,不然引用該變量中的字段,會(huì)報(bào)空指針
    /// 下面的字段,賦初值,就是初始時(shí)展示的全局狀態(tài)
    /// 這地方初值,理應(yīng)從緩存或數(shù)據(jù)庫中取,表明用戶選擇的全局狀態(tài)
    themeColor: Colors.lightBlue
  );
}

///中間全局實(shí)體
///需要增加字段就在這個(gè)實(shí)體里面添加就行了
class StoreModel {
  Color themeColor;

  StoreModel({this.themeColor});
}
  • reducer
    • 這地方改動(dòng)非常小橱鹏,將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;
}

下面?zhèn)z個(gè)模塊代碼沒有改動(dòng)膜蠢,但是為了思路完整,同樣貼出來

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

子模塊使用

  • 這里就用計(jì)數(shù)器模塊的來舉例莉兰,因?yàn)閮H僅只需要改動(dòng)少量代碼挑围,且只涉及state和view,所以其它模塊代碼也不重復(fù)貼出了
  • state
    • 因?yàn)槭怯弥虚g實(shí)體糖荒,所以在clone方法里面必須將實(shí)現(xiàn)的store字段加上杉辙,不然會(huì)報(bào)空指針
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
    • 這里面僅僅改動(dòng)了一行,在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"),
      ///全局主題蜘矢,僅僅在此處改動(dòng)了一行
      backgroundColor: state.store.themeColor,
    ),
    ///下面其余代碼省略....
}
  • 如果其他模塊也需要做主題色狂男,也按照此處邏輯改動(dòng)即可

調(diào)用

  • 調(diào)用和上面說的一樣,用下述全局方式在合適的地方調(diào)用
GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor());

體驗(yàn)

通過上面的優(yōu)化品腹,使用體驗(yàn)提升不是一個(gè)級(jí)別岖食,大大提升的全局模式的擴(kuò)展性,我們就算后期增加了大量的全局字段舞吭,也可以一個(gè)個(gè)模塊慢慢改泡垃,不用一次爆肝全改完蝇恶,猝死的概率又大大減少了夹姥!

img

Component使用

Component是個(gè)比較常用的模塊,上面使用列表的時(shí)候迷扇,就使用到了Component惧浴,這次我們來看看存和,在頁面中直接使用Component,可插拔式使用赶舆!Component的使用總的來說是比較簡(jiǎn)單了哑姚,比較關(guān)鍵的是在State中建立起連接。

效果圖

fish_redux中component
  • 上圖的效果是在頁面中嵌入了倆個(gè)Component芜茵,改變子Component的操作是在頁面中完成的
  • 先看下頁面結(jié)構(gòu)
image-20200905183821129

Component

這地方寫了一個(gè)Component叙量,代碼很簡(jiǎn)單,來看看吧

  • component

這地方代碼是自動(dòng)生成了九串,沒有任何改動(dòng)绞佩,就不貼了

  • state
    • initState():我們需要注意,Component中的initState()方法在內(nèi)部沒有調(diào)用猪钮,雖然自動(dòng)生成的代碼有這個(gè)方法品山,但是無法起到初始化作用,可以刪掉該方法
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這層肘交,就沒創(chuàng)建該文件,老規(guī)矩扑馁,先看看state

  • state
    • 這地方是非常重要的地方涯呻,XxxxConnecto的實(shí)現(xiàn)形式是看官方代碼寫的
    • computed():該方法是必須實(shí)現(xiàn)的,這個(gè)類似直接的get()方法腻要,但是切記不能像get()直接返回state.leftAreaState()或state.rightAreaState复罐,某些場(chǎng)景初始化無法刷新,因?yàn)槭峭粋€(gè)對(duì)象雄家,會(huì)被判斷未更改效诅,所以會(huì)不刷新控件
      • 注意了注意了,這邊做了優(yōu)化,直接返回clone方法乱投,這是對(duì)官方賦值寫法的一個(gè)優(yōu)化咽笼,也可以避免上面說的問題,大家可以思考思考
    • set():該方法是Component數(shù)據(jù)流回推到頁面的state篡腌,保持倆者state數(shù)據(jù)一致褐荷;如果Component模塊更新了自己的State勾效,不寫這個(gè)方法會(huì)報(bào)錯(cuò)的
class CompState implements Cloneable<CompState> {
  AreaState leftAreaState;
  AreaState rightAreaState;

  @override
  CompState clone() {
    return CompState()
      ..rightAreaState = rightAreaState
      ..leftAreaState = leftAreaState;
  }
}

CompState initState(Map<String, dynamic> args) {
  ///初始化數(shù)據(jù)
  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
    • 寫完連接器后嘹悼,我們?cè)赑age里面綁定下,就能使用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就非常簡(jiǎn)單了: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);

  //改變r(jià)ightAreaComponent中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);
}

總結(jié)下

總的來說层宫,Component的使用還是比較簡(jiǎn)單的杨伙;如果我們把某個(gè)復(fù)雜的列表提煉出一個(gè)Component的,很明顯有個(gè)初始化的過程萌腿,這里我們需要將:請(qǐng)求參數(shù)調(diào)體或列表詳情操作限匣,在page頁面處理好,然后再更新給我們綁定的子Component的State毁菱,這樣就能起到初始化某個(gè)模塊的作用米死;至于刷新,下拉等后續(xù)操作贮庞,就讓Component內(nèi)部自己去處理了

廣播

廣播在復(fù)雜的業(yè)務(wù)場(chǎng)景峦筒,能夠起到非常巨大的作用,能非常輕松使用跨頁面交互窗慎,跨Component交互物喷!

fish_redux中是帶有廣播的通信方式,使用的方式很簡(jiǎn)單遮斥,這本是effect層峦失,ctx參數(shù)自帶的一個(gè)api,這里介紹一下

使用

說明:請(qǐng)注意廣播可以通知任何頁面的枚舉方法术吗,你可以單獨(dú)寫一個(gè)枚舉事件尉辑,也可以不寫,直接使用某個(gè)頁面的枚舉事件较屿,是完全可以

  • action
    • 廣播事件單獨(dú)寫了一個(gè)action文件隧魄,僅方便演示,也可以不單獨(dú)新建一個(gè)廣播枚舉Action
enum BroadcastAction { toNotify }

class BroadcastActionCreator {
  ///廣播通知
  static Action toNotify(String msg) {
    return Action(BroadcastAction.toNotify, payload: msg);
  }
}
  • 發(fā)送廣播
    • 這是頁面跳轉(zhuǎn)的方法吝镣,就在此處寫了堤器,如果想看詳細(xì)代碼的話,可以去demo地址里面看下
void _backFirst(Action action, Context<SecondState> ctx) {
  //廣播通信
  ctx.broadcast(BroadcastActionCreator.toNotify("頁面二發(fā)送廣播通知"));
}
  • 接受廣播
Effect<FirstState> buildEffect() {
  return combineEffects(<Object, Effect<FirstState>>{
    //接受發(fā)送的廣播消息
    BroadcastAction.toNotify: _receiveNotify,
  });
}
void _receiveNotify(Action action, Context<FirstState> ctx) async {
  ///接受廣播
  print("跳轉(zhuǎn)一頁面:${action.payload}");
}

說明

廣播的使用還是挺簡(jiǎn)單的末贾,基本和dispatch的使用是一致的闸溃,dispatch是模塊的,而broadcast是處于Page或Component都能進(jìn)行通信交互,很多情況下辉川,我們?cè)谝粋€(gè)頁面進(jìn)行了操作表蝙,其他頁面也需要同步做一些處理,使用廣播就很簡(jiǎn)單了

注意: 廣播發(fā)送和接受是一對(duì)多的關(guān)系乓旗,一處發(fā)送府蛇,可以在多處接受;和dispatch發(fā)送事件屿愚,如果在effect里面接受汇跨,在reducer就無法接受的情況是不一樣的(被攔截了)

開發(fā)小技巧

弱化reducer

無限弱化了reducer層作用

  • 在日常使用fish_redux和flutter_bloc后,實(shí)際能深刻體會(huì)reducer層實(shí)際上只是相當(dāng)于bloc中yield
    或emit關(guān)鍵字的作用妆距,職能完全可以弱化為穷遂,僅僅作為狀態(tài)刷新;這樣可以大大簡(jiǎn)化開發(fā)流程娱据,只需要關(guān)注
    view -> action -> effect (reducer:使用統(tǒng)一的刷新事件)
  • 下面范例代碼蚪黑,處理數(shù)據(jù)的操作直接在effect層處理,如需要更改數(shù)據(jù)中剩,直接對(duì)ctx.state進(jìn)行操作忌穿,涉及刷新頁面的操作,統(tǒng)一調(diào)用onRefresh事件结啼;對(duì)于一個(gè)頁面有幾十個(gè)表單的情況掠剑,這種操作,能大大提升你的開發(fā)速度和體驗(yàn)妆棒,親身體驗(yàn)澡腾,大家可以嘗試下
Reducer<TestState> buildReducer() {
  return asReducer(
    <Object, Reducer<TestState>>{
      TestAction.onRefresh: _onRefresh,
    },
  );
}

TestState _onRefresh(TreeState state, Action action) {
  return state.clone();
}
  • 具體可以查看 玩android 項(xiàng)目代碼;花了一些時(shí)間糕珊,把玩android項(xiàng)目代碼所有模塊全部重構(gòu)了动分,肝痛

widget組合式開發(fā)

說明

這種開發(fā)形式,可以說是個(gè)慣例红选,在android里面是封裝一個(gè)個(gè)View澜公,View里有對(duì)應(yīng)的一套,邏輯自洽的功能喇肋,然后在主xm里面組合這些View坟乾;這種思想完全可以引申到Flutter里,而且蝶防,開發(fā)體驗(yàn)更上幾百層樓甚侣,讓你的widget組合可以更加靈活百變,百變星君

  • view模塊中间学,頁面使用widget組合的方式去構(gòu)造的殷费,只傳入必要的數(shù)據(jù)源和保留一些點(diǎn)擊回調(diào)

  • 為什么用widget組合方式構(gòu)造頁面印荔?

    • 非常復(fù)雜的界面,必須將頁面分成一個(gè)個(gè)小模塊详羡,然后再將其組合仍律, 每個(gè)小模塊Widget內(nèi)部應(yīng)當(dāng)對(duì)自身的的職能,能邏輯自洽的去處理实柠;這種組合的方式呈現(xiàn)的代碼水泉,會(huì)非常的層次分明,不會(huì)讓你的代碼寫著寫著窒盐,突然就變成shit
  • 組合widget關(guān)鍵點(diǎn)

    • 一般來說草则,我們并不關(guān)注widget內(nèi)部頁面的實(shí)現(xiàn),只需要關(guān)心的是widget需要的數(shù)據(jù)源登钥, 以及widget對(duì)交互的反饋畔师;例如:我點(diǎn)擊widget后娶靡,widget回調(diào)事件牧牢,并傳達(dá)一些數(shù)據(jù)給我;至于內(nèi)部怎么實(shí)現(xiàn)姿锭, 外部并不關(guān)心塔鳍,請(qǐng)勿將dispatch傳遞到封裝的widget內(nèi)部,這會(huì)使我們關(guān)注的事件被封裝在內(nèi)部
  • 具體請(qǐng)查看 玩android 項(xiàng)目代碼

最后

Demo地址

  • 這片文章呻此,說實(shí)話轮纫,花了不少精力去寫的,也花了不少時(shí)間構(gòu)思焚鲜;主要是例子掌唾,必須要自己重寫下,反復(fù)思考例子是否合理等等忿磅,頭皮微涼糯彬。
  • 代碼地址:代碼demo地址
  • fish_redux版-玩Android:fish_redux版-玩android
  • 大家如果覺得有收獲,就給我點(diǎn)個(gè)贊吧葱她!你的點(diǎn)贊撩扒,是我碼字的最大動(dòng)力!
img

系列文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末泉手,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子偶器,更是在濱河造成了極大的恐慌斩萌,老刑警劉巖啡氢,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異术裸,居然都是意外死亡倘是,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門袭艺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搀崭,“玉大人,你說我怎么就攤上這事猾编×龆茫” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵答倡,是天一觀的道長轰传。 經(jīng)常有香客問我,道長瘪撇,這世上最難降的妖魔是什么获茬? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮倔既,結(jié)果婚禮上恕曲,老公的妹妹穿的比我還像新娘。我一直安慰自己渤涌,他們只是感情好佩谣,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著实蓬,像睡著了一般茸俭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上安皱,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天调鬓,我揣著相機(jī)與錄音,去河邊找鬼练俐。 笑死袖迎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的腺晾。 我是一名探鬼主播燕锥,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼悯蝉!你這毒婦竟也來了归形?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤鼻由,失蹤者是張志新(化名)和其女友劉穎暇榴,沒想到半個(gè)月后厚棵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蔼紧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年婆硬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奸例。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡彬犯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出查吊,到底是詐尸還是另有隱情谐区,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布逻卖,位于F島的核電站宋列,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏评也。R本人自食惡果不足惜炼杖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望仇参。 院中可真熱鬧嘹叫,春花似錦、人聲如沸诈乒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怕磨。三九已至,卻和暖如春消约,著一層夾襖步出監(jiān)牢的瞬間肠鲫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工或粮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留导饲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓氯材,卻偏偏與公主長得像渣锦,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子氢哮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容