乐趣区

Flutter-应用开发之Bloc模式

基本概念

响应式编程

所谓响应式编程,指的是一种面向数据流和变化传播的编程范式。使用响应式编程范式,意味着可以在编程语言中更加方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

响应式编程最初的目的是为了简化交互式用户界面的创建和实时系统动画的绘制而提出来的一种方法,是为了简化 MVC 软件架构而设计的。在面向对象编程语言中,响应式编程通常以观察者模式的扩展呈现。还可以将响应式流模式和迭代器模式比较,一个主要的区别是,迭代器基于”拉“,而响应式流基于”推“。

使用迭代器是一种命令式编程,由开发者决定何时去访问序列中的 next() 元素。而在响应式流中,与 Iterable-Iterator 对应的是 Publisher-Subscriber。当新的可用元素出现时,发布者通知订阅者,这种”推“正是响应的关键。此外,应用于推入元素上的操作是声明式的而不是命令式的:程序员要做的是表达计算的逻辑,而不是描述精准的控制流程。

除了推送元素,响应式编程还定义了良好的错误处理和完成通知方式。发布者可以通过调用 next() 方法推送新的元素给订阅者,也可以通过调用 onError() 方法发送一个错误信号或者调用 onComplete() 发送一个完成信号。错误信号和完成信号都会终止序列。

响应式编程非常灵活,它支持没有值、一个值或 n 个值的用例 (包括无限序列),因此现在大量的应用程序开发都悄然使用这种流行的模式进行开发。

Stream

在 Dart 中,Stream 和 Future 是异步编程的两个核心 API,主要用于处理异步或者延迟任务等,返回值都是 Future 对象。不同之处在于,Future 用于表示一次异步获得的数据,而 Stream 则可以通过多次触发成功或失败事件来获取数据或错误异常。

Stream 是 Dart 提供的一种数据流订阅管理工具,功能有点类似于 Android 中的 EventBus 或者 RxBus,Stream 可以接收任何对象,包括另外一个 Stream。在 Flutter 的 Stream 流模型中,发布对象通过 StreamController 的 sink 来添加数据,然后通过 StreamController 发送给 Stream,而订阅者则通过调用 Stream 的 listen() 方法来进行监听,listen() 方法会返回一个 StreamSubscription 对象,StreamSubscription 对象支持对数据流进行暂停、恢复和取消等操作。

根据数据流监听器个数的不同,Stream 数据流可以分为单订阅流和多订阅流。所谓单订阅流,指的是整个生命周期只允许存在一个监听器,如果该监听器被取消,则不能继续进行监听,使用的场景有文件 IO 流读取等。而所谓广播订阅流,指的是应用的生命周期内允许有多个监听器,当监听器被添加后就可以对数据流进行监听,此种类型适合需要进行多个监听的场景。

例如,下面是使用 Stream 的单订阅模式进行数据监听的示例,代码如下。

class StreamPage extends StatefulWidget {StreamPage({Key key}): super(key: key);
  @override
  _StreamPageState createState() => _StreamPageState();
}

class _StreamPageState extends State<StreamPage> {StreamController controller = StreamController();
  Sink sink;
  StreamSubscription subscription;

  @override
  void initState() {super.initState();
    sink = controller.sink;
    sink.add('A');
    sink.add(1);
    sink.add({'a': 1, 'b': 2});
    subscription = controller.stream.listen((data) => print('listener: $data'));
  }

  @override
  Widget build(BuildContext context) {return Center();
  }

  @override
  void dispose() {super.dispose();
    sink.close();
    controller.close();
    subscription.cancel();}
}

运行上面的代码,会在控制台输出如下日志信息。

I/flutter (3519): listener: A
I/flutter (3519): listener: 1
I/flutter (3519): listener: {a: 1, b: 2}

与单订阅流不同,多订阅流允许有多个订阅者,并且只要数据流中有新的数据就会进行广播。多订阅流的使用流程和单订阅流一样,只是创建 Stream 流控制器的方式不同,如下所示。

class StreamBroadcastPage extends StatefulWidget {StreamBroadcastPage({Key key}): super(key: key);
  @override
  _StreamBroadcastPageState createState() => _StreamBroadcastPageState();
}

class _StreamBroadcastPageState extends State<StreamBroadcastPage> {StreamController controller = StreamController.broadcast();
  Sink sink;
  StreamSubscription subscription;

  @override
  void initState() {super.initState();
    sink = controller.sink;
    sink.add('A');
    subscription = controller.stream.listen((data) => print('Listener: $data'));
    sink.add('B');
    subscription.pause();
    sink.add('C');
    subscription.resume();}

  @override
  Widget build(BuildContext context) {return Center();
  }

  @override
  void dispose() {super.dispose();
    sink.close();
    controller.close();
    subscription.cancel();}
}

