在构建复杂的应用程序时,我们可能会发现自己编写的逻辑:
- 依赖于多个数据源或Repository
- 需要被多个Widget使用(共享)
在这种情况下,很容易将逻辑放在已有的类(Widget或Repository)中。
但这会导致关注点分离不畅,使我们的代码更难阅读、维护和测试。
事实上,关注点分离是我们需要一个好的应用架构的首要原因。
在本文中,我们将专注于应用层,学习如何在 Flutter 中为电子商务应用实现购物车功能。
我们将从该功能的概念概述入手,从高层次上了解一切是如何组合在一起的。
然后,我们将深入了解一些实现细节,并实现一个依赖于多个资源库的 CartService 类。我们还将学习如何使用 Riverpod(在Service类中使用 Ref)轻松管理多个依赖关系。 准备好了吗? 让我们开始吧!
购物车:用户界面概述
让我们来看看实现购物车功能可能会用到的一些用户界面示例。 最起码,我们需要一个产品页面:
该页面可让我们选择所需的数量 (1),并将产品添加到购物车 (2)。 在右上角,我们还可以看到一个购物车图标,上面有一个徽章,告诉我们购物车中有多少件商品。 我们还需要一个购物车页面:
该页面可让我们编辑数量或从购物车中删除商品。
多个Widget,共享逻辑?
正如我们已经看到的,有多个Widget(每个页面本身就是一个Widget)需要访问购物车数据才能显示正确的用户界面。
换句话说,购物车中的商品(以及更新商品的逻辑)需要在多个Widget中共享。 为了让事情变得更有趣,我们再增加一个要求。
以访客或登录用户身份添加项目
亚马逊或 eBay 等电子商务网站会允许您在创建账户前将物品添加到购物车中。
这样,您就可以以访客身份自由搜索产品目录,只有在结账时才会登录或注册。
那么,我们如何在示例应用程序中复制相同的功能呢?
一种方法是拥有两个购物车:
- 一个是访客使用的本地购物车
- 另一个是登录用户使用的远程购物车。
通过这种设置,我们可以使用以下逻辑将物品添加到正确的购物车中:
dart 代码解读复制代码if user is signed in, then
add item to remote cart
else
add item to local cart
实际上,这意味着我们需要三个Repository来实现工作:
- 一个授权Repository,用于登录和注销;
- 一个本地购物车Repository,供访客用户使用(由本地存储支持);
- 一个远程购物车Repository,供通过身份验证的用户使用(由远程数据库支持)。
购物车:全部要求
总之,我们需要能够:
- 以访客或通过身份验证的用户身份(使用不同的存储库)将物品添加到购物车中,
- 并从不同的Widget/页面中添加。
应用层
在这种情况下,让我们的代码井井有条的最佳方法是引入一个包含 CartService 的应用层来保存我们的所有逻辑:
我们可以看到,CartService 在控制器(只管理Widget状态)和存储库(与不同的数据源对话)之间充当了中间人的角色。
CartService 不关心:
- 管理和更新Widget状态(这是控制器的工作)
- 数据解析和序列化(这是Repository的工作)
它所做的只是根据需要访问相关资源库,从而实现特定于应用程序的逻辑。
注意:其他基于 MVC 或 MVVM 的常见架构会将特定于应用程序的逻辑(以及数据层代码)保留在模型类中。但是,这会导致模型包含过多代码,难以维护。通过根据需要创建存储库和服务,我们可以更好地分离关注点。
现在,我们已经清楚地知道了我们要做什么,让我们来实现所有相关的代码。
购物车的实现
我们的目标是找出如何实现 CartService 类。
由于这依赖于多个数据模型和Repository,我们先来定义这些模型和Repository。
购物车数据模型
从本质上讲,购物车是由产品 ID 和数量标识的物品集合。
我们可以使用列表、地图甚至集合来实现。我发现最有效的方法是创建一个包含值映射的类:
dart 代码解读复制代码class Cart {
const Cart([this.items = const {}]);
/// All the items in the shopping cart, where:
/// - key: product ID
/// - value: quantity
final Mapint > items;
/// Note: ProductID is just a String
}
由于我们希望 Cart 类是不可变的(以防止Widget改变其状态),因此我们可以定义一个扩展,其中包含一些修改当前 Cart 的方法,并返回一个新的 Cart 对象:
dart 代码解读复制代码/// Helper extension used to mutate the items in the shopping cart.
extension MutableCart on Cart {
// implementations omitted for brevity
Cart addItem(Item item) { ... }
Cart setItem(Item item) { ... }
Cart removeItemById(ProductID productId) { ... }
}
我们还可以定义一个 Item 类,将产品 ID 和数量作为一个实体保存:
dart 代码解读复制代码/// A product along with a quantity that can be added to an order/cart
class Item {
const Item({
required this.productId,
required this.quantity,
});
final ProductID productId;
final int quantity;
}
我关于 Flutter 应用程序架构的文章: 领域模型,提供了这些模型类的完整概述。 Auth 和购物车Repository 如前所述,我们需要一个 auth Repository,用来检查是否有已登录用户:
dart 代码解读复制代码abstract class AuthRepository {
/// returns null if the user is not signed in
AppUser? get currentUser;
/// useful to watch auth state changes in realtime
Stream authStateChanges();
// other sign in methods
}
当我们以访客身份使用应用程序时,可以使用 LocalCartRepository 来获取和设置购物车值:
dart 代码解读复制代码abstract class LocalCartRepository {
// get the cart value (read-once)
Future fetchCart();
// get the cart value (realtime updates)
Stream watchCart();
// set the cart value
Future<void> setCart(Cart cart);
}
可以对 LocalCartRepository 类进行子类化,并使用本地存储(使用 Sembast、ObjectBox 或 Isar 等软件包)来实现。
如果我们已登录,则可以使用 RemoteCartRepository 代替:
dart 代码解读复制代码abstract class RemoteCartRepository {
// get the cart value (read-once)
Future fetchCart(String uid);
// get the cart value (realtime updates)
Stream watchCart(String uid);
// set the cart value
Future<void> setCart(String uid, Cart items);
}
该类与 LocalCartRepository 非常相似,但有一个根本区别:所有方法都需要一个 uid 参数,因为每个通过身份验证的用户都将拥有自己的购物车。
如果使用 Riverpod,我们还需要为每个存储库定义一个Provider:
dart 代码解读复制代码final authRepositoryProvider = Provider((ref) {
// This should be overridden in main file
throw UnimplementedError();
});
final localCartRepositoryProvider = Provider((ref) {
// This should be overridden in main file
throw UnimplementedError();
});
final remoteCartRepositoryProvider = Provider((ref) {
// This should be overridden in main file
throw UnimplementedError();
});
请注意所有这些Provider都会抛出一个 UnimplementedError,因为我们已将资源库定义为抽象类。 如果只使用具体类,可以直接实例化并返回。 有关这方面的更多信息,请阅读我在 Flutter 应用程序架构一文中关于抽象类或具体类的说明。
现在,数据模型和存储库都已介绍完毕,让我们来关注一下Service类。
CartService 类
我们可以看到,CartService 类依赖于三个不同的Repository:
因此,我们可以将它们声明为final属性,并作为构造函数参数传递:
dart 代码解读复制代码class CartService {
CartService({
required this.authRepository,
required this.localCartRepository,
required this.remoteCartRepository,
});
final AuthRepository authRepository;
final LocalCartRepository localCartRepository;
final RemoteCartRepository remoteCartRepository;
// TODO: implement methods using these repositories
}
同样,我们也可以定义相应的Provider:
dart 代码解读复制代码final cartServiceProvider = Provider((ref) {
return CartService(
authRepository: ref.watch(authRepositoryProvider),
localCartRepository: ref.watch(localCartRepositoryProvider),
remoteCartRepository: ref.watch(remoteCartRepositoryProvider),
);
});
但如果你不喜欢这么多模板代码,还有另一种选择。 👇
将 Ref 作为参数传递
与其直接传递每个依赖关系,我们可以只声明一个 Ref 属性:
dart 代码解读复制代码class CartService {
CartService(this.ref);
final Ref ref;
}
在定义Provider时,我们只需将 ref 作为参数传递:
dart 代码解读复制代码final cartServiceProvider = Provider((ref) {
return CartService(ref);
});
现在我们已经声明了 CartService 类,让我们为它添加一些方法。
使用 CartService 添加物品
为了让我们的工作更轻松,我们可以定义两个私有方法,用来获取和设置购物车值:
dart 代码解读复制代码class CartService {
CartService(this.ref);
final Ref ref;
/// fetch the cart from the local or remote repository
/// depending on the user auth state
Future _fetchCart() {
final user = ref.read(authRepositoryProvider).currentUser;
if (user != null) {
return ref.read(remoteCartRepositoryProvider).fetchCart(user.uid);
} else {
return ref.read(localCartRepositoryProvider).fetchCart();
}
}
/// save the cart to the local or remote repository
/// depending on the user auth state
Future<void> _setCart(Cart cart) async {
final user = ref.read(authRepositoryProvider).currentUser;
if (user != null) {
await ref.read(remoteCartRepositoryProvider).setCart(user.uid, cart);
} else {
await ref.read(localCartRepositoryProvider).setCart(cart);
}
}
}
请注意,我们可以通过调用 ref.read(provider)读取每个资源库,并调用我们需要的方法。
通过将 Ref 作为参数传递,CartService 现在直接依赖于 Riverpod 软件包,实际依赖关系现在是隐式的。 如果这不是你想要的,只需如上所示显式传递依赖关系即可。 注:我将在另一篇文章中介绍如何使用 Ref 为服务类编写单元测试。
接下来,我们可以创建一个公共 addItem() 方法,该方法在源码中调用 _fetchCart() 和 _setCart():
dart 代码解读复制代码class CartService {
CartService(this.ref);
final Ref ref;
Future _fetchCart() { ... }
Future<void> _setCart(Cart cart) { ... }
/// adds an item to the local or remote cart
/// depending on the user auth state
Future<void> addItem(Item item) async {
// 1. fetch the cart
final cart = await _fetchCart();
// 2. return a copy with the updated data
final updated = cart.addItem(item);
// 3. set the cart with the updated data
await _setCart(updated);
}
}
该方法的作用是:
- 获取购物车(根据授权状态,从本地或远程存储库获取)
- 复制并返回更新后的购物车,
- 使用更新后的数据设置购物车(根据授权状态,使用本地或远程存储库)。
请注意,第二步将调用我们之前在 MutableCart 扩展中定义的 addItem() 方法。 更改购物车的逻辑应位于域层,因为它不依赖于任何服务或存储库。
向 CartService 添加其余方法
就像我们定义了 addItem() 方法一样,我们可以添加控制器将使用的其他方法:
dart 代码解读复制代码class CartService {
...
/// removes an item from the local or remote cart depending on the user auth
/// state
Future<void> removeItemById(String productId) async {
// business logic
final cart = await _fetchCart();
final updated = cart.removeItemById(productId);
await _setCart(updated);
}
/// sets an item in the local or remote cart depending on the user auth state
Future<void> setItem(Item item) async {
final cart = await _fetchCart();
final updated = cart.setItem(item);
await _setCart(updated);
}
}
请注意,第二步总是将购物车更新委托给 MutableCart 扩展中的一个方法,由于它没有依赖关系,因此可以很容易地进行单元测试。
就这样,我们完成了 CartService 的实现! 接下来,让我们看看如何在Controller中使用它。
实现 ShoppingCartItemController
让我们考虑一下如何更新或删除购物车中的物品:
为此,我们将有一个 ShoppingCartItem Widget和一个相应的 ShoppingCartItemController 类,其中包含 updateQuantity 和 deleteItem 方法:
dart 代码解读复制代码class ShoppingCartItemController extends StateNotifier<AsyncValue<void>> {
ShoppingCartItemController({required this.cartService})
: super(const AsyncData(null));
final CartService cartService;
Future<void> updateQuantity(Item item, int quantity) async {
// set loading state
state = const AsyncLoading();
// create an updated Item with the new quantity
final updated = Item(productId: item.productId, quantity: quantity);
// use the cartService to update the cart
// and set the state again (data or error)
state = await AsyncValue.guard(
() => cartService.updateItemIfExists(updated),
);
}
Future<void> deleteItem(Item item) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => cartService.removeItemById(item.productId),
);
}
}
该类中的方法有两个任务:
- 更新Widget状态
- 调用相应的 CartService 方法更新购物车。
请注意,每个方法都只有几行代码。 这是因为 CartService 保存了所有复杂的逻辑,其他控制器也可以重复使用这些逻辑!
最后,让我们定义该控制器的提供程序:
dart 代码解读复制代码final shoppingCartItemControllerProvider =
StateNotifierProvidervoid >>((ref) {
return ShoppingCartItemController(
cartService: ref.watch(cartServiceProvider),
);
});
在这种情况下,我们可以调用 ref.watch(cartServiceProvider),并直接将其传递给构造函数,因为 ShoppingCartItemController 只有一个依赖关系。 但如果我们想将 ref.read 作为读者参数传递,也是可以的。
就是这样。 现在我们已经了解了资源库、服务和控制器如何作为构建复杂购物车功能的构件:
为简洁起见,我不会在此展示如何实现Widget或 AddToCartController,但您可以阅读我的文章《Flutter 应用程序架构》: 演示层》一文,以便更好地理解 widget 和控制器之间是如何交互的。
关于Controller、Service和Repository的注意事项
Controller、Service和Repository等术语经常被混淆,在不同的上下文中有不同的含义。 开发人员喜欢争论这些问题,我们永远不可能让每个人都对这些术语的明确定义达成一致,一劳永逸。 🤷♀️ 我们能做的最好的事情就是选择一个参考架构,并在团队或组织内部统一使用这些术语:
结论
我们现在已经完成了对应用层的概述。由于要涉及的内容很多,因此我们需要做一个简要总结。
如果您发现自己编写的逻辑:
- 依赖于多个数据源或Repository
- 需要被多个Widget使用(共享)
那么可以考虑为其编写一个服务类。与扩展了 StateNotifier 的控制器不同,服务类不需要管理任何状态,因为它们保存的逻辑不是特定于 widget 的。
服务类也不关心数据序列化或如何从外部世界获取数据(这是数据层的职责)。
最后,服务类通常是不必要的。如果创建的服务类只是将方法调用从控制器转发到存储库,那就没有意义了。在这种情况下,控制器可以依赖存储库并直接调用其方法。换句话说,应用层是可选的。
最后,如果您遵循此处概述的功能优先的项目结构,则应根据具体功能决定是否需要服务类。
结束语
应用程序架构是一个引人入胜的话题,我在构建一个中型电子商务应用程序(以及之前的许多其他 Flutter 应用程序)的过程中对它进行了深入探讨。
通过分享这些文章,我希望能帮助您了解这个复杂的话题,从而让您能够自信地设计和构建自己的应用程序。
如果说您应该从这些文章中得到什么启发,那就是
在构建应用程序时,关注点的分离应该是首要考虑的问题。使用分层架构可以让您决定每一层应该做什么,不应该做什么,并在各个组件之间建立清晰的界限。
本文翻译自:codewithandrea.com/articles/fl…
欢迎大家关注我的公众号——【群英传】,专注于「Android」「Flutter」「Kotlin」
我的语雀知识库——www.yuque.com/xuyisheng
评论记录:
回复评论: