fish_redux 「食用指南」

好久沒更新文章了鳖眼,最近趁著娃睡覺的功夫惶傻,嘗試了下 fish_redux,這邊做下記錄壹瘟,安全無毒苟呐,小伙伴們可放心食用(本文基于版本 fish_redux 0.3.1)。

fish_redux 的介紹就不在這廢話了俐筋,需要的小伙伴可以直接查看 fish_redux 官方文檔牵素,這里我們直接通過例子來踩坑。

項目的大概結(jié)構(gòu)如下所示澄者,具體可以查看 倉庫代碼

可以看到 UI 包下充斥著許多的 action笆呆,effect请琳,reducerstate赠幕,view俄精,pagecomponent榕堰,adapter 類竖慧,不要慌,接下來大概的會說明下每個類的職責逆屡。

fish_redux 的分工合作

  1. action 是用來定義一些操作的聲明圾旨,其內(nèi)部包含一個枚舉類 XxxAction 和 聲明類 XxxActionCreator,枚舉類用來定義一個操作魏蔗,ActionCreator 用來定義一個 Action砍的,通過 dispatcher 發(fā)送對應(yīng) Action 就可以實現(xiàn)一個操作。例如我們需要打開一個行的頁面莺治,可以如下進行定義

    enum ExamAction { openNewPage廓鞠, openNewPageWithParams }
    
    class ExamActionCreator {
        static Action onOpenNewPage(){
            // Action 可以傳入一個 payload,例如我們需要攜帶參數(shù)跳轉(zhuǎn)界面谣旁,則可以通過 payload 傳遞
            // 然后在 effect 或者 reducer 層通過 action.payload 獲取
            return const Action(ExamAction.openNewPage);
        }
        
        static Action onOpenNewPageWithParams(String str){
            return Action(ExamAction.openNewPageWithParams, payload: str);
        }
    }
    
  2. effect 用來定義一些副作用的操作床佳,例如網(wǎng)絡(luò)請求,頁面跳轉(zhuǎn)等榄审,通過 buildEffect 方法結(jié)合 Action 和最終要實現(xiàn)的副作用夕土,例如還是打開頁面的操作,可通過如下方式實現(xiàn)

    Effect<ExamState> buildEffect() {
      return combineEffects(<Object, Effect<ExamState>>{
        ExamAction.openNewPage: _onOpenNewPage,
      });
    }
    
    void _onOpenNewPage(Action action, Context<ExamState> ctx) {
      Navigator.of(ctx.context).pushNamed('路由地址');
    }
    
    
  3. reducer 用來定義數(shù)據(jù)發(fā)生變化的操作瘟判,比如網(wǎng)絡(luò)請求后怨绣,數(shù)據(jù)發(fā)生了變化,則把原先的數(shù)據(jù) clone 一份出來拷获,然后把新的值賦值上去篮撑,例如有個網(wǎng)絡(luò)請求,發(fā)生了數(shù)據(jù)的變化匆瓜,可通過如下方式實現(xiàn)

    Reducer<ExamState> buildReducer() {
      return asReducer(
        <Object, Reducer<ExamState>>{
          HomeAction.onDataRequest: _onDataRequest,
        },
      );
    }
    
    ExamState _onDataRequest(ExamState state, Action action) {
      // data 的數(shù)據(jù)通過 action 的 payload 進行傳遞赢笨,reducer 只負責數(shù)據(jù)刷新
      return state.clone()..data = action.payload;
    }
    
  4. state 就是當前頁面需要展示的一些數(shù)據(jù)

  5. view 就是當前的 UI 展示效果

  6. pagecomponent 就是上述的載體,用來將數(shù)據(jù)和 UI 整合到一起

  7. adapter 用來整合列表視圖

Show the code

這邊要實現(xiàn)的例子大概長下面的樣子驮吱,一個 Drawer 列表茧妒,實現(xiàn)主題色,語言左冬,字體的切換功能桐筏,當然后期會增加別的功能,目前先看這部分[home 模塊]拇砰,基本上涵蓋了上述所有的內(nèi)容梅忌。在寫代碼之前狰腌,可以先安裝下 FishRedux 插件,可以快速構(gòu)建類牧氮,直接在插件市場搜索即可

整體配置
void main() {
  runApp(createApp());
}

