<div id="qayss"></div>
  • <xmp id="qayss">
  • <small id="qayss"><small id="qayss"></small></small>
    <div id="qayss"></div>
  • <li id="qayss"></li>
  • <div id="qayss"><div id="qayss"></div></div>
  • <xmp id="qayss"><li id="qayss"></li>
    <div id="qayss"><li id="qayss"></li></div>
  • <small id="qayss"></small>
  • <div id="qayss"></div>
  • <xmp id="qayss"><div id="qayss"></div><div id="qayss"><div id="qayss"></div></div><xmp id="qayss">
    <small id="qayss"><div id="qayss"></div></small><div id="qayss"></div>
  • Flutter 應用程序架構:應用層

    來源:codewithandrea.com 更新時間:2023-11-07 21:42

    如果您需要幫助為您的 Flutter 應用程序選擇最適合的項目結構,可以查看:

    如果您想探索其他流行架構(如 MVP、MVVM 或 Clean Architecture)并了解它們如何與此處提出的架構相比較,可以閱讀以下內容:

    要了解 Riverpod 架構中每個層的更多信息,請閱讀本系列中的其他文章:

    在構建復雜應用程序時,我們可能會發現自己編寫了依賴于多個數據源或存儲庫的邏輯,而且需要被多個小部件共享使用。

    在這種情況下,誘人的做法是將該邏輯放入我們已有的類(小部件或存儲庫)中。

    但這會導致關注點分離不夠明確,使我們的代碼更難閱讀、維護和測試。

    事實上,關注點分離是我們需要良好應用程序架構的首要原因。

    該架構定義了四個明確定界的獨立層:

    flutter-app-architecture.png

    Flutter App Architecture 使用數據、領域、應用和展示層。箭頭演示層間的依賴關系。

    在本文中,我們將專注于應用層,并學習如何在Flutter中為電子商務應用程序實現購物車功能。

    我們將從概念上概述這個功能,以查看高層次上所有內容是如何相互關聯的。

    然后,我們將深入一些實現細節,實現一個依賴于多個存儲庫的CartService類。

    我們還將學習如何在服務類中使用Riverpod輕松管理多個依賴項(使用Ref)。

    準備好了嗎?讓我們開始吧!

    購物車:UI概覽

    讓我們考慮一些示例UI,我們可以使用它們來實現購物車功能。

    至少,我們需要一個產品頁面:
    product-page.png

    產品頁面包含數量選擇器和“添加到購物車”按鈕。

    還有,這個意大利面碟看起來真美味,對吧?這個頁面允許我們選擇所需的數量(1)并將產品添加到購物車(2)。在右上角,我們還可以找到一個購物車圖標,上面有一個標識,告訴我們購物車中有多少件商品。


    我們還需要一個購物車頁面:
    shopping-cart-page.png

    購物車頁面,提供編輯數量和刪除商品選項。

    此頁面允許我們編輯購物車中的商品數量或刪除商品。

    多個小部件,共享邏輯?

    正如我們已經看到的,有多個小部件(每個頁面本身都是一個小部件),它們需要訪問購物車數據以顯示正確的用戶界面。

    換句話說,購物車商品(以及更新它們的邏輯)需要跨多個小部件共享。

    為了使事情更有趣,讓我們添加一個額外的要求。

    作為訪客或已登錄用戶添加商品

    像亞馬遜或 eBay 這樣的電子商務網站允許您在創建帳戶之前將商品添加到購物車。

    這樣,您可以作為訪客自由搜索產品目錄,只有在進行結賬時才登錄或注冊。

    那么,我們如何在我們的示例應用程序中復制相同的功能呢?

    一種方法是擁有兩個購物車:

    • 供訪客使用的本地購物車
    • 供已登錄用戶使用的遠程購物車

    通過這種設置,我們可以使用以下邏輯將商品添加到正確的購物車中:

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

    實際上,這在實踐中意味著我們需要三個存儲庫來使事情正常運作:

    • 一個認證存儲庫,用于登錄和退出
    • 一個本地購物車存儲庫,供游客用戶使用(由本地存儲支持)
    • 一個遠程購物車存儲庫,供經過身份驗證的用戶使用(由遠程數據庫支持)

    購物車:完整要求

    總之,我們需要能夠:

    • 作為游客或經過身份驗證的用戶向購物車添加物品(使用不同的存儲庫)
    • 可以從不同的小部件/頁面執行此操作

    但所有這些邏輯應該放在哪里?

    應用層

    在這種情況下,保持我們的代碼組織良好的最佳方法是引入一個應用層,其中包含一個CartService來保存我們所有的邏輯: shopping-cart-layers.png

    關于購物車功能所使用的層和組件

    正如我們所看到的,CartService 充當了控制器(僅管理小部件狀態)和倉庫(與不同的數據源通信)之間的中間層。

    CartService 不涉及以下方面:

    • 管理和更新小部件狀態(這是控制器的工作)
    • 數據解析和序列化(這是倉庫的工作)

    它的唯一作用是根據需要訪問相關的倉庫來實現特定于應用程序的邏輯。

    注意:基于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應用架構:域模型 的文章為您提供了這些模型類的完整概述。

    身份驗證和購物車存儲庫

    正如我們所討論的,我們需要一個身份驗證存儲庫,以便檢查是否有已登錄的用戶:

    
    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、ObjectBoxIsar 等包)。 而且,如果我們已登錄,我們可以使用 `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類依賴于三個獨立的存儲庫: shopping-cart-layers.png

    購物車功能使用的圖層和組件

    因此我們可以將它們聲明為“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的實現。

    接下來,讓我們看看如何在控制器中使用它。

    實現購物車商品控制器

    讓我們考慮如何更新或移除已經在購物車中的商品:

    shopping-cart-item.png
    一個購物車商品小部件。

    為此,我們將創建一個名為 ShoppingCartItem 的小部件,以及一個相應的 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),
        );
      }
    }
    

    這個類中的方法有兩個職責:

    • 更新小部件狀態
    • 調用相應的CartService方法來更新購物車

    請注意,每個方法只有幾行代碼。這是有意設計的,因為CartService包含所有復雜的邏輯,這些邏輯也可以被其他控制器重用!

    最后,讓我們為這個控制器定義提供者:

    
    final shoppingCartItemControllerProvider =
        StateNotifierProvider<ShoppingCartItemController, AsyncValue<void>>((ref) {
      return ShoppingCartItemController(
        cartService: ref.watch(cartServiceProvider),
      );
    });
    

    在這種情況下,可以調用ref.watch(cartServiceProvider)并直接將其傳遞給構造函數,因為ShoppingCartItemController只有一個依賴項。但如果我們想要將ref.read作為Reader參數傳遞,那也是可以的。

    就是這樣?,F在我們已經看到,存儲庫、服務和控制器可以作為構建復雜購物車功能的構建塊: shopping-cart-layers.png
    關于購物車功能所使用的層和組件
    為簡潔起見,我不會在這里展示小部件或AddToCartController的實現方式,但您可以閱讀我的文章《Flutter應用架構:展示層》,以更好地理解小部件和控制器之間的交互。

    關于控制器、服務和存儲庫的說明

    控制器、服務和存儲庫等術語在不同情境中常?;煜褂?,并帶有不同的含義。

    開發人員喜歡就這些事情爭論,我們永遠無法讓每個人都就這些術語的清晰定義達成一致。 ??‍♀?

    我們能做的最好事情就是選擇一個參考架構,并在我們的團隊或組織內一致地使用這些術語:

    Flutter應用程序架構使用數據、領域、應用和演示層。箭頭顯示各層之間的依賴關系

    結論

    我們已經完成了對應用層的概述。由于要涵蓋的內容很多,因此有必要進行簡要總結。

    如果您發現自己在編寫某些邏輯時:

    • 依賴于多個數據源或存儲庫
    • 需要被多個小部件使用(共享)

    那么考慮為其編寫一個服務類。與擴展StateNotifier的控制器不同,服務類不需要管理任何狀態,因為它們包含的是與小部件無關的邏輯。

    服務類也不關心數據序列化或如何從外部獲取數據(這是數據層的職責)。

    最后,服務類通常是不必要的。如果一個服務類的唯一作用是將方法調用從控制器轉發到存儲庫,那么在這種情況下,控制器可以依賴于存儲庫并直接調用其方法。換句話說,應用層是可選的。

    最后,如果您正在按照此處概述的以功能為先的項目結構進行開發,您應該根據功能逐個功能地決定是否需要服務類。

    結束語

    應用程序架構是一個深受吸引的話題,我在構建中型電子商務應用程序(以及在此之前構建的許多其他Flutter應用程序)時,能夠深入探討它。

    通過分享這些文章,我希望能幫助您深入了解這一復雜的話題,以便您能夠自信地設計和構建自己的應用程序。

    如果有一件事您應該從中學到,那就是:

    在構建應用程序時,關注點的分離應該是一個首要問題。使用分層架構可以讓您決定每一層應該做什么,不應該做什么,并在各種組件之間建立清晰的邊界。

    曰本丰满熟妇XXXX性,一女多男在疯狂伦交,多人乱P杂交公车调教,成人AV在线一区二区三区
    <div id="qayss"></div>
  • <xmp id="qayss">
  • <small id="qayss"><small id="qayss"></small></small>
    <div id="qayss"></div>
  • <li id="qayss"></li>
  • <div id="qayss"><div id="qayss"></div></div>
  • <xmp id="qayss"><li id="qayss"></li>
    <div id="qayss"><li id="qayss"></li></div>
  • <small id="qayss"></small>
  • <div id="qayss"></div>
  • <xmp id="qayss"><div id="qayss"></div><div id="qayss"><div id="qayss"></div></div><xmp id="qayss">
    <small id="qayss"><div id="qayss"></div></small><div id="qayss"></div>