flutter_redux 的学习理解

在刚刚接触flutter的时候使用过fish_redux这个状态管理库,当时觉得好复杂,文件多还跳来跳去的,如今多年过去了,公司用到了flutter_redux这个状态管理库,于是对redux有了新的认识
先说下flutter_redux的整体原理:
1.StoreProvider继承自InheritedWidget,主要作用是为了子控件获取Store的.与Provider状态管理框架不同的是,这里的Store并不是我们的状态,而Provider的data是状态,UI的更新依赖于InheritedWidget的,这里StoreProvider只是为了将Store在子树中传递,不涉及Store的修改以及UI的刷新
2.StoreConnector这个组件通过继承体系发现最终返回的是一个StreamBuilder,所以真正起到响应式状态更新的是这个StreamBuilder

所以总结: flutter_redux用StoreProvider(InheritedWidget)来传递Store,用StoreConnector(StreamBuilder)来响应状态的改变

对于原理其实很好理解,但是为什么fish_redux会受到中大型项目的青睐呢?小项目用起来是费力不讨好的

主要的一点就是分层明确:

对于大的项目,首先要考虑的是好管理,好维护,所以清晰的目录结构以及解耦是大项目选择状态管理框架的重要选项,而redux做到了这一点,他明确分了三大块:
state: 我们的状态,比如int count = 0;
action: 事件,比如点击按钮,我们要让屏幕上数字变化
reducer: 真正处理事件,这里会返回新的(或者是返回旧的)状态,触发StreamBuilder的builder
开发UI的同学不需要关心刷新的事情,需要的时候分发事件就可以了;
而写逻辑的同学也不需要关心哪里调用的我的方法,只针对事件来处理逻辑
那么flutter_redux是如何将这些内容串联起来的呢?

下面就是我学习后的理解

我们需要看下Store的初始化代码

Store(
    this.reducer, {
    required State initialState,
    List<Middleware<State>> middleware = const [],
    bool syncStream = false,
    bool distinct = false,
  }) : _changeController = StreamController.broadcast(sync: syncStream) {
    _state = initialState;
    _dispatchers = _createDispatchers(
      middleware,
      _createReduceAndNotify(distinct),
    );
  }

middleware先忽略,需要传入一个reducer,一个初始状态,reducer是一个函数类型

List<NextDispatcher> _createDispatchers(
    List<Middleware<State>> middleware,
    NextDispatcher reduceAndNotify,
  ) {
    final dispatchers = <NextDispatcher>[]..add(reduceAndNotify);

    // Convert each [Middleware] into a [NextDispatcher]
    for (var nextMiddleware in middleware.reversed) {
      final next = dispatchers.last;

      dispatchers.add(
        (dynamic action) => nextMiddleware(this, action, next),
      );
    }

    return dispatchers.reversed.toList();
  }

NextDispatcher _createReduceAndNotify(bool distinct) {
    return (dynamic action) {
      final state = reducer(_state, action);

      if (distinct && state == _state) return;

      _state = state;
      _changeController.add(state);
    };
  }

这里可以看到_changeController = StreamController.broadcast(sync: syncStream)StreamController被创建了,并且是广播类型的,因为可能被多个StreamBuilder监听
然后就是_createDispatchers这个方法,如果不考虑middleware,那么这个数组里只会有reduceAndNotify这一个元素,而reduceAndNotify就是_createReduceAndNotify(distinct),再_createReduceAndNotify里返回了一个回调函数,所以dispatchers中存储的就是这个回调函数,在这个函数里边调用了reducer,然后将reducer返回的state替换掉之前的,然后就是StreamControlleradd操作,这个回调函数在什么时候执行呢?
就是在storedispatch函数中,在我们的UI中,我们通过调用storedispatch函数去分发事件

dynamic dispatch(dynamic action) {
    return _dispatchers[0](action);
  }

以上是关于状态如何被多订阅StreamController.add到的,现在就来思考一个问题,如果只是简单的add,那必然会导致所有监听StreamController.stream 的 StreamBuilder 都去刷新,比如我state中有两个参数

class MyState {
  final int age;
  final String name;
  MyState({required this.age, required this.name});

  MyState copy({int? age, String? name}) {
    return MyState(age: age??this.age, name: name??this.name);
  }
}

我在一个地方想要监听age,在另一个地方想要监听name,我想要的结果是我在只修改age的时候,监听name的模块不会被刷新,我在只修改name的时候,age的模块不会被刷新,但是如果只是简单的Listen Store中的_changeController.stream,那结果必然是都要刷新,flutter_redux是如何解决这个问题的呢?这里就引出了StoreConnector两个属性

//将Store转换为ViewModel。生成的ViewModel将被传递给构建器函数。
final StoreConverter<S, ViewModel> converter;

//作为性能优化,只有当ViewModel发生变化时,Widget才能重新构建。
//为了使其正常工作,您必须为ViewModel实现==和hashCode,并在创建StoreConnector时将
//distinct选项设置为true。
final bool distinct;

通过注解可以看到,这两个属性就是用来解决上面这种刷新风暴的(如果我state中有100个属性,那必然是刷新风暴),这里主要还是在stream上做文章,我们知道stream是可以使用map,where这些方法的,上面我们说到StoreConnector本质就是使用了一个StreamBuilder