允许上面的代码,输出的日志如下所示。

I/flutter (3519): Listener: B
I/flutter (3519): Listener: C

不过,单订阅 Stream 只有当存在监听的时候才发送数据,广播订阅流 则不考虑这点,有数据就发送;当监听调用 pause 以后,不管哪种类型的 stream 都会停止发送数据,当 resume 之后,把前面存着的数据都发送出去。

sink 可以接受任何类型的数据,也可以通过泛型对传入的数据进行限制,比如我们对 StreamController 进行类型指定 StreamController<int> _controller = StreamController.broadcast(); 因为没有对 Sink 的类型进行限制,还是可以添加除了 int 外的类型参数,但是运行的时候就会报错,_controller 对你传入的参数做了类型判定,拒绝进入。

同时,Stream 中还提供了很多 StremTransformer,用于对监听到的数据进行处理,比如我们发送 0~19 的 20 个数据,只接受大于 10 的前 5 个数据,那么可以对 stream 如下操作。

_subscription = _controller.stream
    .where((value) => value > 10)
    .take(5)
    .listen((data) => print('Listen: $data'));

List.generate(20, (index) => _sink.add(index));

除了 where、take 外,还有很多 Transformer,例如 map,skip 等等,读者可以自行研究。

在 Stream 流模型中,当数据源发生变化时 Stream 会通知订阅者,从而改变控件状态,实现页面的刷新。同时,为了减少开发者对 Stream 数据流的干预,Flutter 提供了一个 StreamBuilder 组件来辅助 Stream 数据流操作,它的构造函数如下所示。

StreamBuilder({
  Key key,
  this.initialData,
  Stream<T> stream,
  @required this.builder,
})

事实上,StreamBuilder 是一个用于监控 Stream 数据流变化并展示数据变化的 StatefulWidget 组件,它会一直记录着数据流中最新的数据,当数据流发生变化时会自动调用 builder() 方法进行视图的重建。例如,下面是使用 StreamController 结合 StreamBuider 对官方的计数器应用进行改进,取代使用 setState 方式来刷新页面,代码如下。

class CountPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {return CountPageState();
  }
}

class CountPageState extends State<CountPage> {
  int count = 0;
  final StreamController<int> controller = StreamController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Center(
          child: StreamBuilder<int>(
              stream: controller.stream,
              builder: (BuildContext context, AsyncSnapshot snapshot) {
                return snapshot.data == null
                    ? Text("0")
                    : Text("${snapshot.data}");
              }),
        ),
      ),
      floatingActionButton: FloatingActionButton(child: const Icon(Icons.add),
          onPressed: () {controller.sink.add(++count);
          }),
    );
  }

  @override
  void dispose() {controller.close();
    super.dispose();}
}

可以看到,相比传统的 setState 方式,StreamBuilder 是一个很大的进步,因为它不需要强行重建整个组件树和它的子组件,只需要重建 StreamBuilder 包裹的组件即可。而例子中使用 StatefulWidget 的原因是需要在组件的 dispose() 方法中释放 StreamController 对象。

BLoC 模式

BLoC 简介

BLoC 是 Business Logic Component 的英文缩写,中文译为业务逻辑组件,是一种使用响应式编程来构建应用的方式。BLoC 最早由谷歌的 Paolo Soares 和 Cong Hui 设计并开发,设计的初衷是为了实现页面视图与业务逻辑的分离。如下图所示,是采用 BLoC 模式的应用程序的架构示意图。

使用 BLoC 方式进行状态管理时,应用里的所有组件被看成是一个事件流,一部分组件可以订阅事件,另一部分组件则消费事件,BLoC 的工程流程下图所示。

如上图所示,组件通过 Sink 向 Bloc 发送事件,BLoC 接收到事件后执行内部逻辑处理,并把处理的结果通过流的方式通知给订阅事件流的组件。在 BLoC 的工作流程中,Sink 接受输入,BLoC 则对接受的内容进行处理,最后再以流的方式输出,可以发现,BLoC 又是一个典型的观察者模式。理解 Bloc 的运作原理,需要重点关注几个对象,分别是事件、状态、转换和流。

  • 事件:在 Bloc 中,事件会通过 Sink 输入到 Bloc 中,通常是为了响应用户交互或者是生命周期事件而进行的操作。
  • 状态:用于表示 Bloc 输出的东西,是应用状态的一部分。它可以通知 UI 组件,并根据当前状态重建其自身的某些部分。
  • 转换:从一种状态到另一种状态的变动称之为转换,转换通常由当前状态、事件和下一个状态组成。
  • 流:表示一系列非同步的数据,Bloc 建立在流的基础之。并且,Bloc 需要依赖 RxDart,它封装了 Dart 在流方面的底层细节实现。

