天天看點

Flutter -- fish_redux 「食用指南」

fish_redux

的介紹就不在這廢話了,需要的小夥伴可以直接檢視

fish_redux

官方文檔

,這裡我們直接通過例子來踩坑。

項目的大概結構如下所示,具體可以檢視

倉庫代碼

可以看到

UI

包下充斥着許多的

action

effect

reducer

state

view

page

component

adapter

類,不要慌,接下來大概的會說明下每個類的職責。

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. page

    component

    就是上述的載體,用來将資料和

    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

愉快的進行使用了,首先,需要定義一個

connector

connector

是用來連接配接兩個父子

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 component

drawer component

,唯一差別就是一個在

page

slots

注冊,一個在

component

slots

注冊。是以建構

drawer

就是需要去建構一個清單,這裡就需要用到

adapter

來處理了。

在老的版本中(本文版本 0.3.1),建構

adapter

一般通過

DynamicFlowAdapter

實作,而且在插件中也可以發現,但是在該版本下,

DynamicFlowAdapter

已經被标記為過時,并且官方推薦使用

SourceFlowAdapter

SourceFlowAdapter

需要指定一個

State

,并且該

State

必須繼承自

AdapterSource

AdapterSource

有兩個子類,分别是可變資料源的

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;
}           

定義完全局

State

Store

後,回到我們的

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;
    };           

定義好全局

State

Store

之後,隻需要

PageState

繼承

GlobalBaseState

就可以愉快的全局狀态更新了,例如我們檢視

ui/settings

該界面涉及了全局狀态的修改,

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: () {
                  // 發送對應的修改主題色的 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);           

定義完成後,在

page

wrapper

屬性設定為

keepAliveWrapper

即可。

PageView

子頁面實作全局狀态

我們在前面提到了實作全局狀态的方案,通過設定

PageRoutres

visitor

屬性實作,但是設定完成後,發現

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. 在需要展示

    dialog

    page

    或者

    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_redux

issue

,很多時候都可以找到滿意的解決方案。