Flutter 学习之旅(四十四) Flutter 状态 BLoC学习

什么是BLoC

BLoC(Business Logic Component)是谷歌提出的一种设计模式,利用Flutter 响应式结构的特点,通过Stream 的方式实现了异步渲染界面,实现布局与业务分离的效果.

这句话中有2个重点,一个是异步渲染,另一个就是业务与布局分离,
以传统的计数器代码为例,我们想要实现这个功能其实是非常简单的

class TsmSimpleBLoCPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _TsmSimpleBLoCState();
}

class _TsmSimpleBLoCState extends State<TsmSimpleBLoCPage> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Simple BLoC 学习'),
        centerTitle: true,
      ),
      body: Container(
        alignment: Alignment.center,
        child: Text(count.toString()),
      ),
      floatingActionButton: FloatingActionButton(
        child: Text('+'),
        onPressed: () {
          setState(() {
            ++count;
          });
        },
      ),
    );
  }
}

使用了BLoC后的代码结构如下

class TsmSimpleBLoCPage extends StatefulWidget{
  @override
  State<StatefulWidget> createState()=>_SimpleBLoCState();

}

class _SimpleBLoCState extends State<TsmSimpleBLoCPage>{

  _SimpleBLoC _bloc;
  @override
  void initState() {
    super.initState();
    _bloc=_SimpleBLoC.of();
  }
  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Simple BLoC 学习'),
        centerTitle: true,
      ),
      body: StreamBuilder(
        stream:_bloc.outStream ,
        initialData: _bloc.data,
        builder: (context,snap){
          return Container(
            alignment: Alignment.center,
            child: Text(snap.data.toString(),style: TextStyle(fontSize: 18),),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: Text('+'),
        onPressed: (){
          _bloc.add();
        },
      ),
    );
  }
}

class _SimpleBLoC{
  int _data;
   StreamController _streamController;
  _SimpleBLoC.of(){
     _streamController=StreamController<int>();
     _data=0;
  }

  dispose(){
    _streamController.close();
  }
  get outStream=>_streamController.stream;

  get data=>_data;

  add(){
    _streamController.sink.add(++_data);
  }
}

使用了BLoC 后,代码变得更多了,那么为什么还要使用他呢,如果你是一个android 开发的话,看起来是不是和MVP结构有点类似, 将逻辑代码与UI分离开,在state 的 build Widget 的方法内没有一行是关于业务逻辑的,在bloc 模块没有一行是关于布局的,这点主要是为了避免Widget 中嵌套了太多关于业务逻辑的判断,导致随着业务发展难以维护,虽然看到有网上说这么做是减少build 的次数,但是看了StreamBuilder 的源码发现,他用来更新widget的方法就是setState 的方法,并没有减少build的次数,只是通过观察者模式减少了业务与数据之间的关系
在使用bloc的过程中尽量使用StatefulWidget,这样你可以使用 dispose 方法来关闭stream ,减少内存泄露

我们先来看一下StreamBuilder 的源码,再来自己封装一个BLoC形式的StatefulWidget,以便于加深印象

class StreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
  const StreamBuilder({
    Key key,
    this.initialData,
    Stream<T> stream,
    @required this.builder,
  }) : assert(builder != null),
       super(key: key, stream: stream);
  final T initialData;
  @override
  AsyncSnapshot<T> initial() => AsyncSnapshot<T>.withData(ConnectionState.none, initialData);
  @override
  AsyncSnapshot<T> afterConnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.waiting);
  @override
  AsyncSnapshot<T> afterData(AsyncSnapshot<T> current, T data) {
    return AsyncSnapshot<T>.withData(ConnectionState.active, data);
  }
  @override
  AsyncSnapshot<T> afterError(AsyncSnapshot<T> current, Object error) {
    return AsyncSnapshot<T>.withError(ConnectionState.active, error);
  }
  @override
  AsyncSnapshot<T> afterDone(AsyncSnapshot<T> current) => current.inState(ConnectionState.done);
  @override
  AsyncSnapshot<T> afterDisconnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.none);
  @override
  Widget build(BuildContext context, AsyncSnapshot<T> currentSummary) => builder(context, currentSummary);
}

