Flutter之fish redux状态管理浅析

什么是Flutter?

Flutter 是来自谷歌的一个出色的跨平台框架,可用来为移动、桌面和 Web 平台构建应用程序。
Flutter之fish redux状态管理浅析_第1张图片   目前,Flutter在移动端的应用比较成熟,阿里的咸鱼、美团、头条等顶级互联网公司都有实践应用,虽然生态资源方面不如React Native,但发展势头非常迅猛,流畅的性能体验,以及苹果、安卓代码的一致性方面都令人印象深刻。
  虽然Web端和桌面端的跨平台应用仍未达到生产环境下的使用,但还是相当值得期待的。

什么是redux?

通俗地说,redux可以理解为基于数据驱动交互界面的一种新的开发模式,特别适合Web端和移动端的应用开发。对于习惯于CS架构的开发人员来说,首次接触redux的概念特别不适应,CS下的界面控件赋值非常简单,给某个控件的属性赋值,界面就会立即做出相应的改变。如果使用redux修改界面的数据,则首先通过Reducer方法给数据池中的某个数据赋值,然后底层逻辑触发界面的刷新操作,从而使得界面的显示结果发生改变。
Flutter之fish redux状态管理浅析_第2张图片  上图演示了redux的大致工作原理,图中并未使用redux的专业术语,方便初次接触的程序员更容易理解一些。根据上图,可以总结出关于redux开发的一些重要特征:

  1. 界面、数据与业务逻辑是分离的,这种分离在复杂的业务逻辑实现时,会带来很多好处:代码结构清晰、便于升级、便于维护、开发效率更高;
  2. 界面的显示方式、布局与内容是被数据驱动的,通过状态数据来控制界面改如何显示与布局,这意味着界面设计是动态的,非常灵活;
    Flutter之fish redux状态管理浅析_第3张图片上图演示了同一个页面源码,在数据驱动下,其动态页面布局与显示的效果。
  3. 由于状态数据与内容数据都是可以保存的,那么记录每一个用户动作对应的数据视图后,完全可以通过数据回放的方式模拟用户操作的每一个动作,对代码调试与分析用户行为具有重大意义;
  4. 状态数据统一保存,可以避免多处保存时状态维护的混乱。

如果还有其它重要特性没有写出来,请大家指正啊!

什么是fish redux?

fish redux是阿里旗下闲鱼团队开源的一种redux框架,基本理念与其它状态管理框架大同小异,例如:scoped_model、provide、RxDart、Bloc等都是非常出色的状态管理框架。网上看资料说,fish redux特别适用于构建中大型的复杂应用,至于为什么不是很理解。尝试参考官方的示例程序,发现需要学习消化不少新的概念,尝试建立简单的应用逻辑后,感觉确实复杂,想过放弃,在学习他人使用provide框架过程中,发现实现逻辑虽然简单,但功能不够强大,似乎只支持全局数据池,不支持页面级别的数据池,在对列表数据项的渲染逻辑中,无法做到部分数据项内容或状态的局部刷新显示,我想这就是fish redux性能更加强大的体现之一吧。不得已,继续回到fish redux框架的学习中吧。

fish redux基本概念介绍

以下的一些基本概念是在开发《信息导航》APP过程中,逐步消化整理出来的,更加高级的概念由于没有使用机会,也未深入学习和了解。下述这些基本概念应该够用了。
Flutter之fish redux状态管理浅析_第4张图片Flutter之fish redux状态管理浅析_第5张图片  上面两个截图分别展示了fish redux基本概念原理图以及对应的页面程序结构。下面分别介绍框架的一些基本概念:

  • page state
    page state用于保存页面内部的状态数据与内容数据,不作用于应用程序的全局,页面关闭,里面的数据也相应被销毁。对应上述截图中的“home_state.dart”文件。
  • global state
    global state用于保存应用程序全局的状态数据与内容数据,可以被所有的页面访问和修改,对应于"global_store/global_state.dart"文件。
  • clone
    为了提高数据刷新界面效率,原则上只有数据发生变化时才触发相应局部界面的刷新。那么如何判断保存数据的state对象发生变化了呢?使用oldState == newState基于对象地址是否发生变化进行快速判断,不必递归遍历对象的每个数据属性。当数据发生变化时,首先将原有的state对象clone一份(原始数据都复制一遍)为newState对象,将变化的数据赋值到新对象的相应数据属性中并返回,这就是一个reducer方法实现原理。如果数据没有变化,则只返回原始state对象,不会触发界面的刷新。clone方法在state中实现,因为state必须继承于Cloneable的泛型类。
  @override
  IntroState clone() {
    return IntroState()
      ..checkValue = checkValue
      ..currentTheme = currentTheme
      ..userInfo = userInfo
      ..appConfig = appConfig
      ..searchMode = searchMode
      ..sourceType = sourceType
      ..contentType = contentType;
  }
  • action
    action是effect方法和reducer方法的统一称呼,所有的effect方法和reducer方法的原始定义都放在xxx_action.dart文件中。
class IntroActionCreator {
  static Action onStartupApp(bool checkValue) {
    return Action(IntroActionEnum.onStartupApp, payload: checkValue);
  }

  static Action onPressUserContractLink() {
    return Action(IntroActionEnum.onPressUserContractLink);
  }

  static Action onPressPrivateContractLink() {
    return Action(IntroActionEnum.onPressPrivateContractLink);
  }
}
  • effect
    effect是Side Effect简称,翻译中文为旁作用或副作用,感觉太别扭了,我还是粗暴地把它理解为事件方法吧,关于界面的初始化事件、销毁事件、点击事件、滑动事件、定时器事件等等的实现都放在xxx_effect.dart文件中。effect方法可以认为处于程序结构的枢纽地位:与界面对接、与reducer对接、与后台服务对接、与手机设备对接、与手机系统对接等等,所有的业务逻辑实现都应该放在effect方法中。命名时建议以on为前缀,表示对事件的响应,方便代码阅读。
