本文由 简悦SimpRead 转码,原文地址 www.christianfindlay.com
讨论Flutter BloC UI模式以及何时适合弯曲或打破规则
BloC是Flutter世界中一个常见的UI模式。谷歌最初在Flutter的早期就创造了这个想法。自其诞生以来,该模式采取了多种形式,现在最流行的方法是使用以该模式命名的库。除了谷歌在原始谈话中阐述的内容外,并没有真正的官方BloC模式。最接近的文档可能是Flutter文档所链接的这篇文章。然而,该模式随着时间的推移不断发展,并获得了一些特征、核心概念和非官方的规则。
本文讨论了BloC规则,以及什么时候弯曲或打破这些规则可能是合适的。其目的是告诉你,遵循BloC规则只是有时会产生最好的代码,严格的规则可能不适用于你的情况。
概述
这里首先要理解的是,我在某些情况下会提到bloobit库。我写这个库是作为BloC的一个替代品。BloC启发了Bloobit,并与ChangeNotifier、Cubit和其他类似的类密切相似。它与BloC共享的核心概念是,它将UI和业务逻辑分开。
我在这里写的任何概念都应该与BloC的实现兼容。然而,如果你使用BloC库,文档中会附带一些规则,我不建议你破坏这些规则。Bloobit没有附带任何规则,我允许你做任何你想做的事情。
了解BloC模式的来源也很重要。与MVVM模式一样,BloC来自于人们同时使用一个以上的UI工具包的时代。如果你看这个早期视频,你可以看到,其目的是在Flutter和Angular Dart之间共享代码。这需要公开地将逻辑和UI分开。现在我们不需要这样做了,因为Flutter到处都在运行。
"业务逻辑 "通常是 "UI逻辑 "的代码,并洒上验证等等。业务逻辑通常最终在后端更重,所以你不应该惊讶,你的Flutter应用程序中的大部分逻辑都是围绕着UI。试图从你的控制器中删除任何UI逻辑可能是徒劳的,并可能使你跳过不必要的圈套。
反模式
"我看到这里有很多反模式!"
一个反模式是
对一个反复出现的问题的常见反应,通常是无效的,并有可能产生很大的反作用。
我不同意这些方法是无效的或有反作用风险的说法。其中一些提示将为你节省时间并提高可维护性。你不应该在什么是或不是适得其反上诉诸权威。你应该根据方法的优点来看待它,了解其中的利弊,并根据这些来做决定。
我并不建议你盲目地跟随我的例子。我建议你权衡所有的思考,并做出自己的决定。
规则不能取代思考
软件开发是困难的,你不能逃避对你的应用程序结构的思考。没有什么银弹可以用来强迫你团队中的人做正确的事情。即使有大量关于现有模式的文档,我们也经常看到明显的错误。其中一些错误是由于对模式的误解造成的,但大多数时候,这都是普通人的失误。
处理这个问题的唯一方法是通过沟通、指导、审查和培训。如果你把初级开发人员扔进深渊,说 "只要遵循BloC文档",你可能会得到遵循规则的代码,但你仍然不会得到团队满意的代码。与其他程序员一起工作,讨论取舍问题,并获得共识。一套规则是无法取代这一点的。
关于测试的说明
正如我在其他地方写的那样,这篇文章假设你主要是写小工具测试。我不会深入探讨为什么你不需要为你的控制器(BloC)写大量的单元测试,但你可以用部件测试来覆盖控制器中的逻辑。任何你不能用部件测试覆盖的东西都可能不会在你的应用程序中发生。
Brace yourself because here we go...
使用可改变的状态
不变性是一件好事,但它需要工作。我已经在Dart和其他语言中写了很多关于它的内容。然而,以一种宽松的方式实现它是一个灾难的秘诀。Dart的记录和其他不可变性功能即将推出。尽管如此,要想获得结构平等的不变性是很有挑战性的。 不变性有好处,但要准备好为它们工作。
BloC模式在可变类型下工作良好。然而,BloC库期望结构平等。你可以绕过这一点,但Bloobit在可变或不可变的状态下都可以工作。你也可以在没有StatelessWidgets的情况下使用易变的状态。
将GlobalKeys注入你的业务逻辑控制器中
给定一个导航器或脚手架的信使键,你可以从业务逻辑中做一些事情,如导航或显示小吃店。你经常发现,在这些事情发生之前,你必须执行业务逻辑。有时,你可能只想在状态满足某些标准时进行导航,而你的代码可能有分支逻辑。
注意,可能很难模拟按键进行测试,但如果你在写widget测试,就不必担心这个问题。如果你写widget测试,你不需要直接测试你的控制器。
在你的控制器中使用对话框回调。
你没有理由不为一个UI动作写一个抽象,比如弹出一个对话框。这段代码将DialogService注入到Bloobit中,是一个实际显示对话框并返回用户选择结果的抽象。同样,将UI逻辑放在控制器中并无不妥,因为UI逻辑是应用程序最关注的。这对测试来说并不构成问题,因为如果你需要的话,你可以模拟对话框。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class User {
const User(this.id, this.name);
final String id;
final String name;
}
class UserScreenState {
const UserScreenState(this.user, this.isProcessing, this.isDeleted);
final User user;
final bool isProcessing;
final bool isDeleted;
UserScreenState copyWith({
User? user,
bool? isProcessing,
bool? isDeleted,
}) =>
UserScreenState(
user ?? this.user,
isProcessing ?? this.isProcessing,
isDeleted ?? this.isDeleted,
);
}
class UserService {
Future<void> deleteUser(String id) =>
Future<void>.delayed(const Duration(seconds: 1));
}
class DialogService {
DialogService(this.navigatorKey);
final GlobalKey<NavigatorState> navigatorKey;
Future<bool?> showYesNoDialog(String title, String message) =>
showDialog<bool>(
context: navigatorKey.currentState!.context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: <Widget>[
TextButton(
child: const Text('Yes'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
TextButton(
child: const Text('No'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
],
),
);
}
class UserScreenBloobit extends Bloobit<UserScreenState> {
UserScreenBloobit(
this.logger,
this.userService,
super.initialState,
this.dialogService,
);
final Logger logger;
final UserService userService;
final DialogService dialogService;
Future<void> delete() async {
try {
logger.info('Delete Clicked');
setState(state.copyWith(isProcessing: true));
final result = await dialogService.showYesNoDialog(
'Delete User',
'Are you sure you want to delete ${state.user.name}?',
) ??
false;
logger.info('User confirmed dialog: ${result ? 'Yes' : 'No'}');
if (result) {
await userService.deleteUser(state.user.id);
logger.info('User Deleted');
setState(state.copyWith(isDeleted: true));
}
}
// ignore: avoid_catches_without_on_clauses
catch (e)
// ignore: empty_catches
{}
setState(state.copyWith(isProcessing: false));
}
}
完整的样本
注意:你也可以用导航键直接显示对话框。
业务逻辑组件之间的流
你可以而且应该在你的业务逻辑组件之间流传状态和其他数据。流和可听物(观察者模式)的全部意义在于允许主体和观察者之间有一个解耦的一对多关系。如果你不这样做,那么首先使用流的意义何在?
BloC库文档这样说。
因为Bloc暴露了流,所以可能很想做一个监听另一个Bloc的Bloc。你不应该这样做。[......]它在两个单元之间形成了一种依赖关系。[......]一般来说,应该不惜一切代价避免同一架构层中的两个实体之间的兄弟姐妹依赖关系,因为它创造了难以维护的紧密耦合。
这是非常合理的,这个例子将一个BloC注入到另一个BloC中,不必要地耦合了两个组件。然而,将一个BloC的流注入到另一个BloC的构造函数中,就没有这样的问题。如果一个BloC发射了一个流,另一个BloC可以安全地监听该流而不产生耦合。只要在构造函数中注入流并监听。当你处置BloC时,在订阅上调用关闭。
import 'dart:async';
class MyBloc {
final Stream<int> _stream;
final _streamController = StreamController<int>();
late final StreamSubscription? _streamSubscription;
Stream<int> get stream => _streamController.stream;
MyBloc(Stream<int> stream) : _stream = stream {
// Inject the source stream into the bloc's constructor and listen to it
// to emit state
_streamSubscription = _stream.listen((data) {
// Emit the data as state
_streamController.add(data);
});
}
void dispose() {
// Clean up when the bloc is disposed
_streamSubscription?.cancel();
_streamController.close();
print('Disposed');
}
}
void main() {
// Create a stream that emits values of type int
final sourceStream = Stream.fromIterable([1, 2, 3]);
// Inject the stream into the bloc's constructor
final bloc = MyBloc(sourceStream);
// Listen to the stream to receive the emitted state
bloc.stream.listen((state) {
print(state); // Prints 1, 2, 3
if (state == 3) {
// Clean up the bloc when it's no longer needed
bloc.dispose();
}
});
}
一个应用程序的一个部分监听应用程序另一部分的状态变化是很常见的。流为你提供了以解耦方式实现这一目的的机制,而这正是流和一般观察者模式的主要目的。
在这里查看交互式样本。
在不必要的情况下避免使用流
上述情况的反面是,你应该只在必要时使用流。不必要的流会占用宝贵的资源,如内存和处理能力,这可能会降低你的应用程序的性能。此外,使用过多的流会使你的代码更难理解和维护。一般来说,最好只在必要时使用流。
如果业务逻辑与UI组件(Widget)是一对一的关系,可以考虑避免使用流。BloC模式总是使用流,但Bloobit不用流也能达到类似的效果。这就是我创建它的原因。它只是调用setState而不是打开和关闭流。
停止使用存储库模式
存储库模式是软件开发中的一种常见设计模式。它将应用程序的数据访问层和业务逻辑层之间的问题分开。这可以使测试应用程序的业务逻辑变得更容易,因为我们可以将其与数据访问层隔离开来进行测试。
然而,widget测试已经提供了一种方法来测试widget的业务逻辑,而不需要使用存储库模式。你可以直接伪造Firestore客户端或HTTP客户端,它为小部件提供数据以执行其业务逻辑。这样就不需要存储库模式了,因为存储库模式会给应用程序增加不必要的复杂性。
如果你不是因为特定的原因而使用存储库模式,一般来说,最好不要使用它,以保持你的应用程序简单和易于测试。
结论
这里的核心信息是,没有什么规则是一成不变的。作为一个开发者,你需要对代码做出决定。你不能依靠一个权威来告诉你什么是对的或错的。当人们警告你不要做某件事时,你应该听他们的意见,但要试着听听 "为什么"。如果建议背后没有坚实的理由,那么你可能不需要重视他们所说的。
此外,要确保该建议确实适用于你。许多建议随着时间的推移会失去其狭窄的范围,变得过于宽泛。有时,人们阻止你写正常的代码,是因为他们误解了最初的问题。
归根结底,关键是要了解利弊,为自己和团队权衡利弊。