Widget createApp() {
  // 頁面路由配置琼腔,所有頁面需在此注冊路由名
  final AbstractRoutes routes = PageRoutes(
      pages: <String, Page<Object, dynamic>>{
        RouteConfigs.route_name_splash_page: SplashPage(), // 起始頁
        RouteConfigs.route_name_home_page: HomePage(), // home 頁
      });

  return MaterialApp(
      title: 'FishWanAndroid',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.light(),
      localizationsDelegates: [ // 多語言配置
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        FlutterI18nDelegate()
      ],
      supportedLocales: [Locale('en'), Locale('zh')],
      home: routes.buildPage(RouteConfigs.route_name_splash_page, null), // 配置 home 頁
      onGenerateRoute: (settings) {
        return CupertinoPageRoute(builder: (context) {
          return routes.buildPage(settings.name, settings.arguments);
        });
      },
    );
}
Home 整體構(gòu)建

Home 頁面整體就是一個帶 Drawer,主體是一個 PageView踱葛,頂部帶一個 banner 控件丹莲,banner 的數(shù)據(jù)我們通過網(wǎng)絡(luò)進行獲取,在 Drawer 是一個點擊列表尸诽,包括圖標甥材,文字和動作,那么我們可以創(chuàng)建一個 DrawerSettingItem 類逊谋,用了創(chuàng)建列表擂达,頭部的用戶信息目前可以先寫死土铺。所以我們可以先搭建 HomeState

class HomeState implements Cloneable<HomeState> {
  int currentPage; // PageView 的當前項
  List<HomeBannerDetail> banners; // 頭部 banner 數(shù)據(jù)
  List<SettingItemState> settings; // Drawer 列表數(shù)據(jù)

  @override
  HomeState clone() {
    return HomeState()
      ..currentPage = currentPage
      ..banners = banners
      ..settings = settings;
  }
}

HomeState initState(Map<String, dynamic> args) {
  return HomeState();
}

同樣的 HomeAction 也可以定義出來

enum HomeAction { pageChange, fetchBanner, loadSettings, openDrawer, openSearch }

class HomeActionCreator {
  static Action onPageChange(int page) { // PageView 切換
    return Action(HomeAction.pageChange, payload: page);
  }

  static Action onFetchBanner(List<HomeBannerDetail> banner) { // 更新 banner 數(shù)據(jù)
    return Action(HomeAction.fetchBanner, payload: banner);
  }

  static Action onLoadSettings(List<SettingItemState> settings) { // 加載 setting 數(shù)據(jù)
    return Action(HomeAction.loadSettings, payload: settings);
  }

  static Action onOpenDrawer(BuildContext context) { // 打開 drawer 頁面
    return Action(HomeAction.openDrawer, payload: context);
  }

  static Action onOpenSearch() { // 打開搜索頁面
    return const Action(HomeAction.openSearch);
  }
}
構(gòu)建 banner

為了加強頁面的復用性胶滋,可以通過 component 進行模塊構(gòu)建,具體查看 banner_component 包下文件悲敷。首先定義 state究恤,因為 banner 作為 home 下的內(nèi)容,所以其 state 不能包含 HomeState 外部的屬性后德,因此定義如下

class HomeBannerState implements Cloneable<HomeBannerState> {
  List<HomeBannerDetail> banners; // banner 數(shù)據(jù)列表

  @override
  HomeBannerState clone() {
    return HomeBannerState()..banners = banners;
  }
}

HomeBannerState initState(Map<String, dynamic> args) {
  return HomeBannerState();
}

action 只有點擊的 Action部宿,所以也可以快速定義

enum HomeBannerAction { openBannerDetail }

class HomeBannerActionCreator {
  static Action onOpenBannerDetail(String bannerUrl) {
    return Action(HomeBannerAction.openBannerDetail, payload: bannerUrl);
  }
}

由于不涉及到數(shù)據(jù)的改變,所以可以不需要定義 reducer瓢湃,通過 effect 來處理 openBannerDetail 即可

Effect<HomeBannerState> buildEffect() {
  return combineEffects(<Object, Effect<HomeBannerState>>{
    // 當收到 openBannerDetail 對應(yīng)的 Action 的時候理张,執(zhí)行對應(yīng)的方法
    HomeBannerAction.openBannerDetail: _onOpenBannerDetail,
  });
}

void _onOpenBannerDetail(Action action, Context<HomeBannerState> ctx) {
  // payload 中攜帶了 bannerUrl 參數(shù),用來打開對應(yīng)的網(wǎng)址
  // 可查看 [HomeBannerActionCreator.onOpenBannerDetail] 方法定義
  RouteConfigs.openWebDetail(ctx.context, action.payload);
}

