代码在这里。
在Flutter生态里有很多的状态管理工具。比如,你一开始就会熟悉的setState
和provider
这些。在其他的工具里,Bloc这个模式非常流行。所以,我们花点时间来学一学。
本文继续用todo app作为例子,尽量接近真实开发环境。大部分主要的操作需要请求后端服务器。状态管理工具当然就使用Bloc模式来实现。另外,还会实现Bloc的单元测试,以此保证Bloc的类都能按照预想的工作。
Bloc是什么
在正式开始之前,如果你对Dart和Flutter有一定的了解。能理解Flutter的状态管理以及一些Flutter单元测试的知识就更好了。
Bloc的意思是Business Logic Component,这个模式用于分离逻辑和用户界面(UI)组件。这样可以让开发者更好的维护代码。
Bloc模式包含一下的部分:
- Bloc:这个是核心部分。它包含业务逻辑和app的状态管理。
- 事件:Bloc会对于每个事件做出反应。事件可以是用户的操作(比如,按钮点击)也可以是外部对数据的更新。
- 状态:状态是用来代表app可以存在的各种状态。Bloc发出更具不同的事件发出状态。UI则根据状态的变化做出反应,绘制出不同的界面。
- 用户界面:用户界面根据状态的变化呈现不同的界面。当Bloc发出一个新的状态,界面就更具这个新的状态呈现一个新的几面。
基本上,Bloc模式会根据不同的事件产生出新的不同的状态。之后,用户界面观察状态的更新,更具新的状态绘制界面。为了更加深刻的说明这个问题,假设我们的todo app,可以点一个按钮表示完成,再点一次表示未完成。
- 事件 :
- 事件完成按钮的toggle事件。未完成的点了就是完成,完成了的点了就是未完成。
- 状态 :
- 初始状态:未完成
- 已完成状态:从未完成转变为已完成
- 未完成状态:从已完成变为未完成
实际代码与这个会有些许不同。已完成会分为:已完成-成功 和已完成-失败。
调用远程API了,Bloc怎么管理状态
这里todo app又可以上场了。这个todo app调用远程API,并在app内使用Bloc管理状态。最后添加上单元测试(或者要尝试测试驱动开发也可以先写测试)。
这个todo app也是老朋友了,已经服务与Riverpod和GetX了。如果你看过这些文章再看这个会觉得简单。那么首先来添加所需要的依赖吧:
yaml
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.5
go_router: ^14.1.0
json_annotation: ^4.9.0
http: ^1.2.1
equatable: ^2.0.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
json_serializable: ^6.8.0
build_runner: ^2.4.9
bloc_test: ^9.1.7
mocktail: ^1.0.3
执行命令:flutter pub get
,这些依赖就安装好了。
Service类
现在来写Service类。这些不用专门写了,直接从其他的todo app里拿过来就可以用。为了方便直接看本文的读者,这里来简单描述一下。有一个http请求的util类:
dart
class HttpRequest {
final String hostUrl = 'http://address_of_your_server:17788'; // 1
var _client = http.Client(); // 2
set client(c) => _client = c;
get client => _client;
Future<List<TodoItem>?> fetchTodoList({
bool all = false,
String completed = 'completed',
}) async {
// ...
}
Future<TodoItem?> fetchTodoById(int todoId) async {
// ...
}
Future<TodoItem> addTodoItem(String todoTitle) async {
// ...
}
Future<void> updateTodo(int id,
{String? todoTitle, String? note, int? status, int? deleted}) async {
// ...
}
Future<void> deleteTodo(int id) async {
// ...
}
}
- address_of_your_server 就是你的服务器地址,你的服务在本机的话可以写本机的ip地址,注意不是localhost或者127.0.0.1,是你的ip地址。要不然在移动端访问不到。
- 需要使用
client
,否则单元测试不方便。更多可以参考关于Flutter测试部分的内容。
其他的方法就是对于todo的操作了。比如,获取todo列表、对todo的CRUD等。在其他的service class里可以使用。
注意:这些service类只是为了让整个的app尽量贴近实际开发而写的。读者在实际开发的时候根据实际情况决定如何操作。
获取todo列表的service类:
dart
class TodoListRepository {
Future<List<TodoItem>?> fetchTodoList({
bool all = false,
String completed = 'completed',
}) async {
HttpRequest request = HttpRequest();
return request.fetchTodoList(all: all, completed: completed);
}
}
更新todo的状态的service类:
dart
class TodoDetailRepository {
Future<TodoItem?> toggleTodoStatus({required TodoItem todo}) async {
HttpRequest request = HttpRequest();
try {
request.updateTodo(todo.id!, status: todo.status);
return todo;
} catch (err) {
return null;
}
}
}
实体类
这里单独拿出来是非常有必要的。
记得在依赖里有一项是equatable
不。这个是用来比较两个实例是否"存在变化"的。在Bloc 中,它还会被用在状态的对比上。是否要重新绘制界面取决于状态是否有变化。equatable
也可以用于对比两个状态是否发生了变化。
dart
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
import 'package:json_annotation/json_annotation.dart';
part 'todo_model.g.dart'; // 1
@immutable
@JsonSerializable() // 2
class TodoItem extends Equatable { // 3
const TodoItem({
this.id,
required this.content,
this.note,
this.deleted,
this.status,
});
final int? id;
final String? content;
final String? note;
final int? deleted;
final int? status;
static fromJson(Map<String, dynamic> json) => _$TodoItemFromJson (json); // 4
Map<String, dynamic> toJson() => _$TodoItemToJson(this); // 4
@override
List<Object?> get props => [id, content, note, deleted, status]; // 5
}
- 1、2、4都会在使用Json实体代码生成的时候用到。
- 3、5会在
equatable
对比中用到。尤其5,是在对比中需要对比的属性,如果这些属性有一个不相等,那么两个对象就是不相等的。
Bloc类
前面的内容都做好咱们就可以正式开始Bloc类的开发了。在开发中你会发现Bloc类连接了Repository层和UI层,Bloc类在中间正好有效的分割了业务逻辑和UI展示。
先来看看一个Bloc类是什么样子的:
dart
class TodoListBloc extends Bloc<TodoListEvent, TodoListState> { // 1
TodoListBloc() : super(const TodoListInitialState()) {
on<TodoListRequested>(_onFetchTodoList); // 2
on<TodoItemUpdated>(_onTodoItemUpdated);
}
}
- 一个Bloc类需要继承
Bloc<E, S>
。E
是事件类型,S
是状态类型。因此,需要先定义事件和状态。 - on()就是用来根据事件执行逻辑代码,之后发出(emit)对应的状态的方法。
定义事件
事件的定义比较简单。在界面显示的时候开始加载todo列表。所以,只需要一个数据请求事件:TodoListRequested
。
dart
sealed class TodoListEvent extends Equatable {}
class TodoListRequested extends TodoListEvent {
@override
List<Object?> get props => [];
}
定义状态
在定义状态的时候还需要考虑一个问题。在本里中,获取所有todo列表的时候是通过网络请求实现的。这需要一个过程:发出请求-->后台解析请求,并返回-->数据返回前端,并在UI显示。在这个过程还需要考虑出错的情况。
所以,在定义状态的时候需要有一个
- 初始状态
- 数据加载完成
- 数据请求出错
从初始状态 到其他两个状态的过程在界面上就是loading。
dart
@immutable
sealed class TodoListState extends Equatable {
const TodoListState({required this.todoList});
final List<TodoItem> todoList;
@override
List<Object?> get props => [todoList];
}
class TodoListInitialState extends TodoListState {
const TodoListInitialState() : super(todoList: const <TodoItem>[]);
}
class TodoListLoadedState extends TodoListState {
const TodoListLoadedState({required super.todoList});
}
class TodoListErrorState extends TodoListState {
const TodoListErrorState() : super(todoList: const <TodoItem>[]);
}
如你所见,在状态的基类中extends
了Equatable
。就像前文所说的,这个是为了比较两个状态的时候更加高效。
在TodoListLoadedState
中,构造函数有一个参数是todo列表。这是因为这个状态在emit的时候,需要带着后台返回的todo列表一起给UI展示用。
事件和状态都齐活了,那么就开始处理Bloc类了。
定义Bloc类
Bloc类的作用就是根据传入的事件,做逻辑处理,之后更具处理的结果发出不同的状态。
dart
class TodoListBloc extends Bloc<TodoListEvent, TodoListState> {
TodoListBloc() : super(const TodoListInitialState()) {
on<TodoListRequested>(_onFetchTodoList);
}
Future<void> _onFetchTodoList(
TodoListEvent event, Emitter<TodoListState> emit) async {
emit(const TodoListInitialState()); // 1
try {
final repository = TodoListRepository(); // 2
final result = await repository.fetchTodoList(all: true);
emit(TodoListLoadedState(todoList: result ?? [])); // 3
} catch (error) {
emit(const TodoListErrorState()); // 4
}
}
}
- 首先发出一个初始状态。
- Repository的实例在这里为了简单是直接初始化在需要使用的地方,其实也可以使用flutter_bloc提供的依赖注入工具。可以参考更新todo状态的部分的代码。
- 如果正确获得数据则发出
TodoListLoadedState
,里面带着todo列表。 - 如果不能正确处理请求,则发出
TodoListErrorState
。
在初始状态发出,正确获取数据或者错误获取数据的状态发出之前是数据加载中。在UI显示loading的界面元素让用户知道。
在UI中使用Bloc类
dart
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 略
),
body: BlocBuilder<TodoListBloc, TodoListState>(builder: (context, state) {
switch (state) { // 2
case TodoListInitialState():
// Loading...
case TodoListLoadedState(): // 3
return ListView.separated(
restorationId: 'sampleItemListView',
itemCount: state.todoList.length,
itemBuilder: (BuildContext context, int index) {
final item = state.todoList[index];
content: item.content ?? '',
// 其他略
},
separatorBuilder: (context, index) => const Divider(),
);
case TodoListErrorState():
// Error, 略...
}
}),
);
}
}
- 使用
BlocBuilder
来根据Bloc发出来的状态来绘制界面。 - 使用
switch
语句。根据state
的类型分别执行不同的分支。 - 在
TodoListLoadedState
状态里就可以使用状态带回来的todo列表。
现在里运行代码查看todo列表就差一点点了,不是运行api后台。
在app中配置Bloc
不配置的话,Bloc是没发使用的。比如前文中所述BlocBuilder
就用不了。下面来看看如何配置:
dart
void main() {
runApp(
MultiRepositoryProvider( // 1
providers: [
RepositoryProvider(
create: (BuildContext context) => TodoDetailRepository()) // 2
],
child: MultiBlocProvider(providers: [ // 3
BlocProvider<TodoListBloc>(
create: (BuildContext context) =>
TodoListBloc()..add(TodoListRequested())), // 4
// BlocProvider<TodoDetailCubit>(
// create: (_) => TodoDetailCubit(const TodoDetailState())),
], child: const MyApp())),
);
}
MultiRepositoryProvider
这是flutter_bloc提供的Repository的依赖注入工具。RepositoryProvider
配置了repository的初始化方式。如:
dart
RepositoryProvider(create: (BuildContext context) => TodoDetailRepository())
MultiBlocProvider
和repository provider类似的,这是一个多个bloc的provider。BlocProvider
配置bloc的初始化方式。
dart
BlocProvider<TodoListBloc>(
create: (BuildContext context) =>
TodoListBloc()..add(TodoListRequested()))
对于repository和bloc的provider的使用,统一方法是context.read<T>()
。比如上例中的repository provider可以这样使用context.read<TodoDetailRepository>()
。而bloc provider的使用方法是:context.read<TodoListBloc>()
。更多详细的内容可以参考文档。
To be continued...