使用Bloc模式开发Flutter Todo应用
在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发出一个新的状态,界面就更具这个新的状态呈现一个新的几面。
开发准备
在开始开发前,我们需要准备好开发环境。对于iOS开发者来说,可以使用appuploader这样的工具来简化开发流程。appuploader是一款专业的iOS开发助手,可以帮助开发者快速完成证书管理、应用打包和上传等工作,让开发者能更专注于代码本身。
首先来添加所需要的依赖:
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';
var _client = http.Client();
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 {
// ...
}
}
Bloc实现
定义事件和状态
dart
sealed class TodoListEvent extends Equatable {}
class TodoListRequested extends TodoListEvent {
@override
List<Object?> get props => [];
}
@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>[]);
}
实现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());
try {
final repository = TodoListRepository();
final result = await repository.fetchTodoList(all: true);
emit(TodoListLoadedState(todoList: result ?? []));
} catch (error) {
emit(const TodoListErrorState());
}
}
}
在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) {
case TodoListInitialState():
// Loading...
case TodoListLoadedState():
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, 略...
}
}),
);
}
}
测试Bloc
dart
class MockTodoListRepository extends Mock implements TodoListRepository {}
blocTest<TodoListBloc, TodoListState>(
'emits [TodoListInitialState, TodoListErrorState] when http request throws an error',
setUp: () => when(mockRepository.fetchTodoList).thenThrow(Exception()),
build: () => TodoListBloc(todoListRepository: mockRepository),
act: (bloc) => bloc.add(TodoListRequested()),
expect: () => <TodoListState>[
const TodoListInitialState(),
const TodoListErrorState()
],
verify: (_) => verify(() =>
mockRepository.fetchTodoList(all: true, completed: 'completed'))
.called(1),
);
总结
通过本文我们学习了如何使用Bloc模式来管理Flutter应用的状态。Bloc模式通过将业务逻辑与UI分离,使代码更易于维护和测试。在实际开发中,可以结合appuploader这样的工具来简化iOS应用的打包和发布流程,提高开发效率。