接著就是對 view 進行定義啦

Widget buildView(HomeBannerState state, Dispatch dispatch, ViewService viewService) {
  var _size = MediaQuery.of(viewService.context).size;

  return Container(
    height: _size.height / 5, // 設(shè)置固定高度
    child: state.banners == null || state.banners.isEmpty
        ? SizedBox()
        : Swiper( // 當有數(shù)據(jù)存在時绵患,才顯示 banner
            itemCount: state.banners.length,
            transformer: DeepthPageTransformer(),
            loop: true,
            autoplay: true,
            itemBuilder: (_, index) {
              return GestureDetector(
                child: FadeInImage.assetNetwork(
                  placeholder: ResourceConfigs.pngPlaceholder,
                  image: state.banners[index].imagePath ?? '',
                  width: _size.width,
                  height: _size.height / 5,
                  fit: BoxFit.fill,
                ),
                onTap: () { // dispatch 對應(yīng)的 Action雾叭,當 effect 或者 reduce 收到會進行對應(yīng)處理
                  dispatch(HomeBannerActionCreator.onOpenBannerDetail(state.banners[index].url));
                },
              );
            },
          ),
  );
}

最后再回到 component,這個類插件已經(jīng)定義好了落蝙,基本上不需要做啥修改

class HomeBannerComponent extends Component<HomeBannerState> {
  HomeBannerComponent()
      : super(
          effect: buildEffect(), // 對應(yīng) effect 的方法
          reducer: buildReducer(), // 對應(yīng) reducer 的方法
          view: buildView, // 對應(yīng) view 的方法
          dependencies: Dependencies<HomeBannerState>(
            adapter: null, // 用于展示數(shù)據(jù)列表
            // 組件插槽织狐,注冊后可通過 viewService.buildComponent 方法生成對應(yīng)組件
            slots: <String, Dependent<HomeBannerState>>{},
          ),
        );
}

這樣就定義好了一個 component,可以通過注冊 slot 方法使用該 component

使用 banner component

在上一步筏勒,我們已經(jīng)定義好了 banner component移迫,這里就可以通過 slot 愉快的進行使用了,首先管行,需要定義一個 connector厨埋,connector 是用來連接兩個父子 state 的橋梁。

// connector 需要繼承 ConnOp 類捐顷,并混入 ReselectMixin揽咕,泛型分別為父級 state 和 子級 state
class HomeBannerConnector extends ConnOp<HomeState, HomeBannerState> with ReselectMixin {
  @override
  HomeBannerState computed(HomeState state) {
    // computed 用于父級 state 向子級 state 數(shù)據(jù)的轉(zhuǎn)換
    return HomeBannerState()..banners = state.banners;
  }

  @override
  List factors(HomeState state) {
    // factors 為轉(zhuǎn)換的因子悲酷,返回所有改變的因子即可
    return state.banners ?? [];
  }
}
Page 中注冊 slot

page 的結(jié)構(gòu)和 component 的結(jié)構(gòu)是一樣的,使用 component 直接在 dependencies 中注冊 slots 即可

class HomePage extends Page<HomeState, Map<String, dynamic>> {
  HomePage()
      : super(
          initState: initState,
          effect: buildEffect(),
          reducer: buildReducer(),
          view: buildView,
          dependencies: Dependencies<HomeState>(
            adapter: null,
            slots: <String, Dependent<HomeState>>{
               // 通過 slot 進行 component 注冊
              'banner': HomeBannerConnector() + HomeBannerComponent(),
              'drawer': HomeDrawerConnector() + HomeDrawerComponent(), // 定義側(cè)滑組件亲善,方式同 banner
            },
          ),
          middleware: <Middleware<HomeState>>[],
        );
}

注冊完成 slot 之后设易,就可以直接在 view 上使用了,使用的方法也很簡單

