0%

Flutter 大型项目架构:状态管理篇

最近对手头上的项目进行重构,总结了有以下几个痛点:

  1. 状态管理混乱
    虽然用了 provider 来做状态管理,但一些代码如:异步请求、事件响应等还是会掺杂在UI页面的代码里面,一旦页面的各种 Widget 多了起来之后,显得非常严重,而且对业务逻辑的测试也不方便,多个组件可能需要共享相同的数据或状态,需要在每个组件中分别创建 Provider 实例,容易导致代码冗余,如果只需要更新页面的部分 Widget 使用Provider 还会导致嵌套过深,也可能导致性能问题、状态不一致以及难以追踪的错误。
  2. 代码分层混乱,所有的代码都在一个工程里面,包括UI页面、ViewModelUtil、网络请求和通用的 Widget 等,模块划分不清晰,模块依赖和调用混乱,而且各个模块没发单独开发和测试,没有明确的分层可能导致业务逻辑和 UI 代码混杂在一起,而且多个项目之间很难做到代码复用。
  3. 业务迭代导致API接口变动频繁,API接口返回的字段一旦出现类型解析问题就会导致出现崩溃,而目前负责 Json 映射的数据模型 Model 类在很多的 UI WidgetBloc 中有使用到,修复这个问题就会导致大量依赖于此类的改动。而为了安全起见,数据模型类中的所有字段必须始终是这样的:int?string?等,但是,这又可能会导致由于 NullPoInterException 而出现应用程序崩溃的风险。
  4. 路由管理问题,fluro作为路由管理必须依赖于 context,如果在逻辑层需要处理页面跳转的话就会比较麻烦。而且参数传递只能作为转成字符串放在 path 中,然后再在注册路由的 Handler 中将字段解析出或者转成 Model,使用起来多少有些不方便。

接下来系列文章来聊一聊怎解决上面的痛点,本篇文章先说一下状态管理方法,现有的项目需要一个能很好地应付大型项目的状态管理机制。

什么是状态管理

Flutter 状态管理是指在 Flutter 应用中有效地管理应用的数据和状态,以确保用户界面(UI)与数据之间的一致性和交互性。在复杂的应用中,数据通常会在不同的部分之间流动和变化,而状态管理的目标是帮助开发者更好地组织、更新和共享这些数据。

下面是使用比较多的状态管理方法及优缺点:

状态管理方法优缺点

上面可以看出如果是小型项目用 setState()InheritedWidget,原生自带,简单快捷。大型项目可以选择更复杂的方法,如 ProviderBLoCRedux。特别是对于复杂的应用逻辑,BLoCRedux 提供了更强大的状态管理和业务分离,将逻辑业务从UI层中剥离,方便维护和拓展功能,但同时学习的复杂度也高一些。在项目初期选择合适的状态管理方法还是很重要的,提升效率的同时也避免了后面频繁更换引起大量代码的变更。

本系列文章主要是针对大型项目的架构来展开的,所以选择了 BLoC 作为状态管理方法,那为什么是 BLoC ?

Why Bloc?

Bloc makes it easy to separate presentation from business logic, making your code fast, easy to test, and reusable.

引用了 bloclibrary.dev 中一句话:Bloc 可以轻松地将展示层与业务逻辑分离,使您的代码快速、易于测试且可重用。

Flutter BLoC 架构中,应用的业务逻辑被组织为一个个的 BLoC。每个 BLoC 是一个独立的单元,一情况下对应一个页面(Page),负责管理这个页面特定的状态和逻辑,如网络请求、请求返回的数据解析处理、点击事件等等。BLoC 通过 Streams(流)和 Sink(数据源)与 UI 组件通信。这种分离的架构使得 UI 组件只需关注显示数据,而将数据获取、处理和变化交给了相应的 BLoC

Bloc 主要由以下几个部分组成:

StateBloc 包含一个状态对象,它存储了当前的应用状态。这个状态可以是任何数据类型,从简单的布尔值到复杂的对象都可以。

1
2
3
4
5
class HomeState  {
bool isShimmerLoading;
int selectedIndex;
dynamic responseData;
}

Event: 事件是触发状态变化的操作或用户动作。当用户与应用交互时,事件会被触发,然后 BLoC 根据事件来决定如何改变状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class HomeEvent {
const HomeEvent();
}

class HomePageInitiated extends HomeEvent {
const HomePageInitiated(this.status);
final AuthenticationStatus status;
}

