Flutter示例系列(二)之状态管理-叁(Bloc)

  • Post author:
  • Post category:其他


开发环境:
Mac OS 10.14.5
VSCode 1.36.1
Flutter 1.9.1+hotfix.2



前言

这是第三篇关于状态管理的文章。第一篇见

Flutter示例系列(二)之状态管理-壹(scoped_model)

,第二篇见

Flutter示例系列(二)之状态管理-贰(fish-redux)

Bloc使用到Stream,在

Dart基础语法之异步支持

(见公众号:Flutter小同学) 中,简单描述了Stream的用法,在

Dart异步编程:Streams

(见公众号:Flutter小同学)中,对Stream进行了简要介绍。



概念

Bloc由以下三个包组成:


  • bloc

    :bloc核心库

  • flutter bloc

    :Flutter组件配合bloc实现快速可交互的移动应用

  • angular bloc

    :Angular组件配合bloc实现快速可交互的web应用

示例主要讲述移动应用,因此在pubspec.yaml 文件引入依赖:

dependencies: 
    bloc:^0.15.0 
    flutter_bloc:^0.21.0

状态管理的方法有很多,选择合适的至关重要。


scoped_model

使用观察者模式,将数据保存到model中,但是需要从父级往下传递,当数据发生改变时,再通知所有的监听类,更新状态。


fish_redux

基于redux设计,很好的将数据与视图分离,但是概念很多,用起来比较麻烦。


bloc

基于Stream设计,业务逻辑与视图更容易分离。设计之初就有三个核心思想:

  • 简单,容易理解易上手
  • 强大,由小组件构成复杂的应用
  • 易测试


bloc理念

  1. events (事件)

events 是bloc的输入。通常根据用户交互来派发,如点击按钮、页面加载。

  1. states (状态)

states 是bloc的输出,也是整个应用状态的一部分。UI组件会被通知状态改变,并重绘部分视图。

  1. transitions(转变)

一个状态到另一个状态的改变过程叫

transitions

。由当前状态、事件和下个状态组成。

  1. streams(流)

stream就是一系列异步的数据。

bloc 建立在 RxDart 之上,它抽象了RxDart所有的实现细节。

要用好 bloc,对 streams 深刻理解显得尤为重要。犹如水从管道中喷涌而出,stream是管道,数据是水。

用 async* 标记函数,使用 yield 并返回包含数据的 stream。

Stream<int> countStream(int max) async* {
    for (int i = 0; i < max; i++) {
        yield i;
    }
}

使用上面生成的stream,返回stream包含整型数值的和。用 async 标记函数,使用 await 并返回包含整型值的 Future。

Future<int> sumStream(Stream<int> stream) async {
    int sum = 0;
    await for (int value in stream) {
        sum += value;
    }
    return sum;
}

主函数中调用:

void main() async {
    /// Initialize a stream of integers 0-9
    Stream<int> stream = countStream(10);
    /// Compute the sum of the stream of integers
    int sum = await sumStream(stream);
    /// Print the sum
    print(sum); // 45
}
  1. blocs

bloc(Business Logic Component)是一个组件,将包含输入事件的stream 转变成 输出状态的stream。

每个bloc必须继承 Bloc类(bloc核心包的部分):

import 'package:bloc/bloc.dart';class CounterBloc extends Bloc<CounterEvent, int> {}

每个bloc必须定义初始化状态,作为事件来之前的状态。如:

@overrideint get initialState => 0;

每个bloc必须实现 mapEventToState 函数,将 event 作为参数,返回带有新 states 的stream。在任何时候都可以用 currentState 访问当前状态:

@override
Stream<int> mapEventToState(CounterEvent event) async* {
    switch (event) {
      case CounterEvent.decrement:
        yield currentState - 1;
        break;
      case CounterEvent.increment:
        yield currentState + 1;
        break;
    }
}

注意:bloc会忽视重复的状态。当 currentState == state时,没有 transiton 发生并且没有改变写进Stream。

每个bloc都有 dispatch 方法。Dispatch 携带 event 触发 mapEventToState,可被展示层或者bloc内部调用,进而通知bloc有新事件。

现在,创建简单的示例:

void main() {
    CounterBloc bloc = CounterBloc();for (int i = 0; i < 3; i++) {
        bloc.dispatch(CounterEvent.increment);
    }
}

从上面可以得出,transition 应该如下:

{
    "currentState": 0,
    "event": "CounterEvent.increment",
    "nextState": 1
}
{
    "currentState": 1,
    "event": "CounterEvent.increment",
    "nextState": 2
}
{
    "currentState": 2,
    "event": "CounterEvent.increment",
    "nextState": 3
}

很可惜,除非我们重写 onTransition,否则不会看到这些转变。


onTransition

是一个可以被重写的方法,来处理每个bloc的转变。它仅在bloc的状态更新时调用。所以我们可以增加日志或者分析代码。


