简介
状态管理是目前Flutter的一个热门话题。
在过去的一年中,各种状态管理技术被提出。
Flutter团队和社区还没有确定一个单一的 "通用 "解决方案。
这是有道理的,因为不同的应用程序有不同的要求。而选择最合适的技术取决于我们要建立的东西。
说实话,有些状态管理技术已经被证明是非常流行的。
- 范围模型(Scoped Model)以其简单性而闻名。
- BLoCs也被广泛使用,它们与Streams和RxDart一起很好地适用于更复杂的应用程序。
- 最近在谷歌I/O大会上,Flutter团队向我们展示了如何使用
Provider包和ChangeNotifier来传播小部件之间的状态变化。
有多种选择可以是一件好事。
但它也可能让人困惑。选择一种技术,随着我们的应用程序的增长,它可以很好地工作和扩展是很重要的。
更重要的是,在早期做出正确的选择可以为我们节省大量的时间和精力。
我对状态管理和应用程序架构的看法
在过去的一年里,我已经建立了很多大大小小的Flutter应用程序。
在这段时间里,我遇到并解决了很多问题。
而且我了解到,在状态管理方面没有银弹。
然而,经过一次又一次的构建和拆解,我已经微调了一种技术,在我所有的项目中都能很好地工作。
所以在这篇文章中,我介绍了一种新的架构模式,它是由我定义的。
- 从现有的模式中借鉴了很多想法
- 调整它们以适应现实世界中Flutter应用程序的需要
在我们看到这个模式是什么样子的之前,让我定义一些目标。
这个模式应该。
- 一旦基本构件清楚了,就应该容易理解
- 在添加新功能时易于复制
- 建立在干净的架构原则之上
- 在编写反应式Flutter应用程序时能很好地工作
- 只需要很少或没有模板代码
- 导致可测试的代码
- 导致可移植的代码
- 倾向于小型、可组合的部件和类
- 易于与异步 API(Futures 和 Streams)集成
- 随着应用程序的规模和复杂性的增加,可以很好地扩展
在Flutter现有的状态管理技术中,这种模式在BLoCs上建立得最多,而且与RxVMS架构相当相似。
不多说了,我很高兴地介绍。
小工具-同步-BLoC-服务模式
简称。WABS(这很酷,因为它包含了我名字的首字母 :D)
这种架构模式有四种变体。
小工具-Bloc-服务
小工具-服务
小工具-Bloc
只有小工具
注意:除了Widget项目外,BLoC和Service项目都是可选的。
换句话说:你可以根据具体情况来使用或省略它们。
现在,让我们通过一个更详细的图来探索完整的实现。
首先,这张图定义了三个应用层。
- UI层:这始终是必要的,因为它是我们的小部件所在的地方。
- 数据层(可选):这是我们添加逻辑和修改状态的地方。
- 服务层(可选):这是我们用来与外部服务通信的地方。
接下来,让我们为每个层能(和不能)做什么定义一些规则。
游戏规则
UI层
这就是我们放置小部件的地方。
小部件可以是无状态的,也可以是有状态的,但它们不应该包括任何显式的状态管理逻辑。
显式状态管理的一个例子是Flutter计数器的例子,当按下增量按钮时,我们用setState() 来增加计数器。
隐式状态管理的例子是一个StatefulWidget ,它包含一个由TextEditingController 管理的TextField 。在这种情况下,我们需要一个StatefulWidget ,因为TextEditingController 会带来副作用(我是 通过艰难的方式发现的),但我们没有显式地管理任何状态。
UI层中的小部件可以自由地调用由块或服务定义的同步或异步方法,并且可以通过StreamBuilder 来订阅流。
注意上图是如何将一个小部件连接到BLoC的输入和输出的。但我们可以使用这种模式将一个部件连接到输入端,而将另一个部件连接到输出端。
换句话说,我们可以实现一个生产者→消费者的数据流。
WABS模式鼓励我们将任何状态管理逻辑转移到数据层。所以我们来看看。
数据层
在这一层,我们可以定义本地或全局应用状态,以及修改状态的代码。
这是用业务逻辑组件(BLoCs)完成的,这是DartConf 2018期间首次引入的模式。
BLoC的构思是为了将业务逻辑与UI层分开,并增加跨平台的代码重用。
当使用BLoC模式时,小部件可以。
- 将事件派发到一个汇中
- 通过一个流来通知状态的更新
根据最初的定义,我们只能通过汇和流与BLoCs通信。
虽然我喜欢这个定义,但我发现它在一些用例中限制性太强。所以在WABS中,我使用了一种BLoC的变种,叫做Async BLoC。
就像BLoCs一样,我们有可以被订阅的输出流。
然而,BLoC的输入可以包括一个同步水槽、一个异步方法或两者。
换句话说,我们从这个。
到这个。
非同步方法可以。
- 将零、一个或多个值添加到输入汇中。
- 返回一个带有结果的
Future<T>。调用代码可以await,以获取结果并做相应的事情。 - 抛出一个异常。调用代码可以通过
try/catch,并在需要时显示一个警告。
稍后,我们将看到一个完整的例子,说明这在实践中是多么有用。
关于BLoCs的更多信息
一个异步BLoC可以定义一个StreamController/Stream 对,如果使用RxDart,则可以定义等同的BehaviorSubject/Observable 。
如果我们愿意,我们甚至可以进行高级流操作,比如用combineLatest 来组合流。为了明确起见。
-
如果需要以某种方式组合,我建议在一个BLoC中拥有多个流。
-
我不鼓励在一个BLoC中使用多个
StreamController。相反,我更喜欢将代码分成两个或更多的BLoC类,以便更好地分离关注点。
我们在数据层/BLoCs中应该/不应该做的事情
- BLoCs应该只包含纯Dart代码。没有UI代码,没有导入Flutter文件,也没有在BLoCs中使用
BuildContext在BLoCs中使用。 - BLoCs不应该直接调用第三方代码。这是服务类的工作。
- Widgets和BLoCs之间的接口与BLoCs和服务之间的接口相同。也就是说,BloCs可以通过sync/async方法直接与服务类通信,并通过流通知更新。
服务层
服务类具有与BLoCs相同的输入/输出接口。
然而,服务和BLoCs之间有一个基本区别。
- BLoCs可以持有和修改状态。
- 服务则不能。
换句话说,我们可以把服务看成是纯粹的、功能性的组件。
它们可以修改和转换从第三方库接收的数据。
例子。Firestore服务
- 我们可以实现一个
FirestoreDatabase服务,作为Firestore的特定领域API-wrapper。 - 数据输入(读取)。将Firestore文档中的键值对流转换为强类型的不可更改的数据模型。
- 数据输出(写)。它将数据模型转换为键值对,以便写入Firestore。
在这种情况下,服务类执行简单的数据操作。与BLoCs不同,它不持有任何状态。
*关于术语的说明。其他文章在提到与第三方库对话的类时使用了Repository这个术语。甚至连Repository模式的定义也随着时间的推移而变化(更多信息请看这篇文章)。在这篇文章中,我并没有明确区分Service和Repository。
把东西放在一起:提供者包
一旦我们定义了我们的BLoCs和服务,我们就需要将它们提供给我们的小部件。
一段时间以来,我一直在使用 provider包。这是一个用于Flutter的完全依赖注入系统,基于InheritedWidget 。
我非常喜欢它的简单性。下面是如何使用它来添加一个认证服务。
return Provider<AuthService>(
builder: (_) => FirebaseAuthService(), // FirebaseAuthService implements AuthService
child: MaterialApp(...),
);
这就是我们如何使用它来创建一个BLoC。
return Provider<SignInBloc>(
builder: (_) => SignInBloc(auth: auth),
dispose: (_, bloc) => bloc.dispose(),
child: Consumer<SignInBloc>(
builder: (_, bloc, __) => SignInPage(bloc: bloc),
),
);
注意Provider widget如何接受一个可选的dispose callback。我们用它来处理BLoCs并关闭相应的StreamController。
Provider 这给了我们一个简单而灵活的API,我们可以用它来添加任何我们想要的东西到我们的widget树。它对BLoCs、服务、价值等都有很好的作用。
我将在我即将发表的一些文章中更详细地谈论如何使用Provider 。现在,我强烈推荐谷歌I/O的这个演讲。
一个真实世界的例子。签到页面
现在我们已经看到了WABS在概念上是如何工作的,让我们用它来构建一个带有Firebase认证的登录流程。
下面是我的《使用Flutter和Firebase的参考认证流程》中的一个交互样本。
一些观察。
- 当签到被触发时,我们禁用所有的按钮,并显示一个
CircularProgressIndicator。我们将loading状态设置为true来做这个。 - 当签到成功或失败时,我们重新启用所有按钮并恢复标题
Text。我们设置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的公共API如何只暴露了一个Stream 和一个Future 。
Stream<bool> get isLoadingStream;
Future<void> signInWithGoogle();
这与我们对Async BLoC的定义一致。
所有的魔法都发生在signInWithGoogle() 方法中。因此,让我们用注释再次回顾一下这个问题。
Future<void> signInWithGoogle() async {
try {
// first, add loading = true to the stream sink
_setIsLoading(true);
// then sign-in and await for the result
return await auth.signInWithGoogle();
} catch (e) {
// if sign in failed, rethrow the exception to the calling code
rethrow;
} finally {
// on success or failure, add loading = false to the stream sink
_setIsLoading(false);
}
}
就像普通的BLoC一样,这个方法向一个水槽添加值。
但除此之外,它还可以异步地返回一个值,或者抛出一个异常。
这意味着我们可以在我们的SignInPage ,写出这样的代码。
// called by the `onPressed` callback of our button
Future<void> _signInWithGoogle(BuildContext context) async {
try {
await bloc.signInWithGoogle();
// handle success
} on PlatformException catch (e) {
// handle error (show alert)
}
}
这段代码看起来很简单。它应该是这样的,因为我们在这里只需要async/await 和try/catch 。
然而,这在BLoC的 "严格 "版本中是不可能的,它只使用一个水槽和一个流。作为参考,在Redux中实现这样的东西...呃...不好玩
Async-LoC看起来是对BLoC的一个小改进,但它带来了所有的不同。
关于处理异常的说明
顺便说一下,另一种可能的处理异常的方法是在流中添加一个错误对象,就像这样。
Future<void> signInWithGoogle() async {
try {
// first, add loading = true to the stream sink
_setIsLoading(true);
// then sign-in and await for the result
return await auth.signInWithGoogle();
} catch (e) {
// add error to the stream
_isLoadingController.addError(e);
} finally {
// on success or failure, add loading = false to the stream sink
_setIsLoading(false);
}
}
然后,在widget类中我们可以写这样的代码。
class SignInPage extends StatelessWidget {
SignInPage({@required this.bloc});
final SignInBloc bloc;
// called by the `onPressed` callback of our button
Future<void> _signInWithGoogle(BuildContext context) async {
await bloc.signInWithGoogle();
}
void build(BuildContext context) {
return StreamBuilder(
stream: isLoadingStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
// show error
showDialog(...);
}
// build UI based on snapshot
}
)
}
}
然而,这是不可取的,原因有二。
- 它在
StreamBuilder的构建器中显示了一个对话框。这不是很好,因为构建器只应该返回一个widget,而不是执行任何指令性代码。 - 这段代码缺乏明确性。我们显示错误的地方与我们登录的地方完全不同。
所以,不要这样做,如上所示,使用try/catch 。 😉
继续前进...
我们可以使用WABS来创建一个 "异步服务 "吗?
当然可以。正如我之前所说。
- BLoCs可以保持和修改状态。
- 服务则不能。
然而,它们面向公众的API也遵守同样的规则。
下面是一个数据库API的服务类的例子。
abstract class Database {
// CRUD operations for Job
Future<void> setJob(Job job);
Future<void> deleteJob(Job job);
Stream<List<Job>> jobsStream();
// CRUD operations for Entry
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);
// handle success
} on PlatformException catch (e) {
// handle error (show alert)
}
}
同样的模式,非常简单的错误处理。
与RxVMS的比较
在这篇文章中,我介绍了Widget-Async-LoC-Service,作为Flutter中现有架构模式的一种改编。
WABS与Thomas Burkhart的RxVMS模式最为相似。
各个层之间甚至有密切的匹配。
两者之间的主要区别在于。
RxCommand是一个强大的抽象,用于处理UI事件和更新。它消除了用BLoCs创建StreamController/Stream 对所需的模板代码。
然而,它确实有一个更大的学习曲线。对于我的用例来说,Async-Bloc可以完成工作,而且更简单,尽管有一点额外的模板。
我还喜欢WABS不需要任何外部库就可以实现(除了Provider包)。
最终选择哪一个取决于你的用例,也取决于个人的偏好和品味。
我应该在我的应用程序中使用BLoCs吗?
BLoCs的学习曲线很陡峭。要理解它们,你还需要熟悉流和StreamBuilder 。
在使用流的过程中,需要考虑各种因素。
- 流的连接状态是什么?(
none,waiting,active,done) - 流是单个还是多个订阅?
StreamController和 总是需要被处理的StreamSubscription- 当Flutter重建widget树时,处理嵌套的
StreamBuilder,会导致棘手的调试问题。
所有这些都给我们的代码增加了更多的开销。
当更新本地应用程序的状态时(例如,将状态从一个widget传播到另一个),有比BLoC更简单的替代方案。我计划在后续文章中写到这一点。
在任何情况下,我发现在用Firestore构建实时应用程序时,BLoC非常有效,因为数据通过流从后端流入应用程序。
在这种情况下,结合流或用RxDart进行转换是很常见的。BLoCs是一个很好的地方,可以做到这一点。
总结
这篇文章是对WABS的深入介绍,这种架构模式我已经在多个项目中使用了一段时间。
说实话,随着时间的推移,我一直在完善它,在写这篇文章之前,我甚至都没有给它起个名字。
正如我之前所说,架构模式只是完成我们工作的工具。我的建议是:选择对你和你的项目更有意义的工具。
如果你在你的项目中使用WABS,让我知道它对你的作用。
编码愉快!
源代码
本文的示例代码取自我的《使用Flutter和Firebase的参考认证流程》。
反过来,这个项目补充了我的Flutter & Firebase课程中的所有深入材料。