class HomePageRefreshed extends HomeEvent {
const HomePageRefreshed(this.completer);
Completer<void> completer;
}

BlocBloc 中包含了实际的业务逻辑。根据接收到的事件,Bloc 可以对状态进行更新、计算、处理异步操作等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HomeBloc extends Bloc<HomeEvent, HomeState> {
HomeBloc() : super(HomeState()) {
on<HomePageInitiated>(
_onHomePageInitiated
);

on<HomePageRefreshed>(
_onHomePageRefreshed
);
}

FutureOr<void> _onHomePageInitiated(
HomePageInitiated event, Emitter<HomeState> emit) {
// 发出一个新状态,通知页面更新。
emit(state.copyWith(isShimmerLoading: false));
}

FutureOr<void> _onHomePageRefreshed(
HomePageRefreshed event, Emitter<HomeState> emit) {
}
}

UI Page:Bloc 使用 Streams 将状态传递给 UI 组件,UI 组件可以订阅 BlocStream 来监听状态变化,同时通过 SinkBloc 发送事件,触发状态变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Padding(
padding: const EdgeInsets.all(12),
child: BlocProvider(
create: (context) {
return HomeBloc();
},
child: BlocBuilder<HomeBloc, HomeState>(
buildWhen: (previous, current) => previous.isShimmerLoading != current.isShimmerLoading,
builder: (context, state) {
return GestureDetector(
onTap: () {
bloc.add(const HomePageInitiated());
},
child: ......,
);
},
)
),
),
);
}

这样做的优势也很明显:

  • 业务逻辑分离: Bloc 架构将业务逻辑从 UI 中分离,使代码更具可读性和可维护性,同时便于单元测试。
  • 可测试性: 由于 Bloc 的状态和逻辑都是独立的,可以轻松地进行单元测试,确保代码质量。
  • 状态可预测: 使用 Streams 将状态传递给 UI 组件,确保状态的一致性和可预测性。

Bloc 实际应用

接下来实现Bloc中发送网络请求,获取数据后再刷新页面,实际应用中会导入freezedinjectableget_it等依赖库,其中 freezed 是数据体代码生成器,injectable 搭配 get_it 解决依赖注入的问题。还有其它依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
name: app
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
sdk: ">=2.15.0 <3.0.0"

dependency_overrides:
analyzer: 5.2.0
build_resolvers: 2.1.0
dart_style: 2.2.4
ffi: ^1.2.0

dependencies:
flutter_localizations:
sdk: flutter
flutter:
sdk: flutter
cupertino_icons: ^1.0.2

# 以下这几个package后面会有文章单独来说
widgets:
path: ../widgets
shared:
path: ../shared
domain:
path: ../domain
resources:
path: ../resources
initializer:
path: ../initializer

injectable:
get_it:
flutter_bloc: 8.0.1
freezed_annotation: 2.2.0
flutter_screenutil: 5.5.3+2
auto_route: 5.0.4
loading_animation_widget: ^1.2.0+4
pull_to_refresh: ^2.0.0
url_launcher:

dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
injectable_generator: ^2.1.4
build_runner: 2.3.3
freezed: 2.3.2
auto_route_generator: 5.0.3
flutter_gen_runner: 4.1.6

flutter_gen:
output: lib/resource/generated
line_lenght: 160
null_safety: true

integrations:
flutter_svg: true

assets:
enabled: true

fonts:
enabled: true

flutter:
uses-material-design: true
generate: false
assets:
- assets/images/

以 Home 页面为例,文件创建好后目录结构大概是这样的:
image.png

其中 home_event.freezed.darthome_state.freezed.dart 需要手动创建好,内容是添加 @freezed 注解后自动生成的,不需要手动更改内容。

home_event.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../base/bloc/base_bloc_event.dart';
part 'home_event.freezed.dart';

abstract class HomeEvent extends BaseBlocEvent {
const HomeEvent();
}

@freezed
class HomePageInitiated extends HomeEvent with _$HomePageInitiated {
const factory HomePageInitiated() = _HomePageInitiated;
}

home_state.dart

1
2
3
4
5
6
7
8
9
10
11
12
import 'package:domain/domain.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:shared/shared.dart';
import '../../../base/bloc/base_bloc_state.dart';
part 'home_state.freezed.dart';

