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阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348