BLoC Widget

Bloc 既是软件开发中的一种架构模式,也是一种软件编程思想。在 Flutter 应用开发中,使用 Bloc 模式需要引入 flutter_bloc 库,借助 flutter_bloc 提供的基础组件,开发者可以快速高效地实现响应式编程。flutter_bloc 提供的常见组件有 BlocBuilder、BlocProvider、BlocListener 和 BlocConsumer 等。

BlocBuilder

BlocBuilder 是 flutter_bloc 提供的一个基础组件,用于构建组件并响应组件新的状态,它通常需要 Bloc 和 builder 两个参数。BlocBuilder 与 StreamBuilder 的作用一样,但是它简化了 StreamBuilder 的实现细节,减少一部分必须的模版代码。而 builder() 方法会返回一个组件视图,该方法会被潜在的触发多次以响应组件状态的变化,BlocBuilder 的构造函数如下所示。

const BlocBuilder({
    Key key,
    @required this.builder,
    B bloc,
    BlocBuilderCondition<S> condition,
  })

可以发现,BlocBuilder 的构造函数里面一共有三个参数,并且 builder 是一个必传参数。除了 builder 和 bloc 参数外,还有一个 condition 参数,该参数用于向 BlocBuilder 提供可选的条件,对 builder 函数进行缜密的控制。

BlocBuilder<BlocA, BlocAState>(condition: (previousState, state) {// 根据返回的状态决定是否重构组件},
  builder: (context, state) {// 根据 BlocA 的状态构建组件}
)

如上所示,条件获取先前的 Bloc 的状态和当前的 bloc 的状态并返回一个布尔类型的值。如果 condition 属性返回 true,那么将调用 state 执行视图的重新构建。如果 condition 返回 false,则不会执行视图的重建操作。

BlocProvider

BlocProvider 是一个 Flutter 组件,可以通过 BlocProvider.of <T>(context) 向其子级提供 bloc。实际使用时,它可以作为依赖项注入到组件中,从而将一个 bloc 实例提供给子树中的多个组件使用。

大多数情况下,我们可以使用 BlocProvider 来创建一个新的 blocs,并将其提供给其它子组件,由于 blocs 是 BlocProvider 负责创建的,那么关闭 blocs 也需要 BlocProvider 进行处理。除此之外,BlocProvider 还可用于向子组件提供已有 bloc,由于 bloc 并不是 BlocProvider 创建的,所以不能通过 BlocProvider 来关闭该 bloc,如下所示。

BlocProvider.value(value: BlocProvider.of<BlocA>(context),
  child: ScreenA(),);

MultiBlocProvider

MultiBlocProvider 是一个用于将多个 BlocProvider 合并为一个 BlocProvider 的组件,MultiBlocProvider 通常用于替换需要嵌套多个 BlocProviders 的场景,从而降低代码的复杂度、提高代码的可读性。例如,下面是一个多 BlocProvider 嵌套的场景。

BlocProvider<BlocA>(create: (BuildContext context) => BlocA(),
  child: BlocProvider<BlocB>(create: (BuildContext context) => BlocB(),
    child: BlocProvider<BlocC>(create: (BuildContext context) => BlocC(),
      child: ChildA(),)
  )
)

可以发现,示例中 BlocA 嵌套 BlocB,BlocB 又嵌套 BlocC,代码逻辑非常复杂且可读性很差。那如果使用 MultiBlocProvider 组件就可以避免上面的问题,改造后的代码如下所示。

MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(create: (BuildContext context) => BlocA(),),
    BlocProvider<BlocB>(create: (BuildContext context) => BlocB(),),
    BlocProvider<BlocC>(create: (BuildContext context) => BlocC(),),
  ],
  child: ChildA(),)

BlocListener

BlocListener 是一个接收 BlocWidgetListener 和可选 Bloc 的组件,适用于每次状态更改都需要发生一次的场景。BlocListener 组件的 listener 参数可以用来响应状态的变化,可以用它来处理更新 UI 视图之外的其他事情。与 BlocBuilder 中的 builder 操作不同,BlocBuilder 组件的状态更改仅会调用一次监听,并且是一个空函数。BlocListener 组件通常用在导航、SnackBar 和显示 Dialog 的场景。