入参接收了一个initialData,在初始化AsyncSnapshot 的时候将这个initialData 添加进去,其他方便并没有什么逻辑,继续看他的父类

abstract class StreamBuilderBase<T, S> extends StatefulWidget {
  const StreamBuilderBase({ Key key, this.stream }) : super(key: key);
  final Stream<T> stream;
  S initial();
  S afterConnected(S current) => current;
  S afterData(S current, T data);
  S afterError(S current, Object error) => current;
  S afterDone(S current) => current;
  S afterDisconnected(S current) => current;

  Widget build(BuildContext context, S currentSummary);

  @override
  State<StreamBuilderBase<T, S>> createState() => _StreamBuilderBaseState<T, S>();
}

class _StreamBuilderBaseState<T, S> extends State<StreamBuilderBase<T, S>> {
  StreamSubscription<T> _subscription;
  S _summary;

  @override
  void initState() {
    super.initState();
    _summary = widget.initial();
    _subscribe();
  }
  
  @override
  void didUpdateWidget(StreamBuilderBase<T, S> oldWidget) {
    super.didUpdateWidget(oldWidget);
    ///前后的数据源不同,先取消注册,再重新注册,
    if (oldWidget.stream != widget.stream) {
      if (_subscription != null) {
        _unsubscribe();
        _summary = widget.afterDisconnected(_summary);
      }
      _subscribe();
    }
  }

  @override
  Widget build(BuildContext context) => widget.build(context, _summary);
  
///在控件移出的时候取消注册
  @override
  void dispose() {
    _unsubscribe();
    super.dispose();
  }
  ///添加各种监听
  void _subscribe() {
    if (widget.stream != null) {
      _subscription = widget.stream.listen((T data) {
        setState(() {
          _summary = widget.afterData(_summary, data);
        });
      }, onError: (Object error) {
        setState(() {
          _summary = widget.afterError(_summary, error);
        });
      }, onDone: () {
        setState(() {
          _summary = widget.afterDone(_summary);
        });
      });
      _summary = widget.afterConnected(_summary);
    }
  }
  
//取消注册
  void _unsubscribe() {
    if (_subscription != null) {
      _subscription.cancel();
      _subscription = null;
    }
  }
}

和包裹inheritedwidget 的statefulwidget 的使用方式基本是保持一致的,在initState 和dispose 方法中分别添加和移除监听,在didUpdateWidget方法中如果有必要则重置监听,在build 的方法内向下暴露当前的data

接下来我们利用StatefulWidget 与 StreamBuilder 封装一个简单的bloc 的base类,方便使用,

abstract class TsmBaseBLoC{

  /**
   *  用来调用 streamcontroller.close()
   */
  void dispose();
}


class TsmBaseBLoCWidget<T extends TsmBaseBLoC > extends StatefulWidget{
  final Widget child;
  final T bloc;
  TsmBaseBLoCWidget({Key key, @required this.child, @required this.bloc}):super(key:key);
  @override
  State<StatefulWidget> createState() =>_TsmBaseBLoCState<T>();
  /**
   * 便于子Widget 通过此方法向上
   * 此方法不能在 TsmBaseBLoCWidget 的child直接使用,需要使用StatelessWidget 或者StatefulWidget
   * 包裹一层,原因是 在findAncestorWidgetOfExactType 这个方法是直接查找他们_parent ,并没有比对自身
   * 他的直接 包裹的子Widget 的bloc 直接使用初始化的就可以了,
   *
   */
  static T of<T extends TsmBaseBLoC>(BuildContext context){
    TsmBaseBLoCWidget<T> provider = context.findAncestorWidgetOfExactType<TsmBaseBLoCWidget<T>>();
    return provider.bloc;
  }

}

