现代化Flutter架构-Riverpod应用层

f71805f56aa62a6e16be151df0902d16.png

点击上方蓝字关注我,知识会给你力量

283743ed00a11486ad4b921ff3630f75.png

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

  • 依赖于多个数据源或Repository

  • 需要被多个Widget使用(共享)

在这种情况下,很容易将逻辑放在已有的类(Widget或Repository)中。

但这会导致关注点分离不畅,使我们的代码更难阅读、维护和测试。

事实上,关注点分离是我们需要一个好的应用架构的首要原因。

0b55ba790099e84c32b66c971874aaea.jpeg

在本文中,我们将专注于应用层,学习如何在 Flutter 中为电子商务应用实现购物车功能。

我们将从该功能的概念概述入手,从高层次上了解一切是如何组合在一起的。

然后,我们将深入了解一些实现细节,并实现一个依赖于多个资源库的 CartService 类。我们还将学习如何使用 Riverpod(在Service类中使用 Ref)轻松管理多个依赖关系。 准备好了吗? 让我们开始吧!

购物车:用户界面概述

让我们来看看实现购物车功能可能会用到的一些用户界面示例。 最起码,我们需要一个产品页面:

498275439e50b322159dcdddbfe69075.jpeg

该页面可让我们选择所需的数量 (1),并将产品添加到购物车 (2)。 在右上角,我们还可以看到一个购物车图标,上面有一个徽章,告诉我们购物车中有多少件商品。 我们还需要一个购物车页面:

eef9666ef564e69f21de4b0630fab3fa.jpeg

该页面可让我们编辑数量或从购物车中删除商品。

多个Widget,共享逻辑?

正如我们已经看到的,有多个Widget(每个页面本身就是一个Widget)需要访问购物车数据才能显示正确的用户界面。

换句话说,购物车中的商品(以及更新商品的逻辑)需要在多个Widget中共享。 为了让事情变得更有趣,我们再增加一个要求。

以访客或登录用户身份添加项目

亚马逊或 eBay 等电子商务网站会允许您在创建账户前将物品添加到购物车中。

这样,您就可以以访客身份自由搜索产品目录,只有在结账时才会登录或注册。

那么,我们如何在示例应用程序中复制相同的功能呢?

一种方法是拥有两个购物车:

  • 一个是访客使用的本地购物车

  • 另一个是登录用户使用的远程购物车。

通过这种设置,我们可以使用以下逻辑将物品添加到正确的购物车中:

if user is signed in, then
    add item to remote cart
else
    add item to local cart

实际上,这意味着我们需要三个Repository来实现工作:

  • 一个授权Repository,用于登录和注销;

  • 一个本地购物车Repository,供访客用户使用(由本地存储支持);

  • 一个远程购物车Repository,供通过身份验证的用户使用(由远程数据库支持)。

购物车:全部要求

总之,我们需要能够:

  • 以访客或通过身份验证的用户身份(使用不同的存储库)将物品添加到购物车中,

  • 并从不同的Widget/页面中添加。

应用层

在这种情况下,让我们的代码井井有条的最佳方法是引入一个包含 CartService 的应用层来保存我们的所有逻辑:

8ad50eaaa102f86efea15a266c2a71e2.jpeg

我们可以看到,CartService 在控制器(只管理Widget状态)和存储库(与不同的数据源对话)之间充当了中间人的角色。

CartService 不关心:

  • 管理和更新Widget状态(这是控制器的工作)

  • 数据解析和序列化(这是Repository的工作)

它所做的只是根据需要访问相关资源库,从而实现特定于应用程序的逻辑。

注意:其他基于 MVC 或 MVVM 的常见架构会将特定于应用程序的逻辑(以及数据层代码)保留在模型类中。但是,这会导致模型包含过多代码,难以维护。通过根据需要创建存储库和服务,我们可以更好地分离关注点。

现在,我们已经清楚地知道了我们要做什么,让我们来实现所有相关的代码。

购物车的实现

我们的目标是找出如何实现 CartService 类。

由于这依赖于多个数据模型和Repository,我们先来定义这些模型和Repository。