Widget buildView(HomeState state, Dispatch dispatch, ViewService viewService) {
  var _pageChildren = <Widget>[
    // page 轉(zhuǎn)換成 widget 通過 buildPage 實現(xiàn)蛹头,參數(shù)表示要傳遞的參數(shù)顿肺,無需傳遞則為 null 即可
    // 目前 HomeArticlePage 只做簡單的 text 展示
    HomeArticlePage().buildPage(null), 
    HomeArticlePage().buildPage(null),
    HomeArticlePage().buildPage(null),
  ];

  return Theme(
    data: ThemeData(primarySwatch: state.themeColor),
    child: Scaffold(
      body: Column(
        children: <Widget>[
          // banner slot
          // 通過 viewService.buildComponent('slotName') 使用,slotName 為 page 中注冊的 component key
          viewService.buildComponent('banner'), 
          Expanded(
            child: TransformerPageView(
              itemCount: _pageChildren.length,
              transformer: ScaleAndFadeTransformer(fade: 0.2, scale: 0.8),
              onPageChanged: (index) {
                // page 切換的時候把當前的 page index 值通過 action 傳遞給 state渣蜗,
                // state 可查看上面提到的 HomeState
                dispatch(HomeActionCreator.onPageChange(index));
              },
              itemBuilder: (context, index) => _pageChildren[index],
            ),
          ),
        ],
      ), 
      // drawer slot屠尊,方式同 banner
      drawer: viewService.buildComponent('drawer'),
    ),
  );
}
更新 banner 數(shù)據(jù)

在前面的 HomeActionCreator 中,我們定義了 onFetchBanner 這個 Action耕拷,需要傳入一個 banner 列表作為參數(shù)讼昆,所以更新數(shù)據(jù)可以這么進行操作

Effect<HomeState> buildEffect() {
  return combineEffects(<Object, Effect<HomeState>>{
    // Lifecycle 的生命周期同 StatefulWidget 對應(yīng),所以在初始化的時候處理請求 banner 數(shù)據(jù)等初始化操作
    Lifecycle.initState: _onPageInit, 
  });
}

void _onPageInit(Action action, Context<HomeState> ctx) async {
  ctx.dispatch(HomeActionCreator.onPageChange(0));
  var banners = await Api().fetchHomeBanner(); // 網(wǎng)絡(luò)請求骚烧,具體的可以查看 `api.dart` 文件
  ctx.dispatch(HomeActionCreator.onFetchBanner(banners)); // 通過 dispatch 發(fā)送 Action
}

一開始我們提到過浸赫,effect 只負責一些副作用的操作,reducer 負責數(shù)據(jù)的修改操作赃绊,所以在 reducer 需要做數(shù)據(jù)的刷新

Reducer<HomeState> buildReducer() {
  return asReducer(
    <Object, Reducer<HomeState>>{
      // 當 dispatch 發(fā)送了對應(yīng)的 Action 的時候既峡,就會調(diào)用對應(yīng)方法
      HomeAction.fetchBanner: _onFetchBanner, 
    },
  );
}

HomeState _onFetchBanner(HomeState state, Action action) {
  // reducer 修改數(shù)據(jù)方式是先 clone 一份數(shù)據(jù),然后進行賦值
  // 這樣就把網(wǎng)絡(luò)請求返回的數(shù)據(jù)更新到 view 層了
  return state.clone()..banners = action.payload; 
}

通過上述操作碧查,就將網(wǎng)絡(luò)的 banner 數(shù)據(jù)加載到 UI

使用 adapter 構(gòu)建 drawer 功能列表

drawer 由一個頭部和列表構(gòu)成运敢,頭部可以通過 component 進行構(gòu)建,方法類似上述 banner componentdrawer component忠售,唯一區(qū)別就是一個在 pageslots 注冊传惠,一個在 componentslots 注冊。所以構(gòu)建 drawer 就是需要去構(gòu)建一個列表稻扬,這里就需要用到 adapter 來處理了卦方。

在老的版本中(本文版本 0.3.1),構(gòu)建 adapter 一般通過 DynamicFlowAdapter 實現(xiàn)腐螟,而且在插件中也可以發(fā)現(xiàn)愿汰,但是在該版本下,DynamicFlowAdapter 已經(jīng)被標記為過時乐纸,并且官方推薦使用 SourceFlowAdapter衬廷。SourceFlowAdapter 需要指定一個 State,并且該 State 必須繼承自 AdapterSource汽绢。AdapterSource 有兩個子類吗跋,分別是可變數(shù)據(jù)源的 MutableSource 和不可變數(shù)據(jù)源的 ImmutableSource,兩者的差別因為官方也沒有給出具體的說明,本文使用 MutableSource 來處理 adapter跌宛。所以對應(yīng)的 state 定義如下

class HomeDrawerState extends MutableSource implements Cloneable<HomeDrawerState> {
 List<SettingItemState> settings; // state 為列表 item component 對應(yīng)的 state

