fish_redux 「食用指南」

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

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 插件,可以快速構建類,直接在插件市場搜索即可

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380