购物车数据模型

从本质上讲,购物车是由产品 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 类是不可变的(以防止Widget改变其状态),因此我们可以定义一个扩展,其中包含一些修改当前 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 应用程序架构的文章: 领域模型,提供了这些模型类的完整概述。 Auth 和购物车Repository 如前所述,我们需要一个 auth Repository,用来检查是否有已登录用户:

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,我们还需要为每个存储库定义一个Provider:

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();
});

请注意所有这些Provider都会抛出一个 UnimplementedError,因为我们已将资源库定义为抽象类。 如果只使用具体类,可以直接实例化并返回。 有关这方面的更多信息,请阅读我在 Flutter 应用程序架构一文中关于抽象类或具体类的说明。

现在,数据模型和存储库都已介绍完毕,让我们来关注一下Service类。

CartService 类

我们可以看到,CartService 类依赖于三个不同的Repository:

3831bbce46127e87177c3b33c98dcc37.jpeg

因此,我们可以将它们声明为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
}

同样,我们也可以定义相应的Provider:

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;
}

在定义Provider时,我们只需将 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() 方法。 更改购物车的逻辑应位于域层,因为它不依赖于任何服务或存储库。

向 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 的实现! 接下来,让我们看看如何在Controller中使用它。

实现 ShoppingCartItemController

让我们考虑一下如何更新或删除购物车中的物品:

3104f4fa69b9a300452f16bf46106939.jpeg

为此,我们将有一个 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 作为读者参数传递,也是可以的。

就是这样。 现在我们已经了解了资源库、服务和控制器如何作为构建复杂购物车功能的构件:

920f824f906a275670bfa30938954425.jpeg

为简洁起见,我不会在此展示如何实现Widget或 AddToCartController,但您可以阅读我的文章《Flutter 应用程序架构》: 演示层》一文,以便更好地理解 widget 和控制器之间是如何交互的。

关于Controller、Service和Repository的注意事项

Controller、Service和Repository等术语经常被混淆,在不同的上下文中有不同的含义。 开发人员喜欢争论这些问题,我们永远不可能让每个人都对这些术语的明确定义达成一致,一劳永逸。 🤷‍♀️ 我们能做的最好的事情就是选择一个参考架构,并在团队或组织内部统一使用这些术语:

888770175fa09ab11b4070b0afd62273.jpeg

结论

我们现在已经完成了对应用层的概述。由于要涉及的内容很多,因此我们需要做一个简要总结。

如果您发现自己编写的逻辑:

  • 依赖于多个数据源或Repository

  • 需要被多个Widget使用(共享)

那么可以考虑为其编写一个服务类。与扩展了 StateNotifier 的控制器不同,服务类不需要管理任何状态,因为它们保存的逻辑不是特定于 widget 的。

服务类也不关心数据序列化或如何从外部世界获取数据(这是数据层的职责)。

最后,服务类通常是不必要的。如果创建的服务类只是将方法调用从控制器转发到存储库,那就没有意义了。在这种情况下,控制器可以依赖存储库并直接调用其方法。换句话说,应用层是可选的。

最后,如果您遵循此处概述的功能优先的项目结构,则应根据具体功能决定是否需要服务类。

结束语

应用程序架构是一个引人入胜的话题,我在构建一个中型电子商务应用程序(以及之前的许多其他 Flutter 应用程序)的过程中对它进行了深入探讨。

通过分享这些文章,我希望能帮助您了解这个复杂的话题,从而让您能够自信地设计和构建自己的应用程序。

如果说您应该从这些文章中得到什么启发,那就是

在构建应用程序时,关注点的分离应该是首要考虑的问题。使用分层架构可以让您决定每一层应该做什么,不应该做什么,并在各个组件之间建立清晰的界限。

本文翻译自:https://codewithandrea.com/articles/flutter-app-architecture-application-layer/

向大家推荐下我的网站 https://www.yuque.com/xuyisheng  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问

往期推荐

本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。

< END >

作者:徐宜生

更文不易,点个“三连”支持一下👇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值