[TS-002] 带你看文档之 Bloc

这篇文章通过带着阅读一遍 bloc 的文档,并补充了 bloc 文档中涉及的知识点,尝试让新手可以快速入门 bloc 的学习。

更新于 2025.6.8,Bloc 9.0.0,文章如果过期需要自行甄别

文档首页:https://bloclibrary.dev/

这是什么

话会扯得很长,但是又不得不介绍一下新的系列——《带你看文档》,这是一个新坑,预计每周一更,因为最佳实践从来不是造轮子,而是更好的使用工具和工具的生态。

这一期就来读 Bloc 的文档,Bloc 是 Flutter 开发中常用的状态管理库,他的优点不必赘述,作用也不必赘述,作为新手开发 Flutter,无脑入手 Bloc 只会让你学到更多知识,比如提供者消费者模式、数据不可变性、测试驱动开发、模块化架构等等,多说无益,立即阅读!

文档大纲

  1. Introduction:介绍部分涵盖了必要的概念解释,属于是必看必读,不过一会儿会用简短的概述在下面总结一下
  2. Linter:这应该是 9.0.0 新增的板块之前应该是没有,或者说没有单独成为一个板块列举出来,如果希望良好的开发体验,比如快速检查代码错误的书写、优化代码格式、甚至是统一团队开发的代码风格
  3. Tutorials: 一些教程,属于是最容易被遗漏,但是很干货的部分都在这里了,但是长篇的教程是无聊的,我会总结一些必要的知识点和索引链接,减轻读文档的负担,但是!还是建议有空自己阅读一下
  4. Tools:这个教你怎么配置插件的,和上面的 Linter 一样,可以不看,但是看了可以提高开发体验。
  5. Reference:这个和传统意义的文末 ref 不一样,这里引用的库都是一些面向具体某种场景的包,用于解决特定的业务问题。比如防抖节流 bloc_concurrency、撤销重做 replay_bloc、缓存处理 hydrated_bloc,还有各种工具和测试包,是一整个开发生态的各种子模块集合。(这里捧一踩一,Getx 全部放在一起,缺少测试和解耦,简直和危楼一样)

快速入门

在阅读繁杂的概念之前,可以通过第一个教程来了解这一些是怎么工作的,我知道 Bloc 在简单的计数器程序上非常的臃肿,称得上杀鸡焉用牛刀,但是直接啃概念又会太过于枯燥。所以我简短的概括一下第一步要了解什么:

Bloc 是怎么工作的

这点在 https://bloclibrary.dev/bloc-concepts/ 中有说,但我还是会省流的概述一下,Bloc 就是一个将各种事件变成状态的工厂,你总是会输入各种事件给 Bloc,然后 Bloc 会输出当前的状态

Bloc 会更强调事件 Event 的概念,比如用户点击了点赞按钮、输入框输入了新的内容、一个页面打开了该初始化了,然后转化为对应的状态 State:点赞数的状态 +1、输入框内容更新校验后是否合法的状态、甚至是最简单的加载到完成的状态。

那复杂的就说完了,很多时候我们是不想定义 Event 的,所以 Bloc 有个简化的版本,就是 Cubit,如果状态简单,触发的事件简单,那完全可以就写一个 Cubit。它输入是靠函数的调用,输出也是 State。

知道这些就够了,因为这是 Bloc,它是一个概念性的东西,和框架无关,因此如果我们想在 Flutter 中使用 Bloc,我们需要使用的是 flutter_bloc。(如果你要在一种上古屠龙神技 angular 中使用 bloc,也有 angular_bloc)

这里还要踩一捧一,一个模块化的库,可以任意添加自己的实现,这符合单一职责原则和开闭原则。如果是 Getx 想放到某个 web 框架里使用,就几乎不可能复用,而且一旦重写就是全部。

flutter_bloc 是怎么工作的

Flutter Bloc Concepts
An overview of the core concepts for package:flutter_bloc.

Flutter 刷新 UI 脱离不了 setState 给组件树标脏,再下一帧去触发刷新。flutter_bloc 就是帮忙做了这样的事,但是 api 更易读。常见的,我把几个 flutter 里默认约定的 api 叫原语,可以参考 [KF-001] Flutter 状态管理的三个“原语”