@freezed
class HomeState extends BaseBlocState with _$HomeState {
factory HomeState({
@Default([]) List<PlayListItemData> playList,
}) = _HomeState;
}

home_bloc.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import 'dart:async';
import 'package:app/base/bloc/base_bloc.dart';
import 'package:domain/domain.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:app/ui/home/bloc/home_event.dart';
import 'package:app/ui/home/bloc/home_state.dart';
import 'package:shared/util/log_utils.dart';

@Injectable()
class HomeBloc extends BaseBloc<HomeEvent, HomeState> {
HomeBloc(this._repository) : super(HomeState()) {
on<HomePageInitiated>(
_onHomePageInitiated,
transformer: log(),
);

on<UserLoadMore>(
_onUserLoadMore,
transformer: log(),
);

on<HomePageRefreshed>(
_onHomePageRefreshed,
transformer: log(),
);
}

final Repository _repository;

FutureOr<void> _onHomePageInitiated(
HomePageInitiated event, Emitter<HomeState> emit) async {
try {
List<PlayListItemData> playList = await _repository.playlist("");
// 获取数据后触发页面更新
emit(state.copyWith(playList: playList));
} catch (err) {
Log.e(err);
}
}
}

Repository 是数据请求的接口,其实现类是RepositoryImpl,通过注解@LazySingleton(as: Repository) 将实现类RepositoryImpl 注入到 di 中,这一块儿将在网络层的重构会说到。

HomePage_HomePageState 继承自 BasePageState,而 BasePageState继承BasePageStateDelegate 并在 BasePageStateDelegate 注册 Bloc

base_page_state.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
abstract class BasePageState<T extends StatefulWidget, B extends BaseBloc>
extends BasePageStateDelegate<T, B> with LogMixin {}

abstract class BasePageStateDelegate<T extends StatefulWidget,
B extends BaseBloc> extends State<T> implements ExceptionHandlerListener {
late final AppNavigator navigator = GetIt.instance.get<AppNavigator>();

late final B bloc = GetIt.instance.get<B>()
..navigator = navigator;

@override
Widget build(BuildContext context) {
return Provider(
create: (context) {},
child: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => bloc),
],
child: buildPageListeners(
child: isAppWidget
? buildPage(context)
: Stack(
children: [
buildPage(context),
BlocBuilder<CommonBloc, CommonState>(
buildWhen: (previous, current) =>
previous.isLoading != current.isLoading,
builder: (context, state) => Visibility(
visible: state.isLoading,
child: buildPageLoading(),
),
),
],
),
)
),
);
}

Widget buildPageListeners({required Widget child}) => child;

Widget buildPageLoading() => const Center(
child: CircularProgressIndicator(
color: Color(0xFF666666),
strokeWidth: 2,
),
);

Widget buildPage(BuildContext context);

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

home_page.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import 'package:app/ui/home/bloc/home_state.dart';
import 'package:flutter/material.dart';
import 'package:app/base/base_page_state.dart';
import 'package:app/common_view/common_scaffold.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/home_bloc.dart';
import 'bloc/home_event.dart';

class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);

@override
_HomePageState createState() => _HomePageState();
}

class _HomePageState extends BasePageState<HomePage, HomeBloc> {
@override
initState() {
super.initState();
// 向 bloc 添加 HomePageInitiated event
bloc.add(const HomePageInitiated());
}

@override
Widget buildPage(BuildContext context) {
return CommonScaffold(
backgroundColor: Colors.white,
hideKeyboardWhenTouchOutside: true,
body: BlocBuilder<HomeBloc, HomeState>(
buildWhen: (previous, current) =>
previous.playList != current.playList,
builder: (ctx, state) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return SizedBox(
height: 120,
child: Text(state.playList[index].name ?? ""),
);
},
separatorBuilder: (BuildContext context, int index) {
return Container();
},
itemCount: state.playList.length);
},
));
}
}

buildWhen 中对比前后数据不一样的时候就会触发更新 ListView 的显示,而且只需要判断监听 playList 这一个字段的数据变化,如果需要监听其它字段来更新其它的 Widget 时也互相不干扰,,整个没有多余的逻辑处理,UI 层干净清爽多了。以事件为驱动,事件一旦被触发,然后 BLoC 根据事件来决定如何改变状态,好处是这里的状态和逻辑都是独立的,可以轻松地进行单元测试来确保代码质量。

欢迎关注我的公众号“flutter技术实践”,原创技术文章第一时间推送。