原文链接:https://medium.com/coding-with-flutter/widget-async-bloc-service-a-practical-architecture-for-flutter-apps-250a28f9251b
作者:Andrea Bizzotto
难度评级:中高级,本文观点仅代表作者本人。
概述
如今,状态管理 是Flutter
的热门话题。
在过去的一年中,各种不同的状态管理技术被提出,但截至目前,Flutter
的团队和相关社区还没有得出单一的 首选解决方案。
这可以理解,因为不同的app
有着不同的业务需求,选择最合适的技术取决于我们正在尝试开发什么样的功能。
事实上,一些状态管理的技术被普遍使用:
-
Scoped Model
以其简单而著称 -
BLoC
也被广泛使用,借助于Streams
和RxDart
,它适用于更复杂的应用程序 -
在最近的
Google I/O
大会上,Flutter
团队向我们展示了如何使用Provider
包和ChangeNotifier
,用于在组件之间传递状态的更改。
有多种选择终归是件好事,但同时也可能会导致困惑,因此,选择一种能够随着app
的迭代依然能良好地运行、且具有优秀拓展性的技术非常重要。
更重要的是,尽早做出正确的选择可以为我们节省大量的时间和精力。
我对状态管理和app架构的看法
过去的一年中,我构建了若干大大小小的Flutter app
,期间我遇到并解决了许多问题,这让我明白了状态管理没有银弹。
然而,在构建完成并将它们一次次的重构之后,我调整出了一种在我所有项目中都能够运行完好的开发体系,因此,在本文中,我将介绍一种我定义的新的架构模式:
-
从现有的开发模式中借鉴了很多思想;
-
调整它们以满足实际开发
Flutter app
的需求。
在揭晓其真面目之前,我先来定义一些目标,这种模式应该:
-
1.只要基本模块清晰,代码就会更 简单易懂
-
2.能够 依葫芦画瓢 轻易追加新的功能
-
3.建立在
Clean
架构的原则之上 -
4.编写 响应式 的
Flutter app
时,该架构也能胜任 -
5.需要很少甚至没有样板代码
-
6.保证代码的可测试性
-
7.保证代码的可移植性
-
8.支持小型、可组合的小部件和类
-
9.与异步
API
轻松集成(Futures
和Streams
) -
10.适用于体量和复杂度逐步增长的应用程序。
在Flutter
现有的状态管理技术中,该模式在很大程度上依赖于 BLoCs ,并且非常类似于 RxVMS 架构。
闲言少叙,接下来我很荣幸地介绍:
Widget-Async-BLoC-Service 模式
简称:WABS (这很酷,会因为它包含我的缩写 :D)。
这种架构模式有四种变体:
1. Widget-Bloc-Service
2. Widget-Service
3. Widget-Bloc
4. Widget only
请注意:除了Widget
项外,BLoC
和Service
项 都是可选的。
换句话说:您可以根据具体情况适当地 使用 或 省略 它们。
现在,让我们通过更详细的图表探究完整的实现:
首先,该图表定义了应用三个的层级:
-
UI层 :当然不可或缺,因为它代表着控件所在的位置
-
数据层(可选):这是我们添加逻辑和修改状态的地方
-
服务层(可选):这是我们与外部服务进行通信的地方
接下来,让我们为每个层级定义一些可做和不可做的规则。
UI层
这是我们添置控件的地方。
控件可以是无状态或有状态的,但它们都不应包含任何 显式 状态管理的逻辑。
显式 状态管理的示例是 Flutter
计数器,当增量按钮被按下时,程序通过 setState()
对计数器进行值的递增。
隐式 状态管理的示例是 StatefulWidget
,它包含由 TextEditingController
管理的 TextField
。 这种情况下,我们需要StatefulWidget
,因为TextEditingController
引入了副作用——这样的好处是我们没有明确地管理任何状态。
UI层的控件可以自由调用由BLoC
或Service
定义的 同步 或 异步 方法,并可以通过StreamBuilder
对流进行订阅。
请注意上图是如何将单个控件连接到BLoC
的输入与输出,我们也可以使用这种模式将一个控件连接到输入,然后将另外一个控件连接到输出:
换句话说,我们可以实现一个 生产者-消费者 的数据流。
WABS 模式鼓励我们将所有状态管理的逻辑都移动到数据层,我们马上将了解它。
数据层
在数据层中,我们可以定义 局部 或 全局 应用程序的状态,以及修改它的代码。
这是通过业务逻辑组件(BLoCs
)完成的,这是在2018 DartConf
时首次引入的模式。
理想化的BLoC
是 将业务逻辑与UI层分离 ,并能够跨多个平台保证代码的高度可复用性。
在BLoC
模式下,控件能够:
-
将事件分发给接收器;
-
通过流通知状态的更新。
根据最初的定义,我们只能通过 接收器 和 流 与BLoC
进行通信。
虽然我喜欢这个定义,但我发现它在许多场景下限制性太强。 因此,在WABS中,我使用了一种名为 Async BLoC 的BLoC
变体。
它和BLoC
一样,我们有可以订阅的输出流;但是,BLoC
输入可以包括 同步接收器、异步方法 甚至 共同的两者。
换句话说,我们从这样:
变成了这样:
异步的方法可以:
-
1.将零个,一个或多个值添加到输入接收器。
-
2.返回一个
Future
的结果,调用的代码可以等待结果并相应地执行某些操作。 -
3.抛出一个异常,调用的代码可以通过
try/catch
捕获它,并在需要时展示一个警告。
稍后,我们将看到一个完整的例子,说明它在实践中的用处。
更多关于BLoC的信息
一个Async BLoC
可以定义一个StreamController/Stream
对,如果使用RxDart
,则等效对应定义一个BehaviorSubject/Observable
。
如果有需要,我们甚至可以执行高级的流操作,例如通过combineLatest
将流组合在一起。 但是要明确:
-
1.如果需要以某种方式组合,我建议在单个
BLoC
中使用多个流。 -
2.我不鼓励在一个
BLoC
中使用多个StreamControllers
。相反,我更喜欢将代码分割到两个或更多的BLoC
类中,以便更好地分离关注点。
数据层/BLoC中的行为
-
1.BLoC应该是纯Dart的——没有UI代码,没有导入
Flutter
相关类和文件,也没有在BLoC
中使用BuildContext
。 -
2.
BLoC
不应 直接 调用第三方相关代码,这应该是Service
做的。 -
3.控件和
BLoC
之间的接口应该和BLoC
和Service
之间的接口保证一致,也就是说,BloC
可以通过同步/异步方法直接与服务类通信,并通过流通知更新。
服务层
Service
类应该具有和BLoC
相同的输入/输出接口。但是,Service
和BLoC
之间存在一个本质性的区别,那就是:
-
BLoC
可以持有和修改状态。 -
Service
不能持有和修改状态。
换句话说,我们可以将Service
视为 纯粹 的功能组件, 它可以修改和转换从第三方库收到的数据。
示例: Firestore service
-
我们可以实现一个
FirestoreDatabase
的Service
作为Firestore
的指定域的API
包装器。 -
输入的数据(读取):将来自
Firestore
文档的键值对的流转换为强类型的不可变数据Model
。 -
数据输出(写入):将数据
Model
转换为键值对,以便写入Firestore
。
这种情况下,Service
类执行简单的数据操作。与BLoC
不同,Service
不具有任何状态。
关于术语的说明:对于与三方服务的通信的类,其他文章通常使用
Repository
来表述;甚至对于Repository
的定义也随着时间的推移而发展(有关更多信息,请参阅此文章)。 在本文中,我没有明确区分Service
和Repository
。
将其聚集在一起:使用Provider包
一旦我们定义了BLoC
和Service
,我们就需要将其与控件相关联。
这段时间以来,我一直在使用 Remi Rousselet 的 Provider 包。 这是一个纯粹基于InheritedWidget
的Flutter
依赖注入系统。
我真的很喜欢它的简洁性,下述代码是如何使用它来添加身份验证服务:
return Provider<AuthService>(
builder: (_) => FirebaseAuthService(), // 实现了AuthService的FirebaseAuthService
child: MaterialApp(...),
);
我们如何使用它来创建BLoC
:
return Provider<SignInBloc>(
builder: (_) => SignInBloc(auth: auth),
dispose: (_, bloc) => bloc.dispose(),
child: Consumer<SignInBloc>(
builder: (_, bloc, __) => SignInPage(bloc: bloc),
),
);
请注意Provider
控件是如何对可选的dispose
回调进行配置的,我们使用它来处理BLoC
并关闭相应的StreamControllers
。
Provider
为我们提供了一个简单灵活的API
,我们可以使用它来向控件树添加任何我们想要的东西。它适用于BLoC
、Service
、数值甚至更多。
我将在稍后的一些文章中更详细地讨论如何使用Provider
。 目前为止,我强烈推荐Google IO
大会上的这个演讲:
https://www.youtube.com/watch?v=d_m5csmrf7I
实战项目:登录页面
现在我们已经了解了WABS
在概念上的工作原理,让我们使用它来构建Firebase
的身份验证流程。
以下是我用Flutter
和Firebase
实现的身份验证流程的示例:
观察到的结果:
-
当触发了登录事件,我们禁用了所有按钮并显示
CircularProgressIndicator
,我们将加载状态设置为true
来达到该效果。 -
登录成功或失败后,我们重新启用所有按钮并恢复标题的内容,我们通过设置
loading=false
达到该效果。 -
登录失败时,我们会弹出一个警示的对话框。
这里是用于驱动这些逻辑的SignInBloc
的简单实现:
import 'dart:async';
import 'package:firebase_auth_demo_flutter/services/auth_service.dart';
import 'package:meta/meta.dart';
class SignInBloc {
SignInBloc({@required this.auth});
final AuthService auth;
final StreamController<bool> _isLoadingController = StreamController<bool>();
Stream<bool> get isLoadingStream => _isLoadingController.stream;
void _setIsLoading(bool isLoading) => _isLoadingController.add(isLoading);
Future<void> signInWithGoogle() async {
try {
_setIsLoading(true);
return await auth.signInWithGoogle();
} catch (e) {
rethrow;
} finally {
_setIsLoading(false);
}
}
void dispose() => _isLoadingController.close();
}
请注意,该BLoC
仅向外暴漏了Stream
和Future
的公共API
:
Stream<bool> get isLoadingStream;
Future<void> signInWithGoogle();
这符合我们对Async BLoC
的定义。
所有的魔法都发生在signInWithGoogle()
方法中。让我们通过注释再次回顾这些代码:
Future<void> signInWithGoogle() async {
try {
// 首先通过将loading=true交给流的接收器
_setIsLoading(true);
// 然后登录并等待结果
return await auth.signInWithGoogle();
} catch (e) {
// 登录失败,将调用代码的异常重新抛出
rethrow;
} finally {
// 登录成功或者失败, 将loading=false交给流的接收器
_setIsLoading(false);
}
}
和一般的BLoC
一样,该方法会向接收器添加值;但除此之外,它也可以异步返回一个值,或抛出一个异常。
这意味着我们可以在SignInPage
中写出这样的代码:
Future<void> _signInWithGoogle(BuildContext context) async {
try {
await bloc.signInWithGoogle();
// 处理成功
} on PlatformException catch (e) {
// 处理失败
}
}
这段代码看起来很简单,事实上也确实如此,因为我们需要的仅仅是async/ await
和try/catch
。
然而,对于仅使用接收器和流的“严格”版本的BLoC
,这是不可能的。仅供参考,在Redux
中实现这样的功能…嗯…并不是那么有趣!😅
——虽然看起来Async-BLoC
似乎对BLoC
来说只是一个很小的改进,但它们完全不同。
处理异常时的注意事项
处理异常的另一种可行性是向流中添加一个error
的对象,如下所示:
Future<void> signInWithGoogle() async {
try {
// 首先通过将loading=true交给流的接收器
_setIsLoading(true);
// 然后登录并等待结果
return await auth.signInWithGoogle();
} catch (e) {
// 向流中添加一个error
_isLoadingController.addError(e);
} finally {
// 登录成功或者失败, 将loading=false交给流的接收器
_setIsLoading(false);
}
}
这样,在widget
类中,我们可以编写如下代码:
class SignInPage extends StatelessWidget {
SignInPage({@required this.bloc});
final SignInBloc bloc;
// 由按钮的`onPressed`回调方法进行调用
Future<void> _signInWithGoogle(BuildContext context) async {
await bloc.signInWithGoogle();
}
void build(BuildContext context) {
return StreamBuilder(
stream: isLoadingStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
// 展示error
showDialog(...);
}
// 基于快照渲染UI
}
)
}
}
但这样并不优雅,原因有二:
-
1.它在
StreamBuilder
的builder
中显示了一个对话框,这不是很好,因为builder
只应该返回一个控件,而不是执行任何命令式的代码。 -
2.代码可读性并不高,我们显示错误的地方与执行登录的地方并不一致。
所以,不要这样做,也不要使用上文所展示的try/catch
。😉
我们能通过WABS创建异步服务吗?
当然,正如我之前所说的:
-
BLoC
可以持有和修改状态。 -
Service
不能持有和修改状态。
但是,他们向外暴露的API
遵循相同的规则。
以下是数据库API
的Service
类示例:
abstract class Database {
// Job 的CRUD操作
Future<void> setJob(Job job);
Future<void> deleteJob(Job job);
Stream<List<Job>> jobsStream();
// Entry的CRUD操作
Future<void> setEntry(Entry entry);
Future<void> deleteEntry(Entry entry);
Stream<List<Entry>> entriesStream({Job job});
}
我们可以使用此API
向Cloud Firestore
中写入和读取数据。
调用下述代码可以将新的Job
写入数据库:
Future<void> _submit(Job job) async {
try {
await database.setJob(job);
// 处理成功
} on PlatformException catch (e) {
// 处理失败(展示警告)
}
}
相同的模式,非常简洁的错误处理。
与RxVMS比较
在本文中,作为Flutter
中已有架构模式的改良,我介绍了Widget-Async-BLoC-Service
。
WABS
与Thomas Burkhart
的 RxVMS模式 最相似。
下面是两者各个层之间的对比:
image两者之间的主要区别在于:
-
WABS
使用 Provider 包,而RxVMS
使用GetIt
服务定位器。 -
WABS
使用简单的异步方法来处理UI事件,而RxVMS
使用的是 RxCommand。
RxCommand
是抽象处理UI事件和更新UI的库,它删除了使用BLoC
创建StreamController
/Stream
对所需的样板代码。
RxCommand
很强大,然而,它确实也带来了更陡峭的学习曲线。我的感受是,尽管需要一些额外的样板代码,但是Async-Bloc
可以保证完成工作并且更简单。
我也喜欢WABS
可以在没有任何外部库的情况下实现(除了Provider
包)。
最终选择哪一个取决于您的实际开发场景,这也和个人喜好和品味息息相关。
我应该在我的应用中使用BLoC吗?
BLoC
具有陡峭的学习曲线。要了解它们,您还需要熟悉Stream
和StreamBuilder
。
使用Stream
时,需要考虑以下因素:
-
流的连接状态是什么(没有,等待,活跃,完成)?
-
流是被单次还是多次订阅?
-
StreamController
和StreamSubscription
始终需要被disposed
。 -
当
Flutter
重建窗口控件树时,处理嵌套的StreamBuilders
会导致调试过程变得很棘手。
这些因素都会让代码有额外的开销。
当更新app
本地的状态(例如,将状态从一个控件传递到另一个控件中)时,BLoC
有更简单的替代方案,这个后文再提。
无论如何,我发现BLoCs
在使用Firestore
构建app
时效果非常明显,其中数据通过流从后端流入app
。
在这种情况下,通常将流进行组合或使用RxDart
对其执行转换,BLoC
很擅长这个。
结论
本文是对WABS
的深入介绍,WABS
是我在多个项目中使用了一段时间后探索得出的架构模式。
说实话,随着时间的推移我一直在改进它,在我写这篇文章之前它都还没有名字。
正如我之前所说,架构模式只是一种工具;我的建议是,选择对您和您的项目更有意义的工具。
如果您在项目中使用了WABS
,请让我知道它是行之有效的方案。😉
愉快地编码吧!
本文源码
Flutter & Firebase构建的身份验证流程:
https://github.com/bizz84/firebase_auth_demo_flutter
接下来的这个项目,它针对我的Flutter
和Firebase Udemy
课程中相关深入的资料进行了补充,链接如下:
Flutter&Firebase:构建一个完整的iOS和Android的应用程序:
https://www.udemy.com/flutter-firebase-build-a-complete-app-for-ios-android/?couponCode=DART15&password=codingwithflutter
网友评论