Flutter状态管理学习手册(三)——Bloc

一、Bloc 介绍

Bloc 的名字比较新颖,这个状态管理框架的目的是将 UI 层和业务逻辑进行分离。Bloc 的复杂度处于 ScopedModel 和 Redux 之间,相较于 ScopedModel,Bloc 拥有分明的架构处于业务逻辑,相较于 Redux,Bloc 着重于业务逻辑的分解,使得整个框架对于开发来讲简单实用。

二、Bloc 的层次结构

Bloc 分为三层:

  • Data Layer(数据层),用于提供数据。
  • Bloc(Business Logic) Layer(业务层),通过继续 Bloc 类实现,用于处理业务逻辑。
  • Presentation Layer(表现层),用于 UI 构建。

Presentation Layer 只与 Bloc Layer 交互,Data Laye 也只与 Bloc Layer 交互。Bloc Layer 作为重要一层,处于表现层和数据层之间,使得 UI 和数据通过 Bloc Layer 进行交互。

image

由此可见,Bloc 的架构和客户端主流的 MVC 和 MVP 架构比较相似,但也存在 Event 和 State 的概念一同构成响应式框架。

三、Bloc 需要知道的概念

BlocProvider,通常做为 App 的根布局。BlocProvider 可以保存 Bloc,在其它页面通过BlocProvider.of<Bloc>(context)获取 Bloc。

Event,用户操作 UI 后发出的事件,用于通知 Bloc 层事件发生。

State,页面状态,可用于构建 UI。通常是 Bloc 将接收到的 Event 转化为 State。

Bloc 架构的核心是 Bloc 类,Bloc 类是一个抽象类,有一个 mapEventToState(event)方法需要实现。mapEventToState(event)顾名思义,就是将用户点击 View 时发出的 event 转化为构建 UI 所用的 State。另外,在 StatefulWidget 中使用 bloc 的话,在 widget dispose 时,要调用 bloc.dispose()方法进行释放。

四、Bloc 的实践

这里以常见的获取列表选择列表为例子。一个页面用于展示选中项和跳转到列表,一个页面用于显示列表。

image
  1. 引入 Redux 的第三方库

pubspec.yaml 文件中引入 flutter_bloc 第三方库支持 bloc 功能。

  # 引入 bloc 第三方库
  flutter_bloc: ^0.9.0
  1. 使用 Bloc 插件

这一步可有可无,但使用插件会方便开发,不使用的话也没什么问题。

Bloc 官方提供了 VSCode 和 Android studio 的插件,方便生成 Bloc 框架用到的相关类。
下文以 Android studio 的插件为例。

比如 list 页面,该插件会生成相应的类

image

从生成的五个文件中也可以看到,list_bloc 负责承载业务逻辑,list_page 负责编写 UI 界面,list_eventlist_state 分别是事件和状态,其中 list.dart 文件是用于导出前面四个文件的。

具体使用可见

Android studio 的 Bloc 插件

VSCode 的 Bloc 插件

  1. 使用 BlocProvider 作为根布局

main.dart 中,使用 BlocProvider 作为父布局包裹,用于传递需要的 bloc。Demo 中包含两个页面,一个是展示页面 ShowPage,一个是列表页面 ListPage。

上面讲到,Bloc 的核心功能在于 Bloc 类,对于展示页面 ShowPage,会有一个 ShowBloc 继续自 Bloc 类。由于展示页面 ShowPage 会和列表页面 ListPage 有数据的互动,所以这里将 ShowBloc 保存在 BlocProvider 中进行传递。

@override
  Widget build(BuildContext context) {
    return BlocProvider(
        bloc: _showBloc,
        child: MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData(
              primarySwatch: Colors.blue,
            ),
            home: ShowPage()));
  }
  1. 展示页面 ShowPage

① ShowEvent

列表的 item 点击后,需要发送一个 event 通知其它页面列表被选中,这里定义一个 SelectShowEvent 作为这种 event 通知。

class SelectShowEvent extends ShowEvent {
  String selected;

  SelectShowEvent(this.selected);
}

② ShowState

State 用于表示一种界面状态,即一个 State 就对应一个界面。插件在一开始会生成一个默认状态,InitialShowState。我们可以使用 InitialShowState 来代表初始的界面。另外,我们自己定义一种状态,SelectedShowState,代表选中列表后的 State。

@immutable
abstract class ShowState {}

class InitialShowState extends ShowState {}

class SelectedShowState extends ShowState {
  String _selectedString = "";

  String get selected => _selectedString;

  SelectedShowState(this._selectedString);
}

③ ShowBloc