class _TsmBaseBLoCState<T> extends State<TsmBaseBLoCWidget>{
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
  @override
  void dispose() {
    widget.bloc.dispose();
    super.dispose();
  }
}

封装后自动实现自动streamcontroller.close 和 在TsmBaseBLoCWidget 的子 StatelessWidget 与StatefulWidget中可以使用var_bloc=TsmBaseBLoCWidget.of<T>(context);来获取该 bloc,

关于findAncestorWidgetOfExactType 方法引起的血案

这个方法的意思向上查找祖先widget,找到第一个符合条件的widget为止,既然是这样,正常情况下A页面启动B页面,是不是会通过 A页面加载B页面呢,这样B页面也就继承了A页面的某些特性,答案肯定不是这样的,先来分析一下如果我们这么做会出现什么问题
1> widget 树结构太复杂,打开的widget 的深度越深 则这个widget中包含所有widget的特性,数据量太大,这个太容易导致oom了,
2> 如果从InheritedWidget 方面来说,如果A启动B,B就会继承A的特性,设计这个InheritedWidget 就完全没有必须要了,

再来从代码上面来讲,打开一个页面使用的Navigator,我看了一下Navigator.of的方法,看一下他是如何实现的,

  static NavigatorState of(
    BuildContext context, {
    bool rootNavigator = false,
    bool nullOk = false,
  }) {
    // Handles the case where the input context is a navigator element.
    NavigatorState navigator;
    if (context is StatefulElement && context.state is NavigatorState) {
        navigator = context.state as NavigatorState;
    }
    if (rootNavigator) {///找到最顶层NavigatorState  ,即MaterialApp 的 navigatorKey
      navigator = context.findRootAncestorStateOfType<NavigatorState>() ?? navigator;
    } else {
    ///找到最近NavigatorState
      navigator = navigator ?? context.findAncestorStateOfType<NavigatorState>();
    }
    return navigator;
  }

在Navitagor.of的方法我们发现他需要一个rootNavigator ,来控制他是获取整个App根NavigatorState 还是向上查找最近一个NavigatorState,
那也就可以理解在整个app中是可以存在多个 NavigatorState, 这样就实现了根NavigatorState 控制了整个app的风格, 最近的NavigatorState 作为模块的根NavigatorState 控制着必要的数据,实现在一个模块共享一些必须的信息,为什么要这么设计,我先来说一下我的需求,以一个机票改期为例, 改期的第一步需要获取到需要改期的行程,根据这个原始行程你需要经过选择日历的界面,选择新航班的页面,选择新仓位的页面,新的确认订单页面,如果你使用的是app 的根NavigatorState,所有的页面是平级的,他们之间不能使用InheritedWidget共享数据 ,将这个数据放在根NavigatorState共享给其他模块又没有什么意义,你如果在选择需要改期的行程页面创建这个NavigatorState , 这个NavigatorState 承接了根NavigatorState 的theme数据,同时又可以利用InheritedWidget 在这个NavigatorState 下面共享这个原始行程的数据,这样实现起来就非常完美了,

这些都是一些理论知识,由于Navigator和NavigatorState 他们是一个控件的整体,就像StatefulWidget 和State 一样,利用Navigator来创建页面我还没有看到,只是一些猜想,而这个灵感就是从findAncestorWidgetOfExactType 这样一个方法而来的,虽然因此思前想后的3个多小时,但是知识的进步真是一件令人愉快的事情
这个里面关于如何实现一个简单的Navigator,希望大家和我多多交流,在后续我也会多加关注这方面的源码,

我学习flutter的整个过程都记录在里面了
https://www.jianshu.com/c/36554cb4c804

最后附上demo 地址

https://github.com/tsm19911014/tsm_flutter

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