Bloc 核心概念

由于官方文档没有中文版,所以自己翻译

一、 bloc

Events

Events是Bloc的输入。通常是为了响应用户交互(例如按钮按下)或生命周期事件(例如页面加载)而添加它们。

  • 在设计应用程序时,我们需要退后一步,定义用户将如何与之交互。在我们的计数器应用程序的上下文中,我们将有两个按钮来递增和递减我们的计数器。
  • 当用户点击其中一个按钮时,需要发生一些事情来通知我们的应用程序的“大脑”,以便它可以响应用户的输入。这是Events起作用的地方。
  • 我们需要能够将递增和递减通知我们应用程序的“大脑”,因此我们需要定义这些事件。
enum CounterEvent { increment, decrement }
  • 本例中,我们可以使用一个enum来表示事件,但在更复杂的情况下,则可能需要使用一个class(特别是需要向Bloc传递信息时)。
  • 至此,我们已经定义了我们的第一个event!注意,到目前为止我们还没有使用过Bloc,也没有发生任何神奇的事情。这只是普通的Dart代码。

States

States是Bloc的输出,代表应用程序状态的一部分。UI组件可以接收States的通知,并根据当前状态重绘其自身的某些部分。

到目前为止,我们已经定义了我们的应用将响应的两个Events:CounterEvent.incrementCounterEvent.decrement
现在需要定义如何表示我们应用的State
由于我们正在构建一个计数器,因此我们的state非常简单:它只是一个整数,代表了计数器的当前值。
稍后我们将看到状态的更复杂的示例,但是本例中,原始类型非常适合作为state表示。

Transitions

从一个state变成另外一种state叫做Transition。
一个Transition包含:

  • 当前state,
  • 触发event,
  • 将要变成的state

当用户与我们的计数器应用程序交互时,他们将触发Increment和Decrement事件,这些事件将更新计数器的状态。所有这些状态更改都可以描述为一系列Transitions。

例如,如果用户打开我们的应用并点击了增量按钮,我们将看到以下内容Transition。

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

由于记录了每个state变化,因此我们能够非常轻松地在对我们的应用程序进行检测的同时,跟踪所有用户交互和状态变化。另外,这使诸如时间旅行调试之类的事情成为可能

Streams

查阅官方文档来了解Streams Dart Documentation

  • Stream是一个异步数据序列

Bloc建立在RxDart之上;但是,它抽象了所有RxDart特定的实现细节。
使用Bloc,必须熟练掌握Streams用法及其原理。

  • 若你对Streams不熟悉,可以回忆一下家里的自来水管道(管道-Stream,水-异步数据)
    我们可以通过写一个async*方法,在Dart中创建一个Stream
Stream<int> countStream(int max) async* {
    for (int i = 0; i < max; i++) {
        yield i;
    }
}

通过把一个方法标记为async*,我们可以使用yield关键字并返回一个Stream数据。在上面的示例中,我们将返回一个最大整数参数的整型Stream

每次我们在async*函数中触发yield我们都会通过Stream推送该数据。

我们可以通过几种方式使用上面的Stream。如果我们想写一个函数来返回一个整数流的和,它可以是这样的:

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

通过将上述函数标记为async,我们可以使用await关键字并返回一个整数的Future。在本例中,我们等待流中的每个值,并返回流中所有整数的和。

我们可以这样组合:

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
}

Blocs

  • Bloc(Business Logic Component 业务逻辑组件)是将传入事件流转换为传出状态流的组件.(这是本章的重点)
  • 每个Bloc必须继承自Bloc基类(该基类源自core block package)
import 'package:bloc/bloc.dart';

class CounterBloc extends Bloc<CounterEvent, int> {

}

在上面的代码片段中,我们将CounterBloc声明为将CounterEvents转换为int的Bloc。

  • 每个Bloc必须定义一个初始状态,该状态是接收任何Events之前的状态。
    本例中,我们希望计数器从0开始
