Flutter开发 -- 使用Bloc管理状态

代码在这里

在Flutter生态里有很多的状态管理工具。比如,你一开始就会熟悉的setStateprovider这些。在其他的工具里,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 {
    // ...
  }
}
  1. address_of_your_server 就是你的服务器地址,你的服务在本机的话可以写本机的ip地址,注意不是localhost或者127.0.0.1,是你的ip地址。要不然在移动端访问不到。
  2. 需要使用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. 1、2、4都会在使用Json实体代码生成的时候用到。
  2. 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);
  }
}
  1. 一个Bloc类需要继承Bloc<E, S>E是事件类型,S是状态类型。因此,需要先定义事件和状态。
  2. 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>[]);
}

如你所见,在状态的基类中extendsEquatable。就像前文所说的,这个是为了比较两个状态的时候更加高效。

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
    }
  }
}
  1. 首先发出一个初始状态。
  2. Repository的实例在这里为了简单是直接初始化在需要使用的地方,其实也可以使用flutter_bloc提供的依赖注入工具。可以参考更新todo状态的部分的代码。
  3. 如果正确获得数据则发出TodoListLoadedState,里面带着todo列表。
  4. 如果不能正确处理请求,则发出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, 略...
        }
      }),
    );
  }
}
  1. 使用BlocBuilder来根据Bloc发出来的状态来绘制界面。
  2. 使用switch语句。根据state的类型分别执行不同的分支。
  3. 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())),
  );
}
  1. MultiRepositoryProvider这是flutter_bloc提供的Repository的依赖注入工具。
  2. RepositoryProvider配置了repository的初始化方式。如:
dart 复制代码
RepositoryProvider(create: (BuildContext context) => TodoDetailRepository()) 
  1. MultiBlocProvider和repository provider类似的,这是一个多个bloc的provider。
  2. 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...

相关推荐
m0_7482478015 小时前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
迷雾漫步者16 小时前
Flutter组件————PageView
flutter·跨平台·dart
迷雾漫步者1 天前
Flutter组件————FloatingActionButton
前端·flutter·dart
coder_pig1 天前
📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK
flutter·ubuntu·jenkins
捡芝麻丢西瓜1 天前
flutter自学笔记5- dart 编码规范
flutter·dart
恋猫de小郭1 天前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
sunly_2 天前
Flutter:导航,tab切换,顶部固定,列表分页滚动
开发语言·javascript·flutter
敲代码的小强2 天前
Flutter项目兼容鸿蒙Next系统
flutter·华为·harmonyos
Zh-jie3 天前
flutter 快速实现侧边栏
前端·javascript·flutter
truemi.733 天前
flutter --no-color pub get 超时解决方法
android·flutter