BlocListener<BlocA, BlocAState>(
  bloc: blocA,
  listener: (context, state) {// 基于 BlocA 的状态执行某些操作}
  child: Container(),)

除此之外,还可以使用条件属性来对监听器函数进行更加缜密的控制。条件属性会通过比较先前的 bloc 的状态和当前的 bloc 的状态返回一个布尔值,如果条件返回 true,那么监听汗水将会被调用,如果条件返回 false,监听函数则不会被调用,如下所示。

BlocListener<BlocA, BlocAState>(condition: (previousState, state) {// 返回 true 或 false 决定是否需要调用监听},
  listener: (context, state) {})

如果需要同时监听多个 bloc 的状态,那么可以使用 MultiBlocListener 组件,如下所示。

BlocListener<BlocA, BlocAState>(
MultiBlocListener(
  listeners: [
    BlocListener<BlocA, BlocAState>(listener: (context, state) {},),
    BlocListener<BlocB, BlocBState>(listener: (context, state) {},),
…
  ],
  child: ChildA(),)

除此之外,flutter_bloc 提供的组件还有 BlocConsumer、RepositoryProvider 和 MultiRepositoryProvider 等。
当状态发生变化时,除了需要更新 UI 视图之外还需要处理一些其他的事情,那么可以使用 BlocListener,BlocListener 包含了一个 listener 用以做除 UI 更新之外的事情,该逻辑不能放到 BlocBuilder 里的 builder 中,因为这个方法会被 Flutter 框架调用多次,builder 方法应该只是一个返回 Widget 的函数。

flutter_bloc 快速上手

使用 flutter_bloc 之前,需要先在工程的 pubspec.yaml 配置文件中添加库依赖,如下所示。

dependencies:
  flutter_bloc: ^4.0.0

使用 flutter packages get 命令将依赖库拉取到本地,然后就可以使用 flutter_bloc 库进行应用开发了。

下面就通过一个计数器应用程序示例来说明 flutter_bloc 库的基本使用流程。在示例程序中有两个按钮和一个用于显示当前计数器值的文本组件,两个按钮分别用来增加和减少计数器的值。按照 Bloc 模式的基本使用规范,首先需要新建一个事件对象,如下所示。

enum CounterEvent {increment, decrement}

然后,新建一个 Bloc 类,用于对计数器的状态进行管理,如下所示。

class CounterBloc extends Bloc<CounterEvent, int> {
  
  @override
  int get initialState => 0;

  @override
  Stream<int> mapEventToState(CounterEvent event) async* {switch (event) {
      case CounterEvent.decrement:
        yield state - 1;
        break;
      case CounterEvent.increment:
        yield state + 1;
        break;
      default:
        throw Exception('oops');
    }
  }
}

通常,继承 Bloc<Event,State> 必须实现 initialState() 和 mapEventToState() 两个方法。其中,initialState() 用于表示事件的初始状态,而 mapEventToState() 方法返回的是经过业务逻辑处理完成之后的状态,此方法可以拿到具体的事件类型,然后根据事件类型进行某些逻辑处理。

为了方便编写 Bloc 文件,我们还可以使用 Bloc Code Generator 插件来辅助 Bloc 文件的生成。安装完成后,在项目上右键,并依次选择【Bloc Generator】->【New Bloc】来创建 Bloc 文件,如下图所示。

Bloc Code Generator 插件生成的 Bloc 文件如下:

bloc
 ├── counter_bloc.dart    // 所有 business logic, 例如加减操作
 ├── counter_state.dart  // 所有 state, 例如 Added、Decreased 
 ├── counter_event.dart  // 所有 event, 例如 Add , Remove 
 └── bloc.dart 

使用 Bloc 之前,需要在应用的最上层容器中进行注册,即在 MaterialApp 组件中注册 Bloc。然后,再使用 BlocProvider.of <T>(context) 获取注册的 Bloc 对象,通过 Bloc 处理业务逻辑。接收和响应状态的变化则需要使用 BlocBuilder 组件,BlocBuilder 组件的 builder 参数会返回组件视图,如下所示。

void main() {runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home:BlocProvider<CounterBloc>(create: (context) => CounterBloc(),
        child: CounterPage(),),
    );
  }
}

class CounterPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);
    return Scaffold(appBar: AppBar(title: Text('Bloc Counter')),
      body: BlocBuilder<CounterBloc, int>(builder: (context, count) {
          return Center(child: Text('$count', style: TextStyle(fontSize: 48.0)),
          );
        },
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(child: Icon(Icons.add),
              onPressed: () {counterBloc.add(CounterEvent.increment);
              },
            ),
          ),
          Padding(padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(child: Icon(Icons.remove),
              onPressed: () {counterBloc.add(CounterEvent.decrement);
              },
            ),
          ),
        ],
      ),
    );
  }
}

运行上面的示例代码,当点击计数器的增加按钮时就会执行加法操作,而点击减少按钮时就会执行减法操作,如下图所示。

可以发现,使用 flutter_bloc 状态管理框架,不需要调用 setState() 方法也可以实现数据状态的改变,并且页面和逻辑是分开的,更适合在中大型项目中使用。本文只介绍了 Bloc 的一些基本知识,详细情况可以查看:Bloc 官方文档

退出移动版