watch 一个状态

当你要监听某个状态,来刷新 UI 的显示时,可以用 BlocBuilder 或者 watch 来实现。一般都是页面上元素的变化,比如播放器的状态、页面加载状态、空态、错误态等。

read 一个事件

当你想触发某个事件的时候,你可以使用 BlocProvider.of 或者 read

  • 读取对应类型的 Cubit 实例,然后调用对应的方法。
  • 读取对应类型的 Bloc 实例,然后通过 add 方法添加一个事件。

一般用于按钮点击,状态变更回调,触发初始化方法等。

listen 一个状态

同样是状态的监听处理,但是用于弹出对话框、跳转路由、触发其他会刷新 UI 的事件,这是一个很特别的情况,因为 Flutter 不能在 UI 刷新的过程中,再次触发刷新,这会让 Flutter 抛出异常,UI 变得不稳定。

所以 listen 要做的就是拿到状态后,延迟一帧去刷新 UI,可以使用 BlocListener 来实现。

select 一个状态(额外补充)

当状态属性复杂,但只想监听状态中的某一个属性,例如用户信息中的一个 name 字段,就可以用 select 去筛选需要的元素,可以用 BlocSelector 或者 select 来实现。

本质上就是一种带有性能优化的 watch,可以避免状态对象刷新时,属性的无关 UI 也刷新。

其他

Bloc 确实有很多冗余的 api:

比如 multi 系列,但是不难理解,就是把嵌套的 api 打破。

BlocConsumer 就是 同时具备 watchlisten 的聚合物,毕竟不是所有的状态都可以只用 watch 处理。

Repository 系列组件只是一种特殊的 BlocProvider,会将实例对象挂载在树上,以便于访问,也就是常说的 DI(dependency injection),不过我推荐这个部分也可以用 get_it 代替掉。

所以现在清晰一点了吗,虽然我一行代码都没贴。

回来读 counter

Flutter Counter
An in-depth guide on how to build a Flutter counter app with bloc.

一个计数器程序被分解为几个类:

  • CounterPage:通过 BlocProvider create 创建 Cubit 实例,并挂在树上
  • CounterCubit:实现了简单的 +1 -1 逻辑
  • CounterView:状态使用 watch 监听,事件使用 read 触发

我们暂且不谈 Observer 和 barrel,这个 example 现在看下来是不是非常易懂。

已经足够了

我是说上面的掌握了之后,你已经可以去写 bloc 代码了,退出文章吧,接下来讲的内容需要很长的时间去学习了。

软件架构

前面说得那么简单的前提,是建立在一个良好的软件架构之上的,我结合 https://bloclibrary.dev/architecture/ 和我个人的理解,大致梳理一个架构:

  • 数据提供者,又叫数据层:是最脏的 CRUD,不要带一点业务在这里,这里就是处理底层 api 调用的,而且是最无趣,随时可以替换的。
  • 领域仓库层:这里会读取来自各种数据的结果,并聚合为领域对象,领域对象是一个 DDD 的概念,说人话就是这里定义了和业务最为相关的模型对象。而领域仓库要做的就是结合业务规则,调用对应的几个 api,该做缓存的做缓存,该持有状态的就通过一些手段去长期持有状态,以便于 BLoC 层的访问。
  • BLoC 层:其实可以是 VM 层、Controller 层,anyway,就是一个拿到领域仓库最简单的数据状态,和触发数据突变事件的位置,所以 Bloc 总是会写得薄薄的一层,真实的复杂业务不会依赖在 Bloc 里面,这样也方便某一天不使用 Bloc 而转为使用其他框架的时候。
  • 视图层:这里处理用户交互,添加事件到 BLoC,监听 BLoC 来的状态,并转化为 UI。

这里有个很好的例子,还是来自最简单的,最喜闻乐见的独立软件三部曲的 todo 软件,但是你看看人家 bloc 作为 example 举例是如何写的

Flutter Todos
An in-depth guide on how to build a Flutter todos app with bloc.

