[译]Flutter 响应式编程:Steams 和 BLoC 实践范例(2) - BLoC 的范围和初始化

原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是作者 Didier BoelensReactive Programming - Streams - BLoC 写的后续

阅读本文前建议先阅读前篇,前篇中文翻译有两个版本:

  1. [译]Flutter响应式编程:Streams和BLoC by JarvanMo
    忠于原作的版本

  2. Flutter中如何利用StreamBuilder和BLoC来控制Widget状态 by 吉原拉面
    省略了一些初级概念,补充了一些个人解读

前言

在了解 BLoC, Reactive ProgrammingStreams 概念后,我又花了些时间继续研究,现在非常高兴能够与你们分享一些我经常使用并且个人觉得很有用的模式(至少我是这么认为的)。这些模式为我节约了大量的开发时间,并且让代码更加易读和调试。

目录

(由于原文较长,翻译发布时进行了分割)

  1. BlocProvider 性能优化
    结合 StatefulWidgetInheritedWidget 两者优势构建 BlocProvider

  2. BLoC 的范围和初始化
    根据 BLoC 的使用范围初始化 BLoC

  3. 事件与状态管理
    基于事件(Event) 的状态 (State) 变更响应

  4. 表单验证
    根据表单项验证来控制表单行为 (范例中包含了表单中常用的密码和重复密码比对)

  5. Part Of 模式
    允许组件根据所处环境(是否在某个列表/集合/组件中)调整自身的行为

文中涉及的完整代码可在 GitHub 查看。

2. BLoC 的范围和初始化

要回答「要在哪初始化 BLoC?」这个问题,需要先搞清楚 BLoC 的可用范围(scope)

2.1. 应用中任何地方可用

在实际应用中,常常需要处理如用户鉴权、用户档案、用户设置项、购物篮等等需要在 App 中任何组件都可访问的数据或状态,这里总结了适用这种情况的两种 BLoC 方案:

2.1.1. 全局单例 (Global Singleton)

这种方案使用了一个不在Widget视图树中的 Global 对象,实例化后可用供所有 Widget 使用。

bloc_singleton.dart

import 'package:rxdart/rxdart.dart';

class GlobalBloc {
  ///
  /// Streams related to this BLoC
  ///
  BehaviorSubject<String> _controller = BehaviorSubject<String>();
  Function(String) get push => _controller.sink.add;
  Stream<String> get stream => _controller;

  ///
  /// Singleton factory
  ///
  static final GlobalBloc _bloc = new GlobalBloc._internal();
  factory GlobalBloc(){
    return _bloc;
  }
  GlobalBloc._internal();
  
  ///
  /// Resource disposal
  ///
  void dispose(){
    _controller?.close();
}

GlobalBloc globalBloc = GlobalBloc();

要使用全局单例 BLoC,只需要 import 后调用定义好的方法即可:

import 'global_bloc.dart';

class MyWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context){
        globalBloc.push('building MyWidget'); //调用 push 方法添加数据 
        return Container();
    }
}

如果你想要一个唯一的、可从应用中任何组件访问的 BLoC 的话,这个方案还是不错的,因为:

  • 简单易用
  • 不依赖任何 BuildContext
  • 当然也不需要通过 context 查找 BlocProvider 的方式来获取 BLoC
  • 释放资源也很简单,只需将 application Widget 基于 StatefulWidget 实现,然后重写其 dispose() 方法,在 dispose() 中调用 globalBloc.dispose() 即可

我也不知道具体是为啥,很多较真的人反对全局单例方案,所以…我们再来看另一种实现方案吧…

2.1.2. 注入到视图树顶层

在 Flutter 中,包含所有页面的ancestor本身必须是 MaterialApp 的父级。 这是因为页面(或者说Route)其实是作为所有页面共用的 Stack 中的一项,被包含在 OverlayEntry 中的。

换句话说,每个页面都有自己独立于任何其它页面Buildcontext。这也解释了为啥不用任何技巧是没办法实现两个页面(或路由)之间数据共享的。

因此,必须将 BlocProvider 作为 MaterialApp 的父级才能实现在应用中任何位置都可使用 BLoC,如下所示:

bloc_on_top.dart

void main() => runApp(Application());

class Application extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<AuthenticationBloc>(
      bloc: AuthenticationBloc(),
      child: MaterialApp(
        title: 'BLoC Samples',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: InitializationPage(),
      ),
    );
  }
}

2.2. 在子视图树(多个页面或组件)中可用

大多数时候,我们只需要在应用的部分页面/组件树中使用 BLoC。举个例子,在一个 App 中有类似论坛的功能模块,在这个功能模块中我们需要用到 BLoC 来实现:

  • 与后端服务器交互,获取、添加、更新帖子
  • 在特定的页面列出需要显示的数据

显然我们不需要将论坛的 BLoC 实现成全局可用,只需在涉及论坛的视图树中可用就行了。

那么可采用通过 BlocProviderBLoC 作为模块子树的根(父级)注入的方式,如下所示:

bloc_init_root.dart

class MyTree extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    return BlocProvider<MyBloc>(
      bloc: MyBloc(),
      child: Column(
        children: <Widget>[
          MyChildWidget(),
        ],
      ),
    );
  }
}

class MyChildWidget extends StatelessWidget {
  @override 
  Widget build(BuildContext context){
    MyBloc = BlocProvider.of<MyBloc>(context);
    return Container();
  }
}

这样,该模块下所有 Widget 都可以通过调用 BlocProvider.of 来获取 BLoC.

注意

上面给出的并不是最佳方案,因为每次 MyTree 重构(rebuild)时都会重新初始化 BLoC ,带来的结果是:

  • 丢失 BLoC 中已经存在的数据内容
  • 重新初始化BLoC 要占用 CPU 时间

在这个例子中更好的方式是使用 StatefulWidget ,利用其持久化 State 的特性解决上述问题,代码如下:

bloc_init_root_2.dart

class MyTree extends StatefulWidget {
 @override
  _MyTreeState createState() => _MyTreeState();
}
class _MyTreeState extends State<MyTree>{
  MyBloc bloc;
  
  @override
  void initState(){
    super.initState();
    bloc = MyBloc();
  }
  
  @override
  void dispose(){
    bloc?.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context){
    return BlocProvider<MyBloc>(
      bloc: bloc,
      child: Column(
        children: <Widget>[
          MyChildWidget(),
        ],
      ),
    );
  }
}

这样实现的话,即使 MyTree 组件重构,也不会重新初始化 BLoC,而是直接使用之前的BLoC实例。

2.3. 单一组件中可用

如果只在某一个组件(Widget)中使用 BLoC,只需要在该组件内构建 BLoC 实例即可。

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

推荐阅读更多精彩内容