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官方文档

阅读 295

推荐阅读