  @override
  HomeDrawerState clone() {
    return HomeDrawerState()
      ..settings = settings;
  }

  @override
  Object getItemData(int index) => settings[index]; // 對應(yīng) index 下的數(shù)據(jù)

  @override
  String getItemType(int index) => DrawerSettingAdapter.settingType; // 對應(yīng) index 下的數(shù)據(jù)類型

  @override
  int get itemCount => settings?.length ?? 0; // 數(shù)據(jù)源長度

  @override
  void setItemData(int index, Object data) => settings[index] = data; // 對應(yīng) index 下的數(shù)據(jù)如何修改
}

同樣酗宋,adapter 也可以如下進行定義

class DrawerSettingAdapter extends SourceFlowAdapter<HomeDrawerState> {
  static const settingType = 'setting';

  DrawerSettingAdapter()
      : super(pool: <String, Component<Object>>{
          // 不同數(shù)據(jù)類型,對應(yīng)的 component 組件疆拘,type 和 state getItemType 方法對應(yīng)
          // 允許多種 type
          settingType: SettingItemComponent(), 
        });
}

經(jīng)過上述兩部分蜕猫,就定義好了 adapter 的主體部分啦,接著就是要實現(xiàn) SettingItemComponent 這個組件哎迄,只需要簡單的 ListTile 即可回右,ListTile 的展示內(nèi)容通過對應(yīng)的 state 來設(shè)置

/// state
class SettingItemState implements Cloneable<SettingItemState> {
  DrawerSettingItem item; // 定義了 ListTile 的圖標,文字漱挚,以及點擊

  SettingItemState({this.item});

  @override
  SettingItemState clone() {
    return SettingItemState()
      ..item = item;
  }
}
/// view
Widget buildView(SettingItemState state, Dispatch dispatch, ViewService viewService) {
  return ListTile(
    leading: Icon(state.item.itemIcon),
    title: Text(
      FlutterI18n.translate(viewService.context, state.item.itemTextKey),
      style: TextStyle(
        fontSize: SpValues.settingTextSize,
      ),
    ),
    onTap: () => dispatch(state.item.action),
  );
}

因為不涉及數(shù)據(jù)的修改翔烁,所以不需要定義 reducer,點擊實現(xiàn)通過 effect 實現(xiàn)即可旨涝,具體的代碼可查看對應(yīng)文件蹬屹,這邊不貼多余代碼了.

經(jīng)過上述步驟,adapter 就定義完成了白华,接下來就是要使用對應(yīng)的 adapter 了慨默,使用也非常方便,我們回到 HomeDrawerComponent 這個類衬鱼,在 adapter 屬性下加上我們前面定義好的 DrawerSettingAdapter 就行了

/// component
class HomeDrawerComponent extends Component<HomeDrawerState> {
  HomeDrawerComponent()
      : super(
          view: buildView,
          dependencies: Dependencies<HomeDrawerState>(
            // 給 adapter 屬性賦值的時候业筏,需要加上 NoneConn<XxxState>
            adapter: NoneConn<HomeDrawerState>() + DrawerSettingAdapter(),
            slots: <String, Dependent<HomeDrawerState>>{
              'header': HeaderConnector() + SettingHeaderComponent(),
            },
          ),
        );
}

/// 對應(yīng) view
Widget buildView(HomeDrawerState state, Dispatch dispatch, ViewService viewService) {
  return Drawer(
    child: Column(
      children: <Widget>[
        viewService.buildComponent('header'),
        Expanded(
          child: ListView.builder(
            // 通過 viewService.buildAdapter 獲取列表信息
            // 同樣憔杨,在 GridView 也可以使用 adapter
            itemBuilder: viewService.buildAdapter().itemBuilder,
            itemCount: viewService.buildAdapter().itemCount,
          ),
        )
      ],
    ),
  );
}

將列表設(shè)置到界面后鸟赫,就剩下最后的數(shù)據(jù)源了,數(shù)據(jù)從哪來呢消别,答案當然是和 banner component 一樣抛蚤,通過上層獲取,這邊不需要通過網(wǎng)絡(luò)獲取寻狂,直接在本地定義就行了岁经,具體的獲取查看文件 home\effect.dart 下的 _loadSettingItems 方法,實現(xiàn)和獲取 banner 數(shù)據(jù)無多大差別蛇券,除了一個本地加載缀壤,一個網(wǎng)絡(luò)獲取。

