fish_redux 「食用指南」

kuky_xs發表於2020-02-21

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

fish_redux 的介紹就不在這廢話了,需要的小夥伴可以直接檢視 fish_redux 官方文件,這裡我們直接通過例子來踩坑。

專案的大概結構如下所示,具體可以檢視 倉庫程式碼

fish_redux 「食用指南」

可以看到 UI 包下充斥著許多的 actioneffectreducerstateviewpagecomponentadapter 類,不要慌,接下來大概的會說明下每個類的職責。

fish_redux 的分工合作

  1. action 是用來定義一些操作的宣告,其內部包含一個列舉類 XxxAction 和 宣告類 XxxActionCreator,列舉類用來定義一個操作,ActionCreator 用來定義一個 Action,通過 dispatcher 傳送對應 Action 就可以實現一個操作。例如我們需要開啟一個行的頁面,可以如下進行定義

    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 一份出來,然後把新的值賦值上去,例如有個網路請求,發生了資料的變化,可通過如下方式實現

    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 展示效果

  6. pagecomponent 就是上述的載體,用來將資料和 UI 整合到一起

  7. adapter 用來整合列表檢視

Show the code

這邊要實現的例子大概長下面的樣子,一個 Drawer 列表,實現主題色,語言,字型的切換功能,當然後期會增加別的功能,目前先看這部分[home 模組],基本上涵蓋了上述所有的內容。在寫程式碼之前,可以先安裝下 FishRedux 外掛,可以快速構建類,直接在外掛市場搜尋即可

fish_redux 「食用指南」

整體配置
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,很多時候都可以找到滿意的解決方案。

相關文章