Bloc 的主要职责是接收 Event,然后把 Event 转化为对应的 State。这里的 ShowBloc 继续自 Bloc,需要重写实现抽象方法 mapEventToState(event)。在这个方法中,我们判断传过来的 event 是不是 SelectShowEvent,是则拿到 SelectShowEvent 中的 selected 变量去构建 SelectedShowState。mapEventToState(event)返回的是一个 Stream,我们通过 yield 关键字去返回一个 SelectedShowState。

class ShowBloc extends Bloc<ShowEvent, ShowState> {
  @override
  ShowState get initialState => InitialShowState();

  @override
  Stream<ShowState> mapEventToState(
    ShowEvent event,
  ) async* {
    if (event is SelectShowEvent) {
      yield SelectedShowState(event.selected);
    }
  }
}

④ ShowPage

在 ShowPage 的界面上,我们需要根据 showBloc 中是否有被选中的列表项目去展于页面,所以这里我们先使用使用BlocProvider.of<ShowBloc>(context)去拿到 showBloc,接着再用 BlocBuilder 根据 showBloc 构建界面。使用 BlocBuilder 的好处就是可以让页面自动响应 showBloc 的变化而变化。

var showBloc = BlocProvider.of<ShowBloc>(context);
...
BlocBuilder(
    bloc: showBloc,
    builder: (context, state) {
      if (state is SelectedShowState) {
        return Text(state.selected);
      }
      return Text("");
    }),
  1. 列表页面 ListPage

① ListEvent

列表页面,我们一开始需要从网络中拉取列表数据,所以定义一个 FetchListEvent 事件在进入页面时通知 ListBloc 去获取列表。

@immutable
abstract class ListEvent extends Equatable {
  ListEvent([List props = const []]) : super(props);
}

class FetchListEvent extends ListEvent {}

② ListState

InitialListState 是插件默认生成的初始状态,另外定义一个 FetchListState 代表获取列表完成的状态。

@immutable
abstract class ListState extends Equatable {
  ListState([List props = const []]) : super(props);
}

class InitialListState extends ListState {}

class FetchListState extends ListState {

  List<String> _list = [];

  UnmodifiableListView<String> get list => UnmodifiableListView(_list);

  FetchListState(this._list);
}

③ ListBloc

在 ListBloc 中,进行从网络获取列表数据的业务。这里通过一个延时操作摸拟网络请求,最后用 yield 返回列表数据。

class ListBloc extends Bloc<ListEvent, ListState> {
  @override
  ListState get initialState => InitialListState();

  @override
  Stream<ListState> mapEventToState(
    ListEvent event,
  ) async* {
    if (event is FetchListEvent) {
      // 模拟网络请求
      await Future.delayed(Duration(milliseconds: 2000));
      var list = [
        "1. Bloc artitechture",
        "2. Bloc artitechture",
        "3. Bloc artitechture",
        "4. Bloc artitechture",
        "5. Bloc artitechture",
        "6. Bloc artitechture",
        "7. Bloc artitechture",
        "8. Bloc artitechture",
        "9. Bloc artitechture",
        "10. Bloc artitechture"
      ];

      yield FetchListState(list);
    }
  }
}

④ ListPage

在列表页面初始化时有两个操作,一个是初始化 listBloc,一个是发出列表请求的 Event。

  @override
  void initState() {
    bloc = ListBloc(); // 初始化listBloc
    bloc.dispatch(FetchListEvent()); // 发出列表请求事件
    super.initState();
  }

接下用,便是用 BlocBuilder 去响应状态。当 state 是 InitialListState,说明未获取列表,则显示 loading 界面,当 state 是 FetchListState 时,说明已经成功获取列表,显示列表界面。

body: BlocBuilder(
    bloc: bloc,
    builder: (context, state) {
      // 根据状态显示界面
      if (state is InitialListState) {
        // 显示 loading 界面
        return buildLoad();
      } else if (state is FetchListState) {
        // 显示列表界面
        var list = state.list;
        return buildList(list);
      }
    }));

最后,记得对 bloc 进行 dispose()

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

具体代码可以到 github 查看。

总结

在 Bloc 的架构中,将一个页面和一个 Bloc 相结合,由页面产生 Event,Bloc 根据业务需要将 Event 转化为 State,再把 State 交给页面中的 BlocBuilder 构建 UI。Demo 中只是给出了简单的状态管理,实际项目中,比如网络请求,有请求中、请求成功、请求失败的多种状态,可以做适当封装使 Bloc 更加易用。相比于 Redux,Bloc 不需要将所有状态集中管理,这样对于不同模块的页面易于拆分,对于代码量比较大的客户端而言,Bloc 的架构会相对比较友好。

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

推荐阅读更多精彩内容