fish_redux 實現(xiàn)全局狀態(tài)

fish_redux 全局狀態(tài)的實現(xiàn)纠亚,我們參考 官方 demo塘慕,首先構(gòu)造一個 GlobalBaseState 抽象類(涉及到全局狀態(tài)變化的 state 都需要繼承該類),這個類定義了全局變化的狀態(tài)屬性蒂胞,例如我們該例中需要實現(xiàn)全局的主題色图呢,語言和字體的改變,那么我們就可以如下定義

abstract class GlobalBaseState {
  Color get themeColor;

  set themeColor(Color color);

  Locale get localization;

  set localization(Locale locale);

  String get fontFamily;

  set fontFamily(String fontFamily);
}

接著需要定義一個全局 State,繼承自 GlobalBaseState 并實現(xiàn) Cloneable

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

  @override
  Locale localization;

  @override
  String fontFamily;

  @override
  GlobalState clone() {
    return GlobalState()
      ..fontFamily = fontFamily
      ..localization = localization
      ..themeColor = themeColor;
  }
}

接著需要定義一個全局的 store 來存儲狀態(tài)值

class GlobalStore {
  // Store 用來存儲全局狀態(tài) GlobalState蛤织,當刷新狀態(tài)值的時候赴叹,通過
  // store 的 dispatch 發(fā)送相關(guān)的 action 即可做出相應(yīng)的調(diào)整
  static Store<GlobalState> _globalStore; 

  static Store<GlobalState> get store => _globalStore ??= createStore(
        GlobalState(),
        buildReducer(), // reducer 用來刷新狀態(tài)值
      );
}

/// action 
enum GlobalAction { changeThemeColor, changeLocale, changeFontFamily }

class GlobalActionCreator {
  static Action onChangeThemeColor(Color themeColor) {
    return Action(GlobalAction.changeThemeColor, payload: themeColor);
  }

  static Action onChangeLocale(Locale localization) {
    return Action(GlobalAction.changeLocale, payload: localization);
  }

  static Action onChangeFontFamily(String fontFamily) {
    return Action(GlobalAction.changeFontFamily, payload: fontFamily);
  }
}

/// reducer 的作用就是刷新主題色,字體和語言
Reducer<GlobalState> buildReducer() {
  return asReducer(<Object, Reducer<GlobalState>>{
    GlobalAction.changeThemeColor: _onThemeChange,
    GlobalAction.changeLocale: _onLocalChange,
    GlobalAction.changeFontFamily: _onFontFamilyChange,
  });
}

GlobalState _onThemeChange(GlobalState state, Action action) {
  return state.clone()..themeColor = action.payload;
}

GlobalState _onLocalChange(GlobalState state, Action action) {
  return state.clone()..localization = action.payload;
}

GlobalState _onFontFamilyChange(GlobalState state, Action action) {
  return state.clone()..fontFamily = action.payload;
}

定義完全局 StateStore 后指蚜,回到我們的 main.dart 下注冊路由部分乞巧,一開始我們使用 PageRoutes 的時候只傳入了 page 參數(shù),還有個 visitor 參數(shù)沒有使用摊鸡,這個就是用來刷新全局狀態(tài)的摊欠。

final AbstractRoutes routes = PageRoutes(
      pages: <String, Page<Object, dynamic>>{
          // ...
      },
      visitor: (String path, Page<Object, dynamic> page) {
        if (page.isTypeof<GlobalBaseState>()) {
          // connectExtraStore 方法將 page store 和 app store 連接起來
          // globalUpdate() 就是具體的實現(xiàn)邏輯
          page.connectExtraStore<GlobalState>(GlobalStore.store, globalUpdate());
        }
      });

