立即注册找回密码

QQ登录

只需一步,快速开始

微信登录

微信扫一扫,快速登录

手机动态码快速登录

手机号快速注册登录

搜索

图文播报

查看: 174|回复: 5

[分享] 如何评价阿里开源的Flutter应用框架Fish Redux?

[复制链接]
发表于 2025-5-25 10:54 | 显示全部楼层 |阅读模式
回复

使用道具 举报

发表于 2025-5-25 10:54 | 显示全部楼层
为啥不用GetX,不比这个香?
回复 支持 反对

使用道具 举报

发表于 2025-5-25 10:55 | 显示全部楼层
大便 这种不持续维护,只出一会的项目纯kpi的。文档都写不明白。
所有阿里系开源一个吊样,只管开源不管维护。
回复 支持 反对

使用道具 举报

发表于 2025-5-25 10:56 | 显示全部楼层
没啥想法,项目是好项目,就是咸鱼已经放弃它了吧。Flutter 2.0升级的NULL SAFETY,现在Flutter都3.7.1了,Fish Redux 还不支持Null Safety
项目都从Fish redux 迁移到GetX了
回复 支持 反对

使用道具 举报

发表于 2025-5-25 10:56 | 显示全部楼层
Fish Redux是闲鱼团队出品的,基于flutter_redux深入定制开发的Flutter应用框架。优点是一整套的完整开发框架,性能方面也有优化。缺点是比较重,对于中小应用上手成本高,维护成本也不低。对于中大型应用会挺不错。国外也有一些不错的框架,如Stacked。
前言

对于非顶级的 Store,我们测试的时候会发现一个有趣的现象,那就是 StoreConnector 构建的 Widget 在状态发生改变的时候,并不会重建整个子组件,而是只更新依赖于 converter 转换后对象的组件。这说明 StoreConnector 能够精准地定位到哪个子组件依赖状态变量,从而实现精准刷新,提高效率。这和 Provider 的 select 方法类似。 本篇我们就来分析一下 StoreConnector 的源码,看一下是如何实现精准刷新的。
验证

我们先看一个示例,来验证一下我们上面的说法,话不多说,先看测试代码。我们定义了两个按钮,一个点赞,一个收藏,每次点击调度对应的 Action 使得对应的数量加1。两个按钮的实现基本类似,只是依赖状态的数据不同。
class DynamicDetailWrapper extends StatelessWidget {
  final store = Store<PartialRefreshState>(
    partialRefreshReducer,
    initialState: PartialRefreshState(favorCount: 0, praiseCount: 0),
  );
  DynamicDetailWrapper({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('build');
    return StoreProvider<PartialRefreshState>(
        store: store,
        child: Scaffold(
          appBar: AppBar(
            title: Text('局部 Store'),
          ),
          body: Stack(
            children: [
              Container(height: 300, color: Colors.red),
              Positioned(
                  bottom: 0,
                  height: 60,
                  width: MediaQuery.of(context).size.width,
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      _PraiseButton(),
                      _FavorButton(),
                    ],
                  ))
            ],
          ),
        ));
  }
}

class _FavorButton extends StatelessWidget {
  const _FavorButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('FavorButton');
    return StoreConnector<PartialRefreshState, int>(
      builder: (context, count) => Container(
        alignment: Alignment.center,
        color: Colors.blue,
        child: TextButton(
          onPressed: () {
            StoreProvider.of<PartialRefreshState>(context)
                .dispatch(FavorAction());
          },
          child: Text(
            '收藏 $count',
            style: TextStyle(color: Colors.white),
          ),
          style: ButtonStyle(
              minimumSize: MaterialStateProperty.resolveWith((states) =>
                  Size((MediaQuery.of(context).size.width / 2), 60))),
        ),
      ),
      converter: (store) => store.state.favorCount,
      distinct: true,
    );
  }
}

class _PraiseButton extends StatelessWidget {
  const _PraiseButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('PraiseButton');
    return StoreConnector<PartialRefreshState, int>(
      builder: (context, count) => Container(
        alignment: Alignment.center,
        color: Colors.green[400],
        child: TextButton(
          onPressed: () {
            StoreProvider.of<PartialRefreshState>(context)
                .dispatch(PraiseAction());
          },
          child: Text(
            '点赞 $count',
            style: TextStyle(color: Colors.white),
          ),
          style: ButtonStyle(
              minimumSize: MaterialStateProperty.resolveWith((states) =>
                  Size((MediaQuery.of(context).size.width / 2), 60))),
        ),
      ),
      converter: (store) => store.state.praiseCount,
      distinct: false,
    );
  }
}按正常的情况,状态更新后应该是整个子组件rebuild,但是实际运行我们发现只有依赖于状态变量的TextButton和其子组件 Text进行了 rebuild。我们在两个按钮的 build 方法打印了对应的信息,然后在 TextButton (build 方法在其父类ButtonStyleButton中)和 Text 组件的 build 中打上断点,来看一下运行效果。


