Flutter应用程序架构之应用层的实例介绍

61 阅读14分钟

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

  • 依赖于多个数据源或存储库
  • 需要被一个以上的widget使用(共享)。

在这种情况下,我们很想把这些逻辑放在我们已经有的类(widget或repositories)中。

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

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

在之前的文章中,我已经介绍了一个参考的应用架构,对我的项目非常有效。

该架构定义了四个具有明确边界的独立层。

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 类可以被子类化并使用本地存储实现(使用SembastObjectBoxIsar等包)。


而如果我们已经登录了,我们可以用一个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);
  }
}

这个方法的作用是

  1. 获取购物车(从本地或远程存储库获取,取决于授权状态)
  2. 复制并返回一个更新的购物车
  3. 用更新后的数据设置购物车(根据授权状态,使用本地或远程存储库)

注意,第二步是调用我们之前在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 类,并有updateQuantitydeleteItem 方法。

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应用)时,对它进行了深入的探索。

通过分享这些文章,我希望我已经帮助你驾驭了这个复杂的话题,这样你就可以放心地设计和构建自己的应用程序。

如果说有一件事你应该从这一切中得到,那就是。

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