@override
int get initialState => 0;
  • 每个Bloc必须实现一个名为mapEventToState的函数。该函数将传入的event作为参数,并必须返回表示层使用的新状态流。我们可以在任何时候使用state属性访问当前的Bloc状态。
@override
Stream<int> mapEventToState(CounterEvent event) async* {
    switch (event) {
      case CounterEvent.decrement:
        yield state - 1;
        break;
      case CounterEvent.increment:
        yield state + 1;
        break;
    }
}
  • 完整的CounterBloc
import 'package:bloc/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 state - 1;
        break;
      case CounterEvent.increment:
        yield state + 1;
        break;
    }
  }
}
  • 注意:Bloc将忽略重复的状态。如果一个Bloc生成State nextState,其中State == nextState,则不会发生转换,也不会对Stream<State>进行更改。

这时你可能会问:如何用Event触发Bloc发出通知?

  • 每个Bloc都有一个add方法,这个方法接收一个event并触发mapEventToState.可以从表示层或者Bloc内部调用Add,并通知Bloc一个新的event
    我们可以创建一个从0到3的简单应用程序
void main() {
    CounterBloc bloc = CounterBloc();

    for (int i = 0; i < 3; i++) {
        bloc.add(CounterEvent.increment);
    }
}
  • 默认情况下,事件总是按照它们被添加的顺序处理,任何新添加的事件都会被加入队列。一旦mapEventToState完成执行,事件就被认为是完全处理过的。
    上面代码片段中的Transitions将是
{
    "currentState": 0,
    "event": "CounterEvent.increment",
    "nextState": 1
}
{
    "currentState": 1,
    "event": "CounterEvent.increment",
    "nextState": 2
}
{
    "currentState": 2,
    "event": "CounterEvent.increment",
    "nextState": 3
}

不幸的是,在当前状态下,我们将无法看到任何这些转换,除非我们重写onTransition

  • onTransition 是一个可重写的方法,用来处理每个本地Bloc的Transition转化
  • onTransition仅在Bloc的状态变化前调用
  • 提示:onTransition是个添加特定Bloc的日志/分析的好地方。
@override
void onTransition(Transition<CounterEvent, int> transition) {
    print(transition);
}

重写onTransition方法,我们可以方便的在Transition发生时自定义转化助理。

我们可以在bloc级别处理Transitions转化,也能处理Exceptions异常

  • onError是一个可重写的方法,用来处理每个本地Bloc的Exception异常。默认情况下,所以异常都会被忽略,不会影响Bloc的功能
  • 注意:如果状态流接收到一个没有stacktrace的错误,则stacktrace参数可能为null
  • 提示:onError是个添加特定Bloc的错误处理的好地方
@override
void onError(Object error, StackTrace stackTrace) {
  print('$error, $stackTrace');
}

通过重写onError我们可以方便的在Exception抛出时自定义异常处理

BlocDelegate

使用Bloc的一个额外好处是我们可以在一个地方访问所有Transitions。在本例我们只有一个Bloc,但是在实际开发中一般会有许多Bloc来管理应用程序状态的不同部分。
如果我们想要做一些事情来响应所有的Transitions转换,我们可以简单地创建我们自己的BlocDelegate

class SimpleBlocDelegate extends BlocDelegate {
  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print(transition);
  }
}
  • 注意:仅需要继承BlocDelegate类且重写onTransition方法
    为了让Bloc使用SimpleBlocDelegate,我们需要调整main()函数
void main() {
  BlocSupervisor.delegate = SimpleBlocDelegate();
  CounterBloc bloc = CounterBloc();

  for (int i = 0; i < 3; i++) {
    bloc.add(CounterEvent.increment);
  }
}

如果我们希望能够对添加的所有Events进行响应,我们还可以在SimpleBlocDelegate中重写onEvent方法。

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);
  }
}

如果我们希望能够对一个Bloc中抛出的所有Expections异常进行响应,我们还可以在SimpleBlocDelegate中重写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');
  }
}
  • 注意:BlocSupervisor是一个单例对象,它监视所有的Blocs并将职责委托给BlocDelegate。

二、flutter_bloc