从运行结果看,点击按钮的时候 TextButton 和 Text的 build 方法均被调用了,但是 FavorButton 和 PraiseButton 的 build 方法并没有调用(未打印对应的信息)。这说明 StoreConnector 进行了精准的局部更新。接下来我们从源码看看是怎么回事?
StoreConnector 源码分析

StoreConnector 的源码很简单,本身 StoreConnector 继承自 StatelessWidget,除了定义的构造方法和属性(均为 final)外,就是一个 build 方法,只是 build方法比较特殊,返回的是一个_StoreStreamListener<S, ViewModel>组件。来看看这个组件是怎么回事。
@override
Widget build(BuildContext context) {
  return _StoreStreamListener<S, ViewModel>(
    store: StoreProvider.of<S>(context),
    builder: builder,
    converter: converter,
    distinct: distinct,
    onInit: onInit,
    onDispose: onDispose,
    rebuildOnChange: rebuildOnChange,
    ignoreChange: ignoreChange,
    onWillChange: onWillChange,
    onDidChange: onDidChange,
    onInitialBuild: onInitialBuild,
  );
}_StoreStreamListener是一个StatefulWidget,核心实现在_StoreStreamListenerState<S, ViewModel>中,代码如下所示。
class _StoreStreamListenerState<S, ViewModel>
    extends State<_StoreStreamListener<S, ViewModel>> {
  late Stream<ViewModel> _stream;
  ViewModel? _latestValue;
  ConverterError? _latestError;

  // `_latestValue!` would throw _CastError if `ViewModel` is nullable,
  // therefore `_latestValue as ViewModel` is used.
  // https://dart.dev/null-safety/understanding-null-safety#nullability-and-generics
  ViewModel get _requireLatestValue => _latestValue as ViewModel;

  @override
  void initState() {
    widget.onInit?.call(widget.store);

    _computeLatestValue();

    if (widget.onInitialBuild != null) {
      WidgetsBinding.instance?.addPostFrameCallback((_) {
        widget.onInitialBuild!(_requireLatestValue);
      });
    }

    _createStream();

    super.initState();
  }

  @override
  void dispose() {
    widget.onDispose?.call(widget.store);

    super.dispose();
  }

  @override
  void didUpdateWidget(_StoreStreamListener<S, ViewModel> oldWidget) {
    _computeLatestValue();

    if (widget.store != oldWidget.store) {
      _createStream();
    }

    super.didUpdateWidget(oldWidget);
  }

  void _computeLatestValue() {
    try {
      _latestError = null;
      _latestValue = widget.converter(widget.store);
    } catch (e, s) {
      _latestValue = null;
      _latestError = ConverterError(e, s);
    }
  }

  @override
  Widget build(BuildContext context) {
    return widget.rebuildOnChange
        ? StreamBuilder<ViewModel>(
            stream: _stream,
            builder: (context, snapshot) {
              if (_latestError != null) throw _latestError!;

              return widget.builder(
                context,
                _requireLatestValue,
              );
            },
          )
        : _latestError != null
            ? throw _latestError!
            : widget.builder(context, _requireLatestValue);
  }

  ViewModel _mapConverter(S state) {
    return widget.converter(widget.store);
  }

  bool _whereDistinct(ViewModel vm) {
    if (widget.distinct) {
      return vm != _latestValue;
    }

    return true;
  }

  bool _ignoreChange(S state) {
    if (widget.ignoreChange != null) {
      return !widget.ignoreChange!(widget.store.state);
    }

    return true;
  }

  void _createStream() {
    _stream = widget.store.onChange
        .where(_ignoreChange)
        .map(_mapConverter)
        // Don't use `Stream.distinct` because it cannot capture the initial
        // ViewModel produced by the `converter`.
        .where(_whereDistinct)
        // After each ViewModel is emitted from the Stream, we update the
        // latestValue. Important: This must be done after all other optional
        // transformations, such as ignoreChange.
        .transform(StreamTransformer.fromHandlers(
          handleData: _handleChange,
          handleError: _handleError,
        ));
  }

  void _handleChange(ViewModel vm, EventSink<ViewModel> sink) {
    _latestError = null;
    widget.onWillChange?.call(_latestValue, vm);
    final previousValue = vm;
    _latestValue = vm;

    if (widget.onDidChange != null) {
      WidgetsBinding.instance?.addPostFrameCallback((_) {
        if (mounted) {
          widget.onDidChange!(previousValue, _requireLatestValue);
        }
      });
    }

    sink.add(vm);
  }

  void _handleError(
    Object error,
    StackTrace stackTrace,
    EventSink<ViewModel> sink,
  ) {
    _latestValue = null;
    _latestError = ConverterError(error, stackTrace);
    sink.addError(error, stackTrace);
  }
}关键的设置都在 initState 方法中。在 initState 方法中,如果设置了 onInit 方法,就会将 store 传递过去调用状态的初始化方法,例如下面就是我们在购物清单应用中对 onInit 属性的使用。
onInit: (store) => store.dispatch(ReadOfflineAction()),接下来是调用_computeLatestValue方法,实际是通过converter方法获得转换后的ViewModel对象的值,这个值存储在ViewModel _latestValue属性中。然后是,如果定义了 onInitialBuild 方法,就会使用 ViewModel 的值做初始化构造。
最后调用了_createStream 方法,这个方法很关键!!!实际上就是吧 Store 的onChange 事件按照一定的过滤方式转变了成了Stream<ViewModel>对象,其实相当于是只监听了 Store 中经过 converter 方法转换后那一部分ViewModel 对象的变化——也就是实现了局部监听。处理数据变化的方法为_handleChange。实际上就是将变化后的 ViewModel 加入到流中:
sink.add(vm);因为 build 方法中使用的是 StremaBuilder 组件,并且会监听_stream 对象,因此当状态数据转换后的 ViewModel 对象发生改变时,会触发 build 方法进行重建。而这个方法最终会调用 StoreConnector 中的 builder 属性对应的方法。这部分代码正好是 PraiseButton 或 FavorButton 的下级组件,这就是为什么状态发生变化时 PraiseButton 和 FavorButton不会被重建的原因,因为他们不是StoreConnector 的下级组件,而是上级组件。
也就是说, 使用StoreConnector这种方式时,当状态发生改变后,之后刷新它的下级组件。因此,从性能考虑,我们可以做最小范围的包裹,比如这个例子,我们可以只包裹 Text 组件,这样 Container 和 TextButton 也不会被刷新了。
为了对比,我们只修改了 PraiseButton 的代码,实际打断点发现点击点赞按钮的Container不会被刷新,而TextButton 会刷新,分析发现是TextButton 的外观样式在点击的时候改变导致的,并不是Store状态改变导致。也就是说,通过最小范围使用 StoreConnector 包裹子组件,我们可以将刷新的范围缩到最小,从而最大限度地提升性能。具体代码可以到这里下载(partial_refresh部分):Redux 状态管理代码
class _PraiseButton extends StatelessWidget {
  const _PraiseButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('PraiseButton');
    return Container(
      alignment: Alignment.center,
      color: Colors.green[400],
      child: TextButton(
        onPressed: () {
          StoreProvider.of<PartialRefreshState>(context)
              .dispatch(PraiseAction());
        },
        child: StoreConnector<PartialRefreshState, int>(
          builder: (context, count) => Text(
            '点赞 $count',
            style: TextStyle(color: Colors.white),
          ),
          converter: (store) => store.state.praiseCount,
          distinct: false,
        ),
        style: ButtonStyle(
            minimumSize: MaterialStateProperty.resolveWith(
                (states) => Size((MediaQuery.of(context).size.width / 2), 60))),
      ),
    );
  }
}总结

