在构建复杂的应用程序时,我们可能会发现自己在编写逻辑时。
- 依赖于多个数据源或存储库
- 需要被一个以上的widget使用(共享)。
在这种情况下,我们很想把这些逻辑放在我们已经有的类(widget或repositories)中。
但这将导致关注点分离不力,使我们的代码更难阅读、维护和测试。
事实上,关注点的分离是我们需要一个好的应用架构的首要原因。
在之前的文章中,我已经介绍了一个参考的应用架构,对我的项目非常有效。
- 使用Riverpod的Flutter应用架构。简介
- Flutter项目结构。功能优先还是层级优先?
- Flutter应用架构。存储库模式
- Flutter 应用程序架构。领域模式
- Flutter 应用程序体系结构。演示层
该架构定义了四个具有明确边界的独立层。
Flutter 应用架构使用数据层、领域层、应用层和表现层。箭头显示各层之间的依赖关系
而在本文中,我们将关注应用层,学习如何在Flutter中实现电子商务应用的购物车功能。
我们将从这个功能的概念性概述开始,看看一切是如何在高层次上结合起来的。
然后,我们将深入到一些实现细节,并实现一个依赖于多个存储库的CartService
类。
我们还将学习如何用Riverpod轻松管理多个依赖关系(使用 Ref
在一个服务类里面)。
准备好了吗?我们开始吧
购物车:用户界面概述
让我们考虑一下我们可能用来实现购物车功能的一些UI示例。
至少,我们需要一个产品页面。
带有数量选择器和 "添加到购物车 "按钮的产品页面。另外,那不是一个可爱的意大利面盘吗?
这个页面让我们选择所需的数量(1)并将产品添加到购物车(2)。在右上角,我们还发现了一个购物车图标,上面有一个徽章,告诉我们购物车里有多少商品。
我们还需要一个购物车页面。
带有编辑数量和删除项目选项的购物车页面
这个页面可以让我们编辑数量或从购物车中删除物品。
多个小工具,共享逻辑?
正如我们已经看到的,有多个widget(每个页面本身就是一个widget)需要访问购物车数据以显示正确的用户界面。
换句话说,购物车项目(以及更新它们的逻辑)需要在多个小工具之间共享。
为了让事情变得更有趣,我们再增加一个要求。
以访客或登录用户的身份添加物品
电子商务网站,如亚马逊或eBay,会让你在创建账户之前将物品添加到购物车中。
这样,你就可以以客人身份自由搜索产品目录,只有在进行结账时才登录或注册。
那么,我们怎样才能在我们的示例应用程序中复制同样的功能呢?
一种方法是有两个购物车。
- 一个由客人使用的本地购物车
- 一个远程购物车,由已登录的用户使用
有了这样的设置,我们就可以通过这个逻辑将物品添加到正确的购物车中。
if user is signed in, then
add item to remote cart
else
add item to local cart
在实践中,这意味着我们需要三个资源库来使事情顺利进行。
- 一个认证库,用于签入和签出
- 一个本地购物车库,供访客用户使用(由本地存储支持)。
- 一个远程购物车库,由经过认证的用户使用(由远程数据库支持)。
购物车:全部要求
总而言之,我们需要能够。
- 以访客或认证用户的身份向购物车添加物品(使用不同的存储库)
- 从不同的小工具/页面中这样做
但所有这些逻辑应该放在哪里呢?
应用层
在这种情况下,保持我们的代码有条理的最好方法是引入一个应用层,其中包含一个CartService
,以容纳我们所有的逻辑。
购物车功能所使用的层和组件
正如我们所看到的,CartService
在控制器(只管理widget状态)和存储库(与不同的数据源对话)之间充当中间人。
CartService
并不关心。
- 管理和更新widget的状态(那是控制器的工作)
- 数据解析和序列化(那是存储库的工作)。
它所做的就是根据需要访问相关的资源库来实现特定的应用逻辑。
注意:其他基于MVC或MVVM的常见架构将这种特定的应用逻辑(连同数据层代码)保留在模型类本身。然而,这可能会导致模型包含太多的代码,难以维护。通过根据需要创建资源库和服务,我们得到了一个更好的关注点分离。
现在我们对我们要做的事情有了清晰的认识,让我们来实现所有相关的代码。
购物车的实现
我们的目标是弄清楚如何实现CartService
类。
由于这取决于多个数据模型和存储库,我们先来定义这些。
购物车的数据模型
从本质上讲,购物车是一个由产品ID和数量标识的物品集合。
我们可以用一个列表、一个地图、甚至一个集合来实现它。我发现最有效的方法是创建一个包含数值地图的类。
class Cart {
const Cart([this.items = const {}]);
/// All the items in the shopping cart, where:
/// - key: product ID
/// - value: quantity
final Map<ProductID, int> items;
/// Note: ProductID is just a String
}
由于我们希望Cart
类是不可变的(以防止小部件突变其状态),我们可以定义一个扩展,用一些方法来修改当前购物车,并返回一个新的 Cart
对象。
/// 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和数量作为一个实体来保存。
/// 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应用架构的文章。领域模型提供了这些模型类的完整概述。如果您想学习如何建立一个完整的电子商务应用程序,请查看我的完整Flutter课程包。
授权和购物车存储库
正如我们所讨论的,我们需要一个认证库,用来检查我们是否有一个签入的用户。
abstract class AuthRepository {
/// returns null if the user is not signed in
AppUser? get currentUser;
/// useful to watch auth state changes in realtime
Stream<AppUser?> authStateChanges();
// other sign in methods
}
当我们以客人身份使用该应用程序时,我们可以使用LocalCartRepository
来获取和设置购物车的值。
abstract class LocalCartRepository {
// get the cart value (read-once)
Future<Cart> fetchCart();
// get the cart value (realtime updates)
Stream<Cart> watchCart();
// set the cart value
Future<void> setCart(Cart cart);
}
LocalCartRepository
类可以被子类化并使用本地存储实现(使用Sembast、ObjectBox或Isar等包)。
而如果我们已经登录了,我们可以用一个RemoteCartRepository
来代替。
abstract class RemoteCartRepository {
// get the cart value (read-once)
Future<Cart> fetchCart(String uid);
// get the cart value (realtime updates)
Stream<Cart> watchCart(String uid);
// set the cart value
Future<void> setCart(String uid, Cart items);
}
这个类与
LocalCartRepository
,有一个根本的区别:所有的方法都需要一个uid
的参数,因为每个经过认证的用户都会有自己的购物车。
如果我们使用Riverpod,我们还需要为每个存储库定义一个提供者。
final authRepositoryProvider = Provider<AuthRepository>((ref) {
// This should be overridden in main file
throw UnimplementedError();
});
final localCartRepositoryProvider = Provider<LocalCartRepository>((ref) {
// This should be overridden in main file
throw UnimplementedError();
});
final remoteCartRepositoryProvider = Provider<RemoteCartRepository>((ref) {
// This should be overridden in main file
throw UnimplementedError();
});
请注意所有这些提供者是如何抛出一个
UnimplementedError
,因为我们已经将存储库定义为抽象类。如果你只使用具体类,你可以直接实例化并返回它们。关于这个问题的更多信息,请阅读我在Flutter应用架构一文中关于抽象或具体类的说明 。存储库模式。
现在,数据模型和存储库都已经出来了,让我们来关注一下服务类。
CartService类
正如我们所见,CartService
类依赖于三个独立的存储库。
购物车功能所使用的层和组件
因此,我们可以将它们声明为final
属性,并将它们作为构造函数参数传递。
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
}
沿着同样的思路,我们可以定义相应的提供者。
final cartServiceProvider = Provider<CartService>((ref) {
return CartService(
authRepository: ref.watch(authRepositoryProvider),
localCartRepository: ref.watch(localCartRepositoryProvider),
remoteCartRepository: ref.watch(remoteCartRepositoryProvider),
);
});
这样做是可行的,它使所有的依赖关系变得明确。
但是,如果你不喜欢有这么多的模板代码,还有一个选择。👇
将Ref作为一个参数传递
与其直接传递每个依赖关系,我们可以只声明一个单一的 Ref
属性。
class CartService {
CartService(this.ref);
final Ref ref;
}
而当我们定义提供者时,我们只需把ref
作为一个参数传递。
final cartServiceProvider = Provider<CartService>((ref) {
return CartService(ref);
});
现在我们已经声明了CartService
类,让我们向它添加一些方法。
使用CartService添加一个项目
为了使我们的生活更轻松,我们可以定义两个私有方法,用来获取和设置购物车的值。
class CartService {
CartService(this.ref);
final Ref ref;
/// fetch the cart from the local or remote repository
/// depending on the user auth state
Future<Cart> _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()
。
class CartService {
CartService(this.ref);
final Ref ref;
Future<Cart> _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()
方法。突变Cart
的逻辑应该住在域层中,因为它不依赖于任何服务或存储库。
向CartService添加其余方法
就像我们已经定义了addItem()
方法一样,我们可以添加控制器将使用的其他方法。
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
的实现。
接下来,让我们看看如何在控制器中使用它。
实现ShoppingCartItemController
让我们考虑如何更新或删除已经在购物车中的物品。
一个购物车项目小部件
要做到这一点,我们将有一个ShoppingCartItem
widget和一个相应的ShoppingCartItemController
类,并有updateQuantity
和deleteItem
方法。
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
保存了所有复杂的逻辑,这也可以被其他控制器重复使用
总结一下,让我们为这个控制器定义提供者。
final shoppingCartItemControllerProvider =
StateNotifierProvider<ShoppingCartItemController, AsyncValue<void>>((ref) {
return ShoppingCartItemController(
cartService: ref.watch(cartServiceProvider),
);
});
在这种情况下,直接调用
ref.watch(cartServiceProvider)
,并将其传递给构造函数就可以了,因为ShoppingCartItemController
只有一个依赖关系。但是如果我们想把ref.read
作为Reader
的参数来传递,那也是可以的。
就这样了。我们现在已经看到存储库、服务和控制器如何作为构建复杂的购物车功能的基石。
购物车功能所使用的层和组件
为了简洁起见,我不会在这里展示小部件或
AddToCartController
,但你可以阅读我关于Flutter应用程序架构的文章。表达层,以更好地理解widget和控制器是如何相互作用的。
关于控制器、服务和存储库的说明
控制器、服务和资源库等术语经常被混淆,在不同的语境中使用不同的含义。
开发者们喜欢争论这些东西,我们永远无法让每个人都对这些术语的明确定义达成一致,一劳永逸。🤷♀️
我们能做的最好的事情就是挑选一个参考架构,并在我们的团队或组织内统一使用这些术语。
Flutter应用架构使用数据层、领域层、应用层和表现层。箭头显示各层之间的依赖关系
总结
我们现在已经完成了对应用层的概述。因为有很多东西要讲,所以简单的总结一下是有必要的。
如果您发现自己在编写一些逻辑时
- 依赖于多个数据源或存储库
- 需要被一个以上的小部件使用(共享)。
然后考虑为其编写一个服务类。不像控制器那样扩展 StateNotifier
的控制器不同,服务类不需要管理任何状态,因为它们持有的逻辑并不是特定于widget的。
服务类也不关心数据序列化或如何从外部世界获取数据(那是数据层的作用)。
最后,服务类往往是不必要的。如果一个服务类所做的只是将方法调用从控制器转发到存储库,那么创建这个服务类就没有意义。在这种情况下,控制器可以依赖资源库并直接调用其方法。换句话说,应用层是可选的。
最后,如果你遵循这里概述的功能优先的项目结构,你应该根据每个功能来决定是否需要服务类。
结束语
应用架构是一个深具魅力的话题,我在构建一个中等规模的电子商务应用(以及之前的许多其他Flutter应用)时,对它进行了深入的探索。
通过分享这些文章,我希望我已经帮助你驾驭了这个复杂的话题,这样你就可以放心地设计和构建你自己的应用程序。
如果说有一件事你应该从这一切中得到,那就是。
构建应用程序时,关注点的分离应该是一个首要关注点。使用分层架构可以让你决定每一层应该做什么,不应该做什么,并在各种组件之间建立明确的界限。