Future _onRefreshPage(Action action, Context<InfoNavPageState> ctx) async {
  GlobalStore.store.dispatch(GlobalReducerCreator.setErrorStatusReducer(false));
  if (_isLoading(ctx.state)) return;
  await _showInfoEntitiesBySourceType(ctx, action);
  await _checkLatestMessage(ctx);
}
  • reducer
    reducer是实现修改page state数据或global state数据的方法,放在xxx_reducer.dart文件中。reducer方法传入需要修改的数据内容以及原始的state对象,如果发现新的数据与原始数据对比并未改变,则直接返回原始对象,不触发界面刷新。如果数据发生变化,则根据原始state对象clone一份新的对象实例,将变化的数据对象保存到新的state对象并返回,触发界面刷新。命名建议以set、change、modify等更改性质单词作为前缀,表示修改状态数据或内容数据。
InfoNavPageState _setAutoSearchReducer(InfoNavPageState state, Action action) {
  final autoSearch = action.payload;
  if (autoSearch == state.autoSearch) return state;
  final newState = state.clone()..autoSearch = autoSearch;
  return newState;
}
  • view
    view是界面定义的入口,放在xxx_view.dart文件中。界面每个组件的事件方法中,通过dispatch或broadcast来调用相应的effect方法,并通过effect方法间接调用相应的reducer方法,此时操作界面时所关联的数据或状态作为参数同步传入。很多程序员一开始就被dart语言的层层嵌套地狱般的代码所吓倒,其实完全没有必要,通过组件按逻辑实现方法重构,可以使flutter界面定义代码变得非常简练而清晰。
Widget buildView(
    InfoNavPageState state, Dispatch dispatch, ViewService viewService) {
  var title = _getInfoNavTitle(state);
  if (state.filterKeywords != null && state.filterKeywords != '') {
    title = "组合条件:${state.filterKeywords}";
  }
  final searchMode = GlobalStore.searchMode;
  final showShortIcon =
      !searchMode && !state.hasParagraph() && !state.hasEntityKeyword();
  return Scaffold(
    appBar: AppBar(
      automaticallyImplyLeading: false,
      title: searchMode
          ? _buildInfoNavSearchBar(state, dispatch, viewService)
          : _buildInfoNavTitle(state, title),
      actions: [
        _buildAppBarSearchIcon(state, dispatch, viewService),
        showShortIcon
            ? _buildAppBarShortIcon(state, dispatch, viewService)
            : SizedBox(),
      ],
    ),
    body: Container(
        alignment: Alignment.center, // 用于错误提示显示的居中
        child: _buildInfoNavContent(state, dispatch, viewService)),
  );
}
  • page
    fish redux框架一个重要的特点就是在程序结构上强制将不同功能的代码进行分离,action、state、effect、reducer、view的代码分别保存在不同的文件中,而page就是上述模块代码总装集成之处,放在xxx_page.dart文件中。这种强制的隔离结构非常适合复杂业务的应用,程序结构清晰,能大幅减轻程序员的记忆负担。对于简单的业务而言,这种结构过于复杂和啰嗦,但是仍然建议大家使用这种分离的程序结构,这也是我硬着头皮选择fish redux的最重要的原因,习惯之后会变得很轻松。
class HomePage extends Page<HomeState, Map<String, dynamic>> {
  /// effect定义了当前页面的操作行为集合
  /// reducer定义了当前页面的数据修改行为集合
  HomePage()
      : super(
          initState: initState,
          effect: buildEffect(),
          reducer: buildReducer(),
          view: buildView,
          wrapper: keepAliveWrapper,
          dependencies: Dependencies<HomeState>(
              adapter: null, slots: <String, Dependent<HomeState>>{}),
          middleware: <Middleware<HomeState>>[],
        );
}

  • dispatch
    dispatch是只能调用本页面内实现的所有effect方法和reducer方法,不支持跨页调用,即在本页的代码实现逻辑中调用其它页面定义的effect方法和reducer方法。
var params = <String, dynamic>{
  ParamNames.lastTimeParam: state.lastPopTime,
  ParamNames.keywordSwitchParam: state.iconQuarterTurns,
};
dispatch(HomeActionCreator.onProcessBackKey(params));
  • broadcast
    broadcast可以实现跨页调用其它页面的effect方法,但是不能跨页调用reducer方法,间接实现思路是,首先跨页调用effect方法,然后在该effect方法中再调用该页的reducer方法。
ctx.broadcast(InfoNavPageActionCreator.onToggleKeywordNav(true));
  • component
    component是一组widget集合,可以看成一个子页面定义,拥有独立action、state、effect、reducer、view等定义,组件与page之间的数据传递需要特殊的逻辑实现,具体请参考fish redux官方示例代码,本应用未实践该应用。好处是,可以将一个复杂的页面进一步细化分割,能够实现界面数据变化的局部刷新逻辑,这是fish redux性能优化的理念之一。
  • adapter
    component无法优化列表数据项的定义,对于所有列表数据项的显示与局部刷新则引入adapter概念,具体介绍请看后续文章《数据列表的状态管理实践》。
  • slot
    slot从字面意思看就是插座、插槽,在fish redux框架中就是集成页面组件的入口,具体参考官方示例代码。

上述的基本概念请参照源码来逐步理解,如有问题请留言共同探讨。
后续文章将继续介绍《数据列表的状态管理实践》以及《APP开发过程中的性能优化理念》,敬请期待。
源码地址

你可能感兴趣的:(移动端状态管理)