很多时候我们在使用第三方插件的时候,都是跑跑 demo,然后直接上手就用。确实,这样也能够达到功能实现的目的,但是如果真的遇到性能上面的问题的时候,往往不知所措。因此,对于有些第三方插件,还是有必要保持好奇心,了解其中的实现机制,做到知其然知其所以然
回复 支持 反对

使用道具 举报

发表于 2025-5-25 10:56 | 显示全部楼层
在今年 4 月份的时候,刚好分析过 Flutter 的几种常见的状态管理设计,其中就包含了 Fish-Redux 相关的分析:
如果说 flutter_redux 是按照 redux 思想设计的单向数据流的框架那么 Fish-Redux 就是把 flutter_redux 变得更加细致化,把三个“大积木”的拼装方式变成了更多零散的模块,让各部位的零件能够更大限度地被复用。




相比起常见的 Redux只有Store、Action、Reducer 和 Middleware的设计,Fish-Redux 在这 Redux 的基础上提出了 Comoponent 的概念,这个概念下 Fish-Redux 会从 Context 、Widget 等地方就开始全面“入侵”你的代码 。

一般情况下使用 Fish-Redux 需要如下步骤:

  • 继承 Page 实现我们的页面。
  • 定义好我们的 State 状态。
  • 定义 effect 、 middleware 、reducer 用于实现副作用、中间件、结果返回处理。
  • 定义 view 用于绘制页面。
  • 定义 dependencies 用户装配控件,这里最骚气的莫过于重载了 + 操作符,然后利用 Connector State 挑选出数据,然后通过 Component 绘制。