onError

是一个可以被重写的方法,来处理每个bloc的Exception(异常)。默认情况下,所有的异常都会被忽略且不会造成影响。

  1. blocDelegate

使用bloc的好处就是在一个地方可以访问所有的 Transitions。

如果想在所有 Transitions的响应中做一些事情,可以创建自己的 BlocDelegate。

class SimpleBlocDelegate extends BlocDelegate {
  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print(transition);
  }
}

告诉bloc使用 SimpleBlocDelegate:

void main() {
  BlocSupervisor.delegate = SimpleBlocDelegate();
  CounterBloc bloc = CounterBloc();for (int i = 0; i < 3; i++) {
    bloc.dispatch(CounterEvent.increment);
  }
}

BlocSupervisor是一个单例,监听所有的blocs,并且委派任务到BlocDelegate。

也可以重写 onEvent,或者 onError:

class SimpleBlocDelegate extends BlocDelegate {
  @override
  void onEvent(Bloc bloc, Object event) {
    super.onEvent(bloc, event);
    print(event);
  }
​
  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print(transition);
  }
​
  @override
  void onError(Bloc bloc, Object error, StackTrace stacktrace) {
    super.onError(bloc, error, stacktrace);
    print('$error, $stacktrace');
  }
}


Flutter bloc 理念


1.Bloc 组件

  • BlocBuilder

是一个需要

Bloc



builder函数

的组件。BlocBuilder 响应新状态去构建组件,类似于StreamBuilder但更简单。builder函数 可能会被调用多次,根据状态返回一个组件的纯函数。

如果想根据状态改变做一些事情(如:导航、提示语等),请看BlocListener。

如果bloc的参数省略,BlocBuilder 会用 BlocProvider 和 当前的BuildContext自动查找。

BlocBuilder<BlocA, BlocAState>(
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

指定bloc时,仅希望在单个组件内访问,并且无法通过 父BlocProvider 和 当前的 BuildContext 访问。

BlocBuilder<BlocA, BlocAState>(
  bloc: blocA, // provide the local bloc instance
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

为 BlocBuilder 提供可选的 condition,来控制 builder函数何时被调用。condition 携带 bloc先前的状态和当前的状态,并返回布尔值。如果返回 true, builder 随着 currentState 被调用,组件重新构建;返回false,不会被调用也不会重建。

BlocBuilder<BlocA, BlocAState>(
  condition: (previousState, currentState) {
    // return true/false to determine whether or not
    // to rebuild the widget with currentState
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)
  • BlocProvider

是通过 BlocProvider.of(context) 为子级提供bloc的 一个组件。它用作依赖项注入(DI)组件,以便bloc的单例可以提供给子树的多个组件。

大多数情况下,BlocProvider 应该构建新的 blocs 用于其他子树。BlocProvider 自己创建bloc,也自动处理bloc。

BlocProvider(
  builder: (BuildContext context) => BlocA(),
  child: ChildA(),
);

一些情况下,BlocProvider为组件树的新部分提供已存在的bloc。最常见的,将已存在的bloc用于新路由。此时BlocProvider不会自动处理bloc。

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

然后,从ChildA() 和 ScreenA() 中检索 BlocA:

BlocProvider.of<BlocA>(context)
  • MultiBlocProvider

是一个合并多个 BlocProvider组件成一个的组件。提高代码可读性并消除嵌套。

使用BlocProvider:

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

使用MultiBlocProvider:

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

是一个携带BlocWidgetListener、可选bloc,并调用 listener 以响应bloc中状态改变的组件。一旦状态改变(如 导航、展示SnackBar、展示对话框等)它就需要发生。

不像 BlocBuilder 中的builder,每次状态改变(不包括初始状态 initialState)listener仅会被调用一次,并且是一个 void 函数。

如果bloc的参数被忽略,BlocListener 会利用 BlocProvider和 当前的 BuildContext自动执行查找。

BlocListener<BlocA, BlocAState>(
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: Container(),
)

仅希望提供一个无法通过 BlocProvider和 当前的 BuildContext 访问的bloc,才指定该bloc。

BlocListener<BlocA, BlocAState>(
  bloc: blocA,
  listener: (context, state) {
    // do stuff here based on BlocA's state
  }
)

如果想掌控何时listener函数 被调用,可以为 BlocListener提供一个可选的 condition。该condition带有先前的bloc状态和当前的bloc状态,并返回布尔值。如果condition返回 true,则使用 currentState 调用listener,如果返回 false,则不调用。

BlocListener<BlocA, BlocAState>(
  condition: (previousState, currentState) {
    // return true/false to determine whether or not
    // to call listener with currentState
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  }
  child: Container(),
)
  • MultiBlocListener

是一个合并多个 BlocListener 成一个的组件。提高代码可读性并消除嵌套。

使用BlocListener:

BlocListener<BlocA, BlocAState>(
  listener: (context, state) {},
  child: BlocListener<BlocB, BlocBState>(
    listener: (context, state) {},
    child: BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
      child: ChildA(),
    ),
  ),
)

使用MultiBlocListener:

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

是一个使用 RepositoryProvider.of(context) 为它的子级提供存储库的组件。被用作依赖注入(DI)组件,为多个子级组件提供 repository单例。BlocProvider 应被用作提供 blocs,但是 RepositiryProvider仅被 repositories 使用。

RepositoryProvider(
  builder: (context) => RepositoryA(),
  child: ChildA(),
);

然后从 ChildA 可以检索 Repository 实例:

RepositoryProvider.of<RepositoryA>(context)
  • MultiRepositoryProvider

是一个合并多个 RepositoryProvider 成一个的组件。提高代码可读性并消除嵌套。

使用RepositoryProvider:

RepositoryProvider<RepositoryA>(
  builder: (context) => RepositoryA(),
  child: RepositoryProvider<RepositoryB>(
    builder: (context) => RepositoryB(),
    child: RepositoryProvider<RepositoryC>(
      builder: (context) => RepositoryC(),
      child: ChildA(),
    )
  )
)

使用MultiRepositoryProvider:

MultiRepositoryProvider(
  providers: [
    RepositoryProvider<RepositoryA>(
      builder: (context) => RepositoryA(),
    ),
    RepositoryProvider<RepositoryB>(
      builder: (context) => RepositoryB(),
    ),
    RepositoryProvider<RepositoryC>(
      builder: (context) => RepositoryC(),
    ),
  ],
  child: ChildA(),
)



使用

接下里使用 BlocBuilder 将 CounterPage 连接到 CounterBloc。


conuter_bloc.dart

enum CounterEvent { increment, decrement }class CounterBloc extends Bloc<CounterEvent, int> {
  @override
  int get initialState => 0;
​
  @override
  Stream<int> mapEventToState(CounterEvent event) async* {
    switch (event) {
      case CounterEvent.decrement:
        yield currentState - 1;
        break;
      case CounterEvent.increment:
        yield currentState + 1;
        break;
    }
  }
}


counter_page.dart

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
          return Center(
            child: Text(
              '$count',
              style: TextStyle(fontSize: 24.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.dispatch(CounterEvent.increment);
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              onPressed: () {
                counterBloc.dispatch(CounterEvent.decrement);
              },
            ),
          ),
        ],
      ),
    );
  }
}