一个数据层,都拆为数据层的 api 模块,和 local storage 的具体实现模块,如果以后它的数据来自 firebase、来自自托管服务、来自任何其他云端服务或者本地服务,都可以单独做一个模块,而且都不影响他原有的代码。这就是开闭原则、接口隔离。

然后是 repo 层,这里虽然写的很简单,甚至是违反我说的不要做“二传手”代码,但是这里是必要的,假如以后本地和远程的数据要做同步,聚合就在 repo 做,数据合并、同步也在 repo 做,多源服务供应商切换也在 repo 做。这里是上班之后经常要去接触的,去编写的业务层代码。

来到 Bloc 层,简单状态走 cubit,复杂状态走 Bloc,定义好所有的用户操作事件和状态,再来编排 BLoC。

最后 UI 层,数据注入、监听刷新、触发事件、弹出消息,就不细说了,都是前面的 1+1=2,现在无非变成 -3+5-2=0 罢了。

跨模块通讯

这里其实我们会有疑问的,不是任何项目都简单得一条流水线下来就完事了,不然还谈这些软件架构干什么呢。举个例子,有一个用户会员信息的 Repository,有一个根据不同会员信息拉取不同数据的 Repository,如果我的会员信息改变,我是希望依赖会员信息的这个 Repo 会刷新数据的。怎么实现呢?

传统办法——加一层 DomainService

Repo 已经不够用了,不同的领域对象之间也有业务耦合,我们不得不加一层领域服务对象来解决问题。但这放在学术里,放在后端微服务里是可行的,但我们只是一个小小的 app,大可不必如此。

错误做法—— eventBus

我不得不再提这个库,使用字符串确实可以解耦模块间的通讯,但是也会带来难以维护的 hard code,就算是 enum 也很难解决这种奇妙的数据流消耗,因为无论有没有模块在监听,发送事件的一端都会发送,这是不可理喻的消耗。

既然用了状态管理的库,就有状态管理库的解决方案

通过跨模块监听状态实现

有几种模式来实现,都在文档里说明了:

  • 通过 bloc 依赖另一个 bloc:我不推荐这个方式,它真的很奇怪,假如相互有依赖,就会形成环形依赖,导致数据沿着依赖环不停传递,而且难以 debug。
  • 常见做法是 BlocListener:毕竟你是知道数据的生命周期的,将数据的生命周期,绑在组件树上,和需要数据的页面的生命周期一起创建,一起销毁,这样非常易于维护,而且侵入性极低。但容易让业务脱离上下文,增加维护时的理解成本(但如果是 userInfo 这类常用的数据状态,就还可以接受)
  • 或者直接将会员 Repo 的状态写成流,让其他模块去直接依赖这个会员模块,并监听数据变化。不过这个也会形成和方案一一样的循环依赖问题,但是由于是 Repo 层,配合 get_it 和 injectable 就可以在编译之前发现循环的依赖。而且业务都较为集中。代价是需要编写更多持有状态的类,和较多的关于流的代码。

数据不可变性

我们会遇到状态更新后,UI 却没有改变的情况,特别是使用了一些集合数据类型的时候。原因是对象修改了属性,但是对象本身,在修改前后,对象都是同一个引用,如果熟悉 Java 就知道是怎么回事,因为虚拟机声明的对象在堆上,但是比较对象的时候只比较了类型和 hash。这应该是这类编程语言老生常谈的问题了。参考 [KF-005] Flutter 中是如何比较对象是否相等的

但其实我们没有那么多可变的数据,状态一定只有在状态管理的位置发生突变,所以我们需要不可变的对象,也就是我们说的不可变性。当对象改变之后就直接创建新对象,这样就解决了前面说的 UI 不刷新的问题。同时也引入了更好维护的数据处理方案。

文档中也对此有类似的问题描述和解决方案:https://bloclibrary.dev/modeling-state/

文档中推荐的是 equatable,这个不会代码生成,但是要手写部分代码,我推荐 freezed,但是需要会使用代码生成。深入的就不介绍了,自己翻文档吧。

如何定义状态模型

