首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

现代化Flutter架构-Riverpod应用层

  • 24-12-16 16:23
  • 3985
  • 41262
juejin.cn

在构建复杂的应用程序时,我们可能会发现自己编写的逻辑:

  • 依赖于多个数据源或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

注:本文转载自juejin.cn的xuyisheng的文章"https://juejin.cn/post/7448645894976946195"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2491) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

140
Android
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top