fish_redux 「食用指南」

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

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

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

可以看到 UI 包下充斥著許多的 action,effect,reducer,state,view,page,component,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

為了加強頁面的復(fù)用性,可以通過 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 必須繼承自 AdapterSourceAdapterSource 有兩個子類,分別是可變數(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 來防止頁面重復(fù)刷新的問題,但是在 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)保持,不重復(fù)刷新
        ) {
    // 實現(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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容