fish_redux 「食用指南」

很久沒更新文章了,最近趁着娃睡覺的功夫,嘗試了下 fish_redux,這邊作下記錄,安全無毒,小夥伴們可放心食用(本文基於版本 fish_redux 0.3.1)。android

fish_redux 的介紹就不在這廢話了,須要的小夥伴能夠直接查看 fish_redux 官方文檔,這裏咱們直接經過例子來踩坑。git

項目的大概結構以下所示,具體能夠查看 倉庫代碼github

能夠看到 UI 包下充斥着許多的 actioneffectreducerstateviewpagecomponentadapter 類,不要慌,接下來大概的會說明下每一個類的職責。redux

fish_redux 的分工合做

  1. action 是用來定義一些操做的聲明,其內部包含一個枚舉類 XxxAction 和 聲明類 XxxActionCreator,枚舉類用來定義一個操做,ActionCreator 用來定義一個 Action,經過 dispatcher 發送對應 Action 就能夠實現一個操做。例如咱們須要打開一個行的頁面,能夠以下進行定義api

    enum ExamAction { openNewPage, openNewPageWithParams }
    
    class ExamActionCreator {
        static Action onOpenNewPage(){
            // Action 能夠傳入一個 payload,例如咱們須要攜帶參數跳轉界面,則能夠經過 payload 傳遞
            // 而後在 effect 或者 reducer 層經過 action.payload 獲取
            return const Action(ExamAction.openNewPage);
        }
        
        static Action onOpenNewPageWithParams(String str){
            return Action(ExamAction.openNewPageWithParams, payload: str);
        }
    }
    複製代碼
  2. effect 用來定義一些反作用的操做,例如網絡請求,頁面跳轉等,經過 buildEffect 方法結合 Action 和最終要實現的反作用,例如仍是打開頁面的操做,可經過以下方式實現安全

    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 用來定義數據發生變化的操做,好比網絡請求後,數據發生了變化,則把原先的數據 clone 一份出來,而後把新的值賦值上去,例若有個網絡請求,發生了數據的變化,可經過以下方式實現markdown

    Reducer<ExamState> buildReducer() {
      return asReducer(
        <Object, Reducer<ExamState>>{
          HomeAction.onDataRequest: _onDataRequest,
        },
      );
    }
    
    ExamState _onDataRequest(ExamState state, Action action) {
      // data 的數據經過 action 的 payload 進行傳遞,reducer 只負責數據刷新
      return state.clone()..data = action.payload;
    }
    複製代碼
  4. state 就是當前頁面須要展現的一些數據網絡

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

  6. pagecomponent 就是上述的載體,用來將數據和 UI 整合到一塊兒async

  7. adapter 用來整合列表視圖

Show the code

這邊要實現的例子大概長下面的樣子,一個 Drawer 列表,實現主題色,語言,字體的切換功能,固然後期會增長別的功能,目前先看這部分[home 模塊],基本上涵蓋了上述全部的內容。在寫代碼以前,能夠先安裝下 FishRedux 插件,能夠快速構建類,直接在插件市場搜索便可

總體配置
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 總體構建

Home 頁面總體就是一個帶 Drawer,主體是一個 PageView,頂部帶一個 banner 控件,banner 的數據咱們經過網絡進行獲取,在 Drawer 是一個點擊列表,包括圖標,文字和動做,那麼咱們能夠建立一個 DrawerSettingItem 類,用了建立列表,頭部的用戶信息目前能夠先寫死。因此咱們能夠先搭建 HomeState

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

  @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 數據
    return Action(HomeAction.fetchBanner, payload: banner);
  }

  static Action onLoadSettings(List<SettingItemState> settings) { // 加載 setting 數據
    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);
  }
}
複製代碼
構建 banner

爲了增強頁面的複用性,能夠經過 component 進行模塊構建,具體查看 banner_component 包下文件。首先定義 state,由於 banner 做爲 home 下的內容,因此其 state 不能包含 HomeState 外部的屬性,所以定義以下

class HomeBannerState implements Cloneable<HomeBannerState> {
  List<HomeBannerDetail> banners; // banner 數據列表

  @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);
  }
}
複製代碼

因爲不涉及到數據的改變,因此能夠不須要定義 reducer,經過 effect 來處理 openBannerDetail 便可

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

void _onOpenBannerDetail(Action action, Context<HomeBannerState> ctx) {
  // payload 中攜帶了 bannerUrl 參數,用來打開對應的網址
  // 可查看 [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, // 設置固定高度
    child: state.banners == null || state.banners.isEmpty
        ? SizedBox()
        : Swiper( // 當有數據存在時,才顯示 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 對應的 Action,當 effect 或者 reduce 收到會進行對應處理
                  dispatch(HomeBannerActionCreator.onOpenBannerDetail(state.banners[index].url));
                },
              );
            },
          ),
  );
}
複製代碼

最後再回到 component,這個類插件已經定義好了,基本上不須要作啥修改