在使用流程上 Fish-Redux 更复杂了,但是这带来的好处就是 复用的颗粒度更细了,装配和功能更加的清晰,而如下图所示,它的工作逻辑也相对复杂,并且设计理念也是“另辟蹊径”。



简单来说的流程是:

  • 1、Page 的构建需要 State 、Effect 、Reducer 、view 、dependencies 、 middleware 等参数。


  • 2、Page 的内部 PageProvider 是一个 InheritedWidget 用户状态共享。


  • 3、Page 内部会通过 createMixedStore 创建 Store 对象。


  • 4、Store 对象对外提供的 subscribe 方法,在订阅时会将订阅的方法添加到内部 List<_VoidCallback> _listeners


  • 5、Store 对象内部的 StreamController.broadcast 创建出了 _notifyController 对象用于广播更新。


  • 6、Store 对象内部的 subscribe 方法,会在 ComponentState 中添加订阅方法 onNotify,如果调用在 onNotify 中最终会执行 setState更新UI。


  • 7、Store 对象对外提供的 dispatch 方法,执行时内部会执行 4 中的  List<_VoidCallback> _listeners,触发 onNotify。


  • 8、Page 内部会通过 Logic 创建 Dispatch ,执行时经历 Effect -> Middleware -> Stroe.dispatch -> Reducer -> State ->  _notifyController ->  _notifyController.add(state) 等流程。


  • 9、以上流程最终就是 Dispatch 触发 Store 内部 _notifyController , 最终会触发 ComponentState 中的 onNotify 中的setState更新UI
Fish-Redux 的整体流程更加复杂,内部的 ContxtSys 、Componet 、ViewSerivce 、 Logic 等模块也让代码的复用得到了提高,但从整个流程可以看出  Fish-Redux控件到页面更新,全都进行了新的独立设计,而这里面最有意思的,莫不过 dependencies

如下图所示,得益于 Fish-Redux 内部 ConnOpMixin 中对操作符的重载,我们可以通过 DoubleCountConnector() + DoubleCountComponent() 来实现Dependent 的组装。



Dependent 的组装中 Connector 会从总 State 中读取需要的小 State 用于 Component 的绘制,这样很好的达到了 模块解耦与复用 的效果。

而使用中我们组装的 dependencies 最后都会通过 ViewService 提供调用调用能力,比如调用 buildAdapter 用于列表能力,调用 buildComponent 提供独立控件能力等。

可以看出 Fish-Redux 的内部实现复杂度是比较高的,在提供组装、复用、解耦的同时,也对项目进行了一定程度的入侵,如果对于 Flutter 或者 Dart 并不熟悉,或者项目业务逻辑属于中小型的情况,不大建议使用 。

Flutter完整开发实战详解(十二、全面深入理解状态管理设计) - 掘金
Flutter 文章汇总地址

Flutter 完整实战实战系列文章专栏
Flutter 番外的世界系列文章专栏

资源推荐


Github : github.com/CarGuo
开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter
开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo
开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook
开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
回复 支持 反对

使用道具 举报

发表回复

您需要登录后才可以回帖 登录 | 立即注册 微信登录 手机动态码快速登录

本版积分规则

关闭

官方推荐 上一条 /3 下一条

快速回复 返回列表 客服中心 搜索 官方QQ群 洽谈合作
快速回复返回顶部 返回列表