/// globalUpdate
globalUpdate() => (Object pageState, GlobalState appState) {
      final GlobalBaseState p = pageState;

      if (pageState is Cloneable) {
        final Object copy = pageState.clone();
        final GlobalBaseState newState = copy;

        // pageState 屬性和 appState 屬性不相同,則把 appState 對應(yīng)的屬性賦值給 newState
        if (p.themeColor != appState.themeColor) {
          newState.themeColor = appState.themeColor;
        }

        if (p.localization != appState.localization) {
          newState.localization = appState.localization;
        }

        if (p.fontFamily != appState.fontFamily) {
          newState.fontFamily = appState.fontFamily;
        }

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

      return pageState;
    };

定義好全局 StateStore 之后柱宦,只需要 PageState 繼承 GlobalBaseState 就可以愉快的全局狀態(tài)更新了些椒,例如我們查看 ui/settings 該界面涉及了全局狀態(tài)的修改,state掸刊,action 等可自行查看免糕,我們直接看 view

Widget buildView(SettingsState state, Dispatch dispatch, ViewService viewService) {
  return Theme(
    data: ThemeData(primarySwatch: state.themeColor),
    child: Scaffold(
      appBar: AppBar(
        title: Text(
          FlutterI18n.translate(_ctx, I18nKeys.settings),
          style: TextStyle(fontSize: SpValues.titleTextSize, fontFamily: state.fontFamily),
        ),
      ),
      body: ListView(
        children: <Widget>[
          ExpansionTile(
            leading: Icon(Icons.color_lens),
            title: Text(
              FlutterI18n.translate(_ctx, I18nKeys.themeColor),
              style: TextStyle(fontSize: SpValues.settingTextSize, fontFamily: state.fontFamily),
            ),
            children: List.generate(ResourceConfigs.themeColors.length, (index) {
              return GestureDetector(
                onTap: () {
                  // 發(fā)送對應(yīng)的修改主題色的 action,effect 根據(jù) action 做出相應(yīng)的響應(yīng)策略
                  dispatch(SettingsActionCreator.onChangeThemeColor(index));
                },
                child: Container(
                  margin: EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 4.0),
                  width: _size.width,
                  height: _itemHeight,
                  color: ResourceConfigs.themeColors[index],
                ),
              );
            }),
          ),
          // 省略語言選擇忧侧,字體選擇石窑,邏輯同主題色選擇,具體查看 `setting/view.dart` 文件
        ],
      ),
    ),
  );
}

/// effect
Effect<SettingsState> buildEffect() {
  return combineEffects(<Object, Effect<SettingsState>>{
    SettingsAction.changeThemeColor: _onChangeThemeColor,
  });
}

void _onChangeThemeColor(Action action, Context<SettingsState> ctx) {
  // 通過 GlobalStore dispatch 全局變化的 action蚓炬,全局的 reducer 做出響應(yīng)松逊,并修改主題色
  GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor(ResourceConfigs.themeColors[action.payload]));
}

別的界面也需要做類似的處理,就可以實現(xiàn)全局切換狀態(tài)啦~

一些小坑

在使用 fish_redux 的過程中肯夏,肯定會遇到這樣那樣的坑经宏,這邊簡單列舉幾個遇到的小坑

保持 PageView 子頁面的狀態(tài)

如果不使用 fish_redux 的情況下,PageView 的子頁面我們都需要混入一個 AutomaticKeepAliveClientMixin 來防止頁面重復刷新的問題驯击,但是在 fish_redux 下烁兰,并沒有顯得那么容易,好在官方在 Page 中提供了一個 WidgetWrapper 類型參數(shù)徊都,可以方便解決這個問題沪斟。首先需要定義一個 WidgetWrapper

class KeepAliveWidget extends StatefulWidget {
  final Widget child;

  KeepAliveWidget(this.child);

  @override
  _KeepAliveWidgetState createState() => _KeepAliveWidgetState();
}

class _KeepAliveWidgetState extends State<KeepAliveWidget> with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) { 
    return widget.child;
  }

  @override
  bool get wantKeepAlive => true;
}

Widget keepAliveWrapper(Widget child) => KeepAliveWidget(child);

定義完成后,在 pagewrapper 屬性設(shè)置為 keepAliveWrapper 即可暇矫。

PageView 子頁面實現(xiàn)全局狀態(tài)

我們在前面提到了實現(xiàn)全局狀態(tài)的方案主之,通過設(shè)置 PageRoutresvisitor 屬性實現(xiàn),但是設(shè)置完成后李根,發(fā)現(xiàn) PageView 的子頁面不會跟隨修改槽奕,官方也沒有給出原因,那么如何解決呢朱巨,其實也很方便史翘,我們定義了全局的 globalUpdate 方法,在 Page 的構(gòu)造中,connectExtraStore 下就可以解決啦