class HomeBannerComponent extends Component<HomeBannerState> {
  HomeBannerComponent()
      : super(
          effect: buildEffect(), // 對應 effect 的方法
          reducer: buildReducer(), // 對應 reducer 的方法
          view: buildView, // 對應 view 的方法
          dependencies: Dependencies<HomeBannerState>(
            adapter: null, // 用於展現數據列表
            // 組件插槽,註冊後可經過 viewService.buildComponent 方法生成對應組件
            slots: <String, Dependent<HomeBannerState>>{},
          ),
        );
}
複製代碼

這樣就定義好了一個 component,能夠經過註冊 slot 方法使用該 component

使用 banner component

在上一步,咱們已經定義好了 banner component,這裏就能夠經過 slot 愉快的進行使用了,首先,須要定義一個 connectorconnector 是用來鏈接兩個父子 state 的橋樑。

// connector 須要繼承 ConnOp 類,並混入 ReselectMixin,泛型分別爲父級 state 和 子級 state
class HomeBannerConnector extends ConnOp<HomeState, HomeBannerState> with ReselectMixin {
  @override
  HomeBannerState computed(HomeState state) {
    // computed 用於父級 state 向子級 state 數據的轉換
    return HomeBannerState()..banners = state.banners;
  }

  @override
  List factors(HomeState state) {
    // factors 爲轉換的因子,返回全部改變的因子便可
    return state.banners ?? [];
  }
}
複製代碼
Page 中註冊 slot

page 的結構和 component 的結構是同樣的,使用 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(), // 定義側滑組件,方式同 banner
            },
          ),
          middleware: <Middleware<HomeState>>[],
        );
}
複製代碼

註冊完成 slot 以後,就能夠直接在 view 上使用了,使用的方法也很簡單

Widget buildView(HomeState state, Dispatch dispatch, ViewService viewService) {
  var _pageChildren = <Widget>[
    // page 轉換成 widget 經過 buildPage 實現,參數表示要傳遞的參數,無需傳遞則爲 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 數據

在前面的 HomeActionCreator 中,咱們定義了 onFetchBanner 這個 Action,須要傳入一個 banner 列表做爲參數,因此更新數據能夠這麼進行操做

Effect<HomeState> buildEffect() {
  return combineEffects(<Object, Effect<HomeState>>{
    // Lifecycle 的生命週期同 StatefulWidget 對應,因此在初始化的時候處理請求 banner 數據等初始化操做
    Lifecycle.initState: _onPageInit, 
  });
}

void _onPageInit(Action action, Context<HomeState> ctx) async {
  ctx.dispatch(HomeActionCreator.onPageChange(0));
  var banners = await Api().fetchHomeBanner(); // 網絡請求,具體的能夠查看 `api.dart` 文件
  ctx.dispatch(HomeActionCreator.onFetchBanner(banners)); // 經過 dispatch 發送 Action
}
複製代碼

一開始咱們提到過,effect 只負責一些反作用的操做,reducer 負責數據的修改操做,因此在 reducer 須要作數據的刷新

Reducer<HomeState> buildReducer() {
  return asReducer(
    <Object, Reducer<HomeState>>{
      // 當 dispatch 發送了對應的 Action 的時候,就會調用對應方法
      HomeAction.fetchBanner: _onFetchBanner, 
    },
  );
}

HomeState _onFetchBanner(HomeState state, Action action) {
  // reducer 修改數據方式是先 clone 一份數據,而後進行賦值
  // 這樣就把網絡請求返回的數據更新到 view 層了
  return state.clone()..banners = action.payload; 
}
複製代碼

經過上述操做,就將網絡的 banner 數據加載到 UI

使用 adapter 構建 drawer 功能列表

drawer 由一個頭部和列表構成,頭部能夠經過 component 進行構建,方法相似上述 banner componentdrawer component,惟一區別就是一個在 pageslots 註冊,一個在 componentslots 註冊。因此構建 drawer 就是須要去構建一個列表,這裏就須要用到 adapter 來處理了。

在老的版本中(本文版本 0.3.1),構建 adapter 通常經過 DynamicFlowAdapter 實現,並且在插件中也能夠發現,可是在該版本下,DynamicFlowAdapter 已經被標記爲過期,而且官方推薦使用 SourceFlowAdapterSourceFlowAdapter 須要指定一個 State,而且該 State 必須繼承自 AdapterSourceAdapterSource 有兩個子類,分別是可變數據源的 MutableSource 和不可變數據源的 ImmutableSource,二者的差異由於官方也沒有給出具體的說明,本文使用 MutableSource 來處理 adapter。因此對應的 state 定義以下

class HomeDrawerState extends MutableSource implements Cloneable<HomeDrawerState> {
 List<SettingItemState> settings; // state 爲列表 item component 對應的 state

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

  @override
  Object getItemData(int index) => settings[index]; // 對應 index 下的數據

  @override
  String getItemType(int index) => DrawerSettingAdapter.settingType; // 對應 index 下的數據類型

  @override
  int get itemCount => settings?.length ?? 0; // 數據源長度

  @override
  void setItemData(int index, Object data) => settings[index] = data; // 對應 index 下的數據如何修改
}
複製代碼

一樣,adapter 也能夠以下進行定義

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

  DrawerSettingAdapter()
      : super(pool: <String, Component<Object>>{
          // 不一樣數據類型,對應的 component 組件,type 和 state getItemType 方法對應
          // 容許多種 type
          settingType: SettingItemComponent(), 
        });
}
複製代碼

通過上述兩部分,就定義好了 adapter 的主體部分啦,接着就是要實現 SettingItemComponent 這個組件,只須要簡單的 ListTile 便可,ListTile 的展現內容經過對應的 state 來設置

/// 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),
  );
}
複製代碼