StreamBuilder<ViewModel>(
            stream: _stream,
            builder: (context, snapshot) {
              if (_latestError != null) throw _latestError!;

              return widget.builder(
                context,
                _requireLatestValue,
              );
            },
          )

但是这里的_stream是经过处理的,在initState中

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

    _computeLatestValue();

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

    _createStream();

    super.initState();
  }

void _createStream() {
  _stream = widget.store.onChange
      .where(_ignoreChange) //可以抛给外界判断是否需要这次流数据
      .map(_mapConverter) // 将state变成更小的数据 ,比如上面的MyState我只监听age,那么这里就可以回调回来state.age
 
      .where(_whereDistinct)//这里是根据distinct,以及state.age来自动过滤的
      .transform(StreamTransformer.fromHandlers(
    handleData: _handleChange,
    handleError: _handleError,
  ));
}

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

    return true;
  }

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

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

    return true;
  }

_whereDistinct的作用就是,当distinct为true时,判断新数据和旧数据是否相同(这里也能很好的理解为什么说必须为ViewModel实现==和hashCode),如果不同返回true(需要刷新),相同返回false(不接收这次的流数据).

这样一来,比如我监听的是age,但是我state中name改变了,数据流经_mapConverter的时候会将state转变成age,_whereDistinct会判断新旧age是否相同,这样一来,name的修改,就不会影响这里的刷新了,所以如果你想要name变化的同时这个模块也被刷新,distinct设置为false就好了,_whereDistinct直接返回true,直接放行

到这里,如果按照上文中不考虑middleware,那么_dispatchers中只会有一个回调函数,取出并且执行它,就完成了一系列的状态变化页面刷新,如此一来就完美的串联起了state,action,reducer这几个角色

最后呢说说middleware,在没有他的时候

862b9497-7250-4981-a37e-5ea6e96f081d.png

我们的view直接触发action,给到reducer去处理,但是这里就会有些问题,reducer是同步方法,如果我们需要一个网络请求怎么办,我们不能再reducer中做一步操作,所以flutter_redux给出了一个方法就是不直接将action给到reducer,等网络请求完成后再去将获取到的数据包装成action给到reducer,如图
be92afe7-1306-45f9-b82d-b7dfaa97763b.png
这个就是middleware中间件的作用,因为我们不能一步到位,所以迫不得已需要这个
他具体怎么做到的,我们需要回头看Store初始化的代码,里边调用了_createDispatchers

List<NextDispatcher> _createDispatchers(
    List<Middleware<State>> middleware,
    NextDispatcher reduceAndNotify,
  ) {
    final dispatchers = <NextDispatcher>[]..add(reduceAndNotify);

    // Convert each [Middleware] into a [NextDispatcher]
    for (var nextMiddleware in middleware.reversed) {
      final next = dispatchers.last;

      dispatchers.add(
        (dynamic action) => nextMiddleware(this, action, next),
      );
    }

    return dispatchers.reversed.toList();
  }

我们发现middleware被倒序遍历,然后添加完后返回的时候又是对dispatchers做了倒序,所以最后的效果是reduceAndNotify(也就是reducer处理action)放到最后,前边的middleware顺序就是初始化时候的顺序,然后每一个middleware都指向下一个dispatcher,这样就做到了,中间件都执行一遍,然后给到reducer处理,而中间件是可以async来修饰执行的
比如常用的一个

typedef ThunkAction<State> = dynamic Function(Store<State> store);

dynamic thunkMiddleware<State>(
  Store<State> store,
  dynamic action,
  NextDispatcher next,
) {
  if (action is ThunkAction<State>) {
    return action(store);
  } else {
    return next(action);
  }
}

//我们可以这样初始化
final store =
      Store<CounterState>(counterReducer, initialState: CounterState(0,0),middleware: [
        thunkMiddleware
      ]);

所以按照上面的代码逻辑最终在dispatchers这个数组里存储了两个回调函数一个是thunkMiddleware,一个是reduceAndNotify,并且是thunkMiddleware在前,这个时候如果调用dispatch,会先取到thunkMiddleware
如果action是ThunkAction的类型,thunkMiddleware会去执行action(因为ThunkAction是个函数类型,action是个回调函数)
所以我们可以写个ThunkAction类型的函数

test(Store<CounterState> store) async {
  final searchResults = await Future.delayed(
    Duration(seconds: 1),
    () => CounterAddAction(2),
  );
  store.dispatch(searchResults);
}

这里边进行了模拟网络请求,然后包装成新的action,再次调用dispatch函数,会再次进入thunkMiddleware,
由于CounterAddAction不是ThunkAction类型,所以会走else,由下一个dispatcher处理,而下一个就是reducer了
我们可以看到test函数是个异步函数,网络请求,延时操作可以在这里进行.
有了middleware,我们可以方便的对一个事件进行层层处理,甚至可以截停事件

还有一种方式就是有一个事件action1(请求)直接调用dispatch给到reducer,reducer请求网络数据并且将原来的state返回,然后通过then的方式再次调用dispatch,而这次的action2(刷新)才是真正刷新UI.

到此有关flutter_redux学习总结完毕,欢迎大家沟通交流!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容