class HomeArticlePage extends Page<HomeArticleState, Map<String, dynamic>> {
  HomeArticlePage()
      : super(
          initState: initState,
          effect: buildEffect(),
          reducer: buildReducer(),
          view: buildView,
          dependencies: Dependencies<HomeArticleState>(
            adapter: null,
            slots: <String, Dependent<HomeArticleState>>{},
          ),
          wrapper: keepAliveWrapper, // 實現(xiàn) `PageView` 子頁面狀態(tài)保持琼讽,不重復刷新
        ) {
    // 實現(xiàn) `PageView` 子頁面的全局狀態(tài)
    connectExtraStore<GlobalState>(GlobalStore.store, globalUpdate()); 
  }
}
如何實現(xiàn) Dialog 等提示

flutter 中必峰,Dialog 等也屬于組件,所以钻蹬,通過 component 來定義一個 dialog 再合適不過了吼蚁,比如我們 dispatch 一個 action 需要顯示一個 dialog,那么可以通過如下步驟進行實現(xiàn)

  1. 定義一個 dialog component

    class DescriptionDialogComponent extends Component<DescriptionDialogState> {
      DescriptionDialogComponent()
          : super(
              effect: buildEffect(),
              view: buildView,
            );
    }
    
    /// view
    Widget buildView(DescriptionDialogState state, Dispatch dispatch, ViewService viewService) {
      var _ctx = viewService.context;
    
      return AlertDialog(
        title: Text(FlutterI18n.translate(_ctx, I18nKeys.operatorDescTitle)),
        content: Text(FlutterI18n.translate(_ctx, I18nKeys.operatorDescContent)),
        actions: <Widget>[
          FlatButton(
            onPressed: () {
              dispatch(DescriptionDialogActionCreator.onClose());
            },
            child: Text(
              FlutterI18n.translate(_ctx, I18nKeys.dialogPositiveGet),
            ),
          )
        ],
      );
    }
    
    /// effect
    Effect<DescriptionDialogState> buildEffect() {
      return combineEffects(<Object, Effect<DescriptionDialogState>>{
        DescriptionDialogAction.close: _onClose,
      });
    }
    
    void _onClose(Action action, Context<DescriptionDialogState> ctx) {
      Navigator.of(ctx.context).pop();
    }
    
    // action问欠,state 省略肝匆,具體可以查看 `home\drawer_component\description_component` 
    
  2. 在需要展示 dialogpage 或者 component 注冊 slots

  3. 在對應(yīng)的 effect 調(diào)用 showDialog,通過 Context.buildComponent 生成對應(yīng)的 dialog view

    void _onDescription(Action action, Context<SettingItemState> ctx) {
      showDialog(
        barrierDismissible: false,
        context: ctx.context,
        // ctx.buildComponent('componentName') 會生成對應(yīng)的 widget
        builder: (context) => ctx.buildComponent('desc'), // desc 為注冊 dialog 的 slotName
      );
    }
    

目前遇到的坑都在這顺献,如果大家在使用過程中遇到別的坑旗国,可以放評論一起討論,或者查找 fis_reduxissue注整,很多時候都可以找到滿意的解決方案能曾。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市肿轨,隨后出現(xiàn)的幾起案子寿冕,更是在濱河造成了極大的恐慌,老刑警劉巖椒袍,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驼唱,死亡現(xiàn)場離奇詭異,居然都是意外死亡驹暑,警方通過查閱死者的電腦和手機玫恳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來岗钩,“玉大人纽窟,你說我怎么就攤上這事肖油〖嫦牛” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵森枪,是天一觀的道長视搏。 經(jīng)常有香客問我,道長县袱,這世上最難降的妖魔是什么浑娜? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮式散,結(jié)果婚禮上筋遭,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好漓滔,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布编饺。 她就那樣靜靜地躺著,像睡著了一般响驴。 火紅的嫁衣襯著肌膚如雪透且。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天豁鲤,我揣著相機與錄音秽誊,去河邊找鬼。 笑死琳骡,一個胖子當著我的面吹牛锅论,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播楣号,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼棍厌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了竖席?” 一聲冷哼從身側(cè)響起耘纱,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎毕荐,沒想到半個月后束析,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡憎亚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年员寇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片第美。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡蝶锋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出什往,到底是詐尸還是另有隱情扳缕,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布别威,位于F島的核電站躯舔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏省古。R本人自食惡果不足惜粥庄,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望豺妓。 院中可真熱鬧惜互,春花似錦布讹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蔫慧,卻和暖如春挠乳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背姑躲。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工睡扬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人黍析。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓卖怜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親阐枣。 傳聞我的和親對象是個殘疾皇子马靠,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344