由於不涉及數據的修改,因此不須要定義 reducer,點擊實現經過 effect 實現便可,具體的代碼可查看對應文件,這邊不貼多餘代碼了.

通過上述步驟,adapter 就定義完成了,接下來就是要使用對應的 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(),
            },
          ),
        );
}

/// 對應 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,
          ),
        )
      ],
    ),
  );
}
複製代碼

將列表設置到界面後,就剩下最後的數據源了,數據從哪來呢,答案固然是和 banner component 同樣,經過上層獲取,這邊不須要經過網絡獲取,直接在本地定義就好了,具體的獲取查看文件 home\effect.dart 下的 _loadSettingItems 方法,實現和獲取 banner 數據無多大差異,除了一個本地加載,一個網絡獲取。

fish_redux 實現全局狀態

fish_redux 全局狀態的實現,咱們參考 官方 demo,首先構造一個 GlobalBaseState 抽象類(涉及到全局狀態變化的 state 都須要繼承該類),這個類定義了全局變化的狀態屬性,例如咱們該例中須要實現全局的主題色,語言和字體的改變,那麼咱們就能夠以下定義

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 並實現 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 來存儲狀態值

class GlobalStore {
  // Store 用來存儲全局狀態 GlobalState,當刷新狀態值的時候,經過
  // store 的 dispatch 發送相關的 action 便可作出相應的調整
  static Store<GlobalState> _globalStore; 

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

/// 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 參數,還有個 visitor 參數沒有使用,這個就是用來刷新全局狀態的。

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() 就是具體的實現邏輯
          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 對應的屬性賦值給 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 並將數據設置到 ui
      }

      return pageState;
    };
複製代碼

定義好全局 StateStore 以後,只須要 PageState 繼承 GlobalBaseState 就能夠愉快的全局狀態更新了,例如咱們查看 ui/settings 該界面涉及了全局狀態的修改,stateaction 等可自行查看,咱們直接看 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: () {
                  // 發送對應的修改主題色的 action,effect 根據 action 作出相應的響應策略
                  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 作出響應,並修改主題色
  GlobalStore.store.dispatch(GlobalActionCreator.onChangeThemeColor(ResourceConfigs.themeColors[action.payload]));
}
複製代碼

別的界面也須要作相似的處理,就能夠實現全局切換狀態啦~

一些小坑

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

保持 PageView 子頁面的狀態

若是不使用 fish_redux 的狀況下,PageView 的子頁面咱們都須要混入一個 AutomaticKeepAliveClientMixin 來防止頁面重複刷新的問題,可是在 fish_redux 下,並無顯得那麼容易,好在官方在 Page 中提供了一個 WidgetWrapper 類型參數,能夠方便解決這個問題。首先須要定義一個 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 屬性設置爲 keepAliveWrapper 便可。

PageView 子頁面實現全局狀態

咱們在前面提到了實現全局狀態的方案,經過設置 PageRoutresvisitor 屬性實現,可是設置完成後,發現 PageView 的子頁面不會跟隨修改,官方也沒有給出緣由,那麼如何解決呢,其實也很方便,咱們定義了全局的 globalUpdate 方法,在 Page 的構造中,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, // 實現 `PageView` 子頁面狀態保持,不重複刷新
        ) {
	// 實現 `PageView` 子頁面的全局狀態
    connectExtraStore<GlobalState>(GlobalStore.store, globalUpdate()); 
  }
}
複製代碼
如何實現 Dialog 等提示

flutter 中,Dialog 等也屬於組件,因此,經過 component 來定義一個 dialog 再合適不過了,好比咱們 dispatch 一個 action 須要顯示一個 dialog,那麼能夠經過以下步驟進行實現

  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. 在對應的 effect 調用 showDialog,經過 Context.buildComponent 生成對應的 dialog view

    void _onDescription(Action action, Context<SettingItemState> ctx) {
      showDialog(
        barrierDismissible: false,
        context: ctx.context,
        // ctx.buildComponent('componentName') 會生成對應的 widget
        builder: (context) => ctx.buildComponent('desc'), // desc 爲註冊 dialog 的 slotName
      );
    }
    複製代碼

目前遇到的坑都在這,若是你們在使用過程當中遇到別的坑,能夠放評論一塊兒討論,或者查找 fis_reduxissue,不少時候均可以找到滿意的解決方案。

相關文章
相關標籤/搜索