最近对手头上的项目进行重构,总结了有以下几个痛点:
- 状态管理混乱
虽然用了 provider
来做状态管理,但一些代码如:异步请求、事件响应等还是会掺杂在UI页面的代码里面,一旦页面的各种 Widget
多了起来之后,显得非常严重,而且对业务逻辑的测试也不方便,多个组件可能需要共享相同的数据或状态,需要在每个组件中分别创建 Provider
实例,容易导致代码冗余,如果只需要更新页面的部分 Widget
使用Provider
还会导致嵌套过深,也可能导致性能问题、状态不一致以及难以追踪的错误。
- 代码分层混乱,所有的代码都在一个工程里面,包括UI页面、
ViewModel
、Util
、网络请求和通用的 Widget
等,模块划分不清晰,模块依赖和调用混乱,而且各个模块没发单独开发和测试,没有明确的分层可能导致业务逻辑和 UI 代码混杂在一起,而且多个项目之间很难做到代码复用。
- 业务迭代导致API接口变动频繁,API接口返回的字段一旦出现类型解析问题就会导致出现崩溃,而目前负责
Json
映射的数据模型 Model
类在很多的 UI Widget
和 Bloc
中有使用到,修复这个问题就会导致大量依赖于此类的改动。而为了安全起见,数据模型类中的所有字段必须始终是这样的:int?
、string?
等,但是,这又可能会导致由于 NullPoInterException
而出现应用程序崩溃的风险。
- 路由管理问题,
fluro
作为路由管理必须依赖于 context
,如果在逻辑层需要处理页面跳转的话就会比较麻烦。而且参数传递只能作为转成字符串放在 path
中,然后再在注册路由的 Handler
中将字段解析出或者转成 Model
,使用起来多少有些不方便。
接下来系列文章来聊一聊怎解决上面的痛点,本篇文章先说一下状态管理方法,现有的项目需要一个能很好地应付大型项目的状态管理机制。
什么是状态管理
Flutter 状态管理是指在 Flutter 应用中有效地管理应用的数据和状态,以确保用户界面(UI)与数据之间的一致性和交互性。在复杂的应用中,数据通常会在不同的部分之间流动和变化,而状态管理的目标是帮助开发者更好地组织、更新和共享这些数据。
下面是使用比较多的状态管理方法及优缺点:
上面可以看出如果是小型项目用 setState()
或 InheritedWidget
,原生自带,简单快捷。大型项目可以选择更复杂的方法,如 Provider
、BLoC
或 Redux
。特别是对于复杂的应用逻辑,BLoC
和 Redux
提供了更强大的状态管理和业务分离,将逻辑业务从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
主要由以下几个部分组成:
State
: Bloc
包含一个状态对象,它存储了当前的应用状态。这个状态可以是任何数据类型,从简单的布尔值到复杂的对象都可以。
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; }
|
Bloc
: Bloc
中包含了实际的业务逻辑。根据接收到的事件,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 组件可以订阅 Bloc
的 Stream
来监听状态变化,同时通过 Sink
向 Bloc
发送事件,触发状态变化。
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
中发送网络请求,获取数据后再刷新页面,实际应用中会导入freezed
、injectable
和get_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' 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 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 页面为例,文件创建好后目录结构大概是这样的:
其中 home_event.freezed.dart
和 home_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.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技术实践”,原创技术文章第一时间推送。