其实在状态管理里面,用枚举去定义一些状态的情况太常见了,但是对于初学者来说,什么是状态都很难定义得很好,往往就会出现重复、缺失、歧义。这个在文档中有命名规范相关的描述,至少至少命名按照这里来,不要出现 loaded loading 这样的状态,这是一组歧义的定义。

状态也有一些预制菜版本,比如 riverpod 的默认异步 wrapper,flutter 的 snapshot,flutter 文档中提供的 command。但这些状态都只提供了很基础的定义,如果要写好一个复杂的状态,就需要深入的学习,可以参考学习音频播放器的状态的定义 https://pub.dev/packages/just_audio#the-state-model ,或者去系统的学习状态流图怎么画,一个复杂的业务状态是避免不了这些定义的。

其他的其他

很高兴你能看到这里,是的,这篇文章有些草率,而且时间比较赶,上班之后整个 EPLab 的事情都停更了。较多的文字和 0 配图的段落,完全讲不完庞大的知识体系,只能到处挖坑,日后再填。希望能对于入门 Bloc 框架,有一星半点的帮助,以及入门之后如何继续学习,指出一个方向。

读文档就是如此,文档不是看一遍就会的,可能你也会像我一样,抱着 bloc 的文档,反反复复的看了三四年,然后总能发现新东西,这是一份好的文档能带给你的最好的收获,之所以想做这个系列也是这个原因。下一期想了解什么,欢迎在评论区留言!

Read more

[TS-001] Flutter 开发环境搭建思路

大家好,这里是优雅实践实验室,这一节将介绍如何搭建 Flutter 开发环境。 Flutter 开发环境是由什么组成的 eplab 只提供思路,不会手把手教学。 所以从思路出发。当提及配置 Flutter 开发环境的时候,我们到底在配置什么? 由 Flutter 的设计可知,Flutter 是编译后嵌入到目标平台的。 所以我们需要同时具备 Flutter 开发环境和目标平台的开发环境。 * Flutter 开发环境 * Flutter SDK * dart SDK * pub 命令 * IDE + Flutter 插件 * git 等环境杂项 * 目标平台的开发环境(按须配置) * 目标平台 SDK * 目标平台的模拟器(可选) Flutter 开发环境 需要配置基础的 Flutter SDK、IDE 及其插件、和一些杂项开发环境。

By lilua

[TS-999] Flutter 介绍

这个视频核心目的是扫盲,解答一些常见的 Flutter 相关的问题。 Flutter 跨平台开发支持哪些平台? 一般情况下说的是:生成产物可以运行在哪些平台,官方支持的是六大平台:Android、iOS、MacOS、Linux、Windows、Web。 开发平台支持使用四大平台:MacOS、Linux、Windows、ChromeOS。 容易被忽略的是 Flutter 是一个优秀的开源项目,良好的分层架构和可移植的 Dart 语言,让它本身就支持编译到任意一个嵌入式设备中,比如和丰田合作的车载系统、腾讯曾出过一个打印机系统、开源鸿蒙系统。 Flutter 是否支持鸿蒙开发? 正如上所示,Flutter 天生支持任何嵌入式设备开发,鸿蒙本质也是嵌入式设备。所以就技术本身而言,Flutter 是支持鸿蒙的。 就细而论,开源鸿蒙、鸿蒙Next、用户手上的鸿蒙,是否都可以用 Flutter 开发呢?还是那句话,技术本身是支持的,无论鸿蒙怎么变化。 Flutter

By lilua

[TS-000] Flutter 教学视频逐字稿介绍

这是优雅实践实验室产出视频之前的需要提前准备好的逐字稿脚本(Transcript),将会记录所有收集到的文档资料、代码片段、旁白台词和最终产出的视频。 💡注意:视频内容可能会过期,但是逐字稿会更新,在必要的时候重置当期对应的教学视频。 色彩搭配 主色 #042B59 #0553B1 #027DFD 辅色 #F25D50 #FFF275 #6200EE #1CDAC5 搭配方法 使用 Material Design 的配色,将主色和辅色两两搭配,得到一套主题,并遵循配色规范使用主题色卡。

By lilua