(一)Bloc Widgets

BlocBuilder

BlocBuilder 是一个包含Blocbuilder方法的Flutter部件,它处理在构建部件的过程中的不同的状态。与StreamBuilder类似,但是API更简洁,代码量更少。builder方法是一个可能被多次调用,返回不同状态的widget的纯函数。

如果您想“做”任何事情来响应状态更改,如导航、显示对话框等,请参阅BlocListener
如果忽略了bloc参数,那么BlocBuilder将使用BlocProvider和当前的BuildContext自动执行查找。

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

只有当您希望提供一个范围限制的简单widget且不能通过父BlocProvider和当前BuildContext访问的bloc时,才指定该bloc。

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

若要在调用生成器builder方法时进行细粒度控制,可以为BlocBuilder提供一个可选条件.该条件获取以前的bloc状态和当前的bloc状态,并返回一个布尔值。如果条件返回true,builder会根据state重新调用,widget将重新构建。如果条件返回false,则不会调用builder,widget也不会重新构建。

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

BlocProvider是通过 BlocProvider.of<T>(context 提供一个bloc给其children的Flutter widget。它用作依赖项注入(DI)widget,因此可以将一个bloc的单个实例提供给子树中的多个widgets。

通常BlocProvider是用来创建新bloc,这些bloc将对子树的其余部分生效。下面代码中,由于BlocProvider负责创建bloc,它将自动处理关闭bloc。

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

在某些情况下,可以使用BlocProvider将现有bloc提供给widget树的新部分。现有bloc需要作用在一个新的route上时通常会这么用。下面代码中,BlocProvider不会自动关闭bloc,因为它没有创建它。

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

无论是ChildA还是ScreenA我们都能通过BlocProvider.of<BlocA>(context)检索BlocA

MultiBlocProvider

MultiBlocProvider可以将多个BlocProviderwidgets合并成一个。
MultiBlocProvider改进了可读性,可以避免嵌套多个Blocprovider的写法。通过使用MultiBlocProvider,我们可以从麻烦的旧写法:

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

到简洁的新写法

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”,并调用侦听器listener来响应bloc中的状态更改的一个Flutter widget。它应该用于每个状态变化会触发一次的功能,比如导航栏、显示一个SnackBar、显示一个Dialog等等。

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提供一个可选条件。该条件获取以前的bloc状态和当前的bloc状态,并返回一个布尔值。如果条件返回true,则使用state调用侦听器。如果条件返回false,则不会使用state调用侦听器。

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

MultiBlocListener是用来合并多个BlocListener成一个的widget。它能使可读性更好,避免了嵌套多个BlocListeners的写法。也就是你会从下面这种繁琐的旧写法:

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

到简洁的新写法:

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

RepositoryProvider是通过RepositoryProvider.of<T>(context)为其children提供一个repository存储库的Flutter widget。它是依赖项注入(DI)的widget,因此可以将repository存储库的单个实例提供给子树中的多个widgets。应该使用BlocProvider来提供blocs,而RepositoryProvider应该只用于repositories存储库。

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

通过ChildA我们可以依据RepositoryProvider.of<RepositoryA>(context)检索Repository

MultiRepositoryProvider

MultiRepositoryProvider是合并多个RepositoryProvider成一个的widget。它可读性更好,避免了嵌套多个RepositoryProvider的写法。我们可以将原先繁琐的写法:

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

替换为简洁的:

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

(二)Usage

让我们看看如何使用BlocBuilder将一个CounterPage widget连接到一个CounterBloc

counter_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 state - 1;
        break;
      case CounterEvent.increment:
        yield state + 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.add(CounterEvent.increment);
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              onPressed: () {
                counterBloc.add(CounterEvent.decrement);
              },
            ),
          ),
        ],
      ),
    );
  }
}

至此,我们已经成功地将展示层与业务逻辑层分离。注意,CounterPage widget不知道用户点击按钮时发生了什么。widget只是告诉CounterBloc,用户已经按下了递增或递减按钮。

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

推荐阅读更多精彩内容