成功的将展示层和逻辑层分离。当点击按钮时,CounterPage组件并不知道发生什么。该组件仅简单的告诉 CounterBloc 用户按下了递增或者递减按钮。



Demo

接下来,构建一个

计数器Demo

,详细注释请参考demo。


创建项目


详情见

Flutter示例系列(一)之创建项目


在 pubspec.yaml 文件中引入 flutter_bloc 依赖库:

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^0.21.0

demo只有两个按钮来控制加或减,一个文本组件显示当前值,以此设计 CounterEvents。


Counter Events

enum CounterEvent { increment, decrement }


Counter States


状态就是整型值,因此不需要自定义类


Counter 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 currentState - 1;
        break;
      case CounterEvent.increment:
        yield currentState + 1;
        break;
    }
  }
}


Counter App

void main() => runApp(MyApp());class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: BlocProvider<CounterBloc>(
        builder: (context) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

使用 BlocProvider组件,让整个子树(CounterPage)都可以获取 CounterBloc实例。它也可以自动处理CounterBloc,因此不需要使用 StatefulWidget.


Counter Page

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
          return Center(
            child: Text(
              '$count',
              style: TextStyle(fontSize: 24.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.dispatch(CounterEvent.increment);
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              onPressed: () {
                counterBloc.dispatch(CounterEvent.decrement);
              },
            ),
          ),
        ],
      ),
    );
  }
}

在main.dart中,把CounterPage包裹在BlocProvider中,因此使用 BlocProvider.of(context) 获取 CounterBloc实例。

为了相应状态改变时重建UI,因此使用 BlocBuilder组件。

BlocBuild带有可选的bloc参数,但是也可以指定bloc类型和状态类型,BlocBuild会自动找到bloc的类型,因此不需要显式使用 BlocProvider.of(context)。

注意:如果在BlocBuild中指定一个bloc,那作用域仅限该组件,并且无法通过父类的 BlocProvider 和当前的BuildContext 访问。



总结

由此,我们完成了计数器demo,成功的将展示层和逻辑层分离。当用户点击按钮时,CounterPage仅仅发送事件到 CounterBloc。此外,CounterBloc也仅仅转换事件返回整型值。


本文Demo地址



Bloc git地址



Bloc官方文档

=================================================================


个人博客



Github


个人公众号:Flutter小同学


个人网站



版权声明:本文为Crazy_SunShine原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。