跳到主內容

應用架構指南

構建 Flutter 應用的推薦方式。

以下頁面演示瞭如何使用最佳實踐來構建應用。本指南中的建議適用於大多數應用,能夠使它們更易於擴充套件、測試和維護。但請注意,這些只是指導方針而非鐵律,你應該根據自己的獨特需求進行調整。

本節對 Flutter 應用的架構方式進行了概括性介紹。它解釋了應用的各個層級以及每個層級中存在的類。本節之後的部分將提供具體的程式碼示例,並逐步演示如何實現這些建議。

專案結構概覽

#

關注點分離 (Separation-of-concerns) 是設計 Flutter 應用時要遵循的最重要原則。你的 Flutter 應用應該分為兩個主要層級:UI 層和資料層。

每一層進一步細分為不同的元件,每個元件都有明確的職責、定義良好的介面、邊界和依賴項。本指南建議將應用拆分為以下元件:

  • 檢視
  • 檢視模型 (View models)
  • 倉庫 (Repositories)
  • 服務 (Services)

MVVM

#

如果你瞭解 Model-View-ViewModel 架構模式 (MVVM),你會對這套方案感到熟悉。MVVM 是一種將應用功能拆分為三個部分的架構模式:Model(模型)、ViewModel(檢視模型)和 View(檢視)。檢視和檢視模型組成了應用的 UI 層。倉庫和服務則代表了應用的資料層,即 MVVM 中的模型層。下一節將定義這些元件中的每一個。

MVVM architectural pattern

應用中的每個功能都應包含一個用於描述 UI 的檢視、一個用於處理邏輯的檢視模型、一個或多個作為應用資料來源(Single Source of Truth)的倉庫,以及零個或多個用於與外部 API(如客戶端伺服器和平臺外掛)互動的服務。

應用的單個功能可能需要以下所有物件:

An example of the Dart objects that might exist in one feature using the architecture described on page.

到本頁結束時,我們將詳細解釋每個物件及其連線箭頭。在本指南中,以下簡化版的圖表將作為核心參考。

A simplified diagram of the architecture described on this page.

UI 層

#

應用的 UI 層負責與使用者互動。它向用戶展示應用資料並接收使用者輸入,例如點選事件和表單輸入。

UI 會對資料變化或使用者輸入做出響應。當 UI 從倉庫接收到新資料時,它應該重新渲染以顯示新資料。當用戶與 UI 互動時,UI 也應做出相應改變以反映該互動。

根據 MVVM 設計模式,UI 層由兩個架構元件組成:

  • 檢視 (Views) 描述瞭如何向用戶呈現應用資料。具體來說,它們是指構成一個功能的元件集合 (compositions of widgets)。例如,檢視通常(但不總是)是一個包含 Scaffold 元件以及元件樹中下方所有元件的螢幕。檢視還負責將響應使用者互動的事件傳遞給檢視模型。
  • 檢視模型 (View models) 包含將應用資料轉換為UI 狀態 (UI State) 的邏輯,因為來自倉庫的資料格式通常與 UI 所需展示的資料格式不同。例如,你可能需要合併來自多個倉庫的資料,或者想要過濾資料記錄列表。

檢視和檢視模型應該保持一對一的關係。

A simplified diagram of the architecture described on this page with the view and view model objects highlighted.

簡單來說,檢視模型管理 UI 狀態,而檢視負責顯示該狀態。透過使用檢視和檢視模型,你的 UI 層可以在配置更改(如螢幕旋轉)期間保持狀態,並且你可以獨立於 Flutter 元件來測試 UI 的邏輯。

應用的功能是以使用者為中心的,因此由 UI 層定義。每對配對的檢視檢視模型都定義了應用中的一個功能。這通常是應用中的一個螢幕,但並非必須如此。例如,考慮登入和登出操作。登入通常是在一個特定螢幕上完成的,該螢幕的唯一目的就是為使用者提供登入方式。在應用程式碼中,登入螢幕由 LoginViewModel 類和 LoginView 類組成。

另一方面,登出操作通常不在專門的螢幕上完成。登出功能通常以選單中的按鈕、使用者賬戶螢幕或許多其他位置呈現給使用者。它通常會出現在多個位置。在這種情況下,你可以擁有一個 LogoutViewModel 和一個 LogoutView,其中僅包含一個可以嵌入到其他元件中的按鈕。

檢視

#

在 Flutter 中,檢視即應用的元件類。檢視是渲染 UI 的主要方法,不應包含任何業務邏輯。它們應接收從檢視模型傳遞而來的所有渲染所需資料。

A simplified diagram of the architecture described on this page with the view object highlighted.

檢視中唯一應該包含的邏輯是:

  • 基於檢視模型中的標誌位或可空欄位來顯示或隱藏元件的簡單 if 語句。
  • 動畫邏輯。
  • 基於裝置資訊(如螢幕尺寸或方向)的佈局邏輯。
  • 簡單的路由邏輯。

所有與資料相關的邏輯都應在檢視模型中處理。

檢視模型 (View models)

#

檢視模型公開了渲染檢視所需的應用資料。在本頁描述的架構設計中,Flutter 應用的大部分邏輯都存在於檢視模型中。

A simplified diagram of the architecture described on this page with the view model object highlighted.

檢視模型的主要職責包括:

  • 從倉庫中檢索應用資料並將其轉換為適合檢視呈現的格式。例如,它可能會過濾、排序或聚合資料。
  • 維護檢視所需的當前狀態,以便檢視在重建時不會丟失資料。例如,它可能包含用於有條件地渲染元件的布林標誌,或者跟蹤輪播圖當前處於哪一段的欄位。
  • 向檢視公開回調函式(稱為命令 (commands)),這些回撥可以附加到事件處理器上,例如按鈕按下或表單提交。

命令以 命令模式 (command pattern) 命名,是 Dart 函式,允許檢視在無需瞭解具體實現的情況下執行復雜的邏輯。命令作為檢視模型類的成員編寫,由檢視類中的手勢處理器呼叫。

你可以在 應用架構案例研究UI 層 部分找到檢視、檢視模型和命令的示例。

如需瞭解 Flutter 中 MVVM 的入門知識,請檢視 狀態管理基礎知識

資料層

#

應用的資料層處理業務資料和邏輯。資料層由兩部分架構組成:服務和倉庫。這些部分應該具有定義明確的輸入和輸出,以簡化其可重用性和可測試性。

A simplified diagram of the architecture described on this page with the Data layer highlighted.

使用 MVVM 的術語,服務和倉庫構成了你的模型層 (model layer)

倉庫 (Repositories)

#

倉庫 (Repository) 類是模型資料的單一事實來源。它們負責從服務輪詢資料,並將原始資料轉換為領域模型 (domain models)。領域模型代表了應用需要的資料,並以檢視模型類可以消費的格式進行組織。應用中處理的每種不同型別的資料都應該有一個對應的倉庫類。

倉庫負責處理與服務相關的業務邏輯,例如:

  • 快取。
  • 錯誤處理。
  • 重試邏輯。
  • 重新整理資料。
  • 為新資料輪詢服務。
  • 基於使用者操作重新整理資料。

A simplified diagram of the architecture described on this page with the Repository object highlighted.

倉庫以領域模型的形式輸出應用資料。例如,社交媒體應用可能有一個 UserProfileRepository 類,它公開一個 Stream<UserProfile?>,每當使用者登入或登出時就會發出一個新值。

倉庫輸出的模型被檢視模型所使用。倉庫和檢視模型之間是多對多關係。一個檢視模型可以使用多個倉庫來獲取所需資料,而一個倉庫也可以被多個檢視模型使用。

倉庫之間不應相互感知。如果你的應用邏輯需要來自兩個倉庫的資料,你應該在檢視模型或領域層中合併這些資料,特別是當你的倉庫到檢視模型的邏輯較為複雜時。

管理應用範圍內的會話狀態。

#

由於倉庫是應用資料的單一事實來源,它們也是管理應用生命週期狀態 (app-wide lifecycle state) 的理想場所——即那些需要在多個檢視模型之間共享,但不應超出當前應用會話而持久存在的資料。

應用生命週期狀態的例子包括活動的登入會話、記憶體中的資料快取或臨時的應用設定。由於檢視模型和倉庫之間是多對多關係,多個檢視模型可以依賴同一個倉庫例項(通常透過服務定位器或依賴注入容器管理)。這允許不同的功能透過倉庫公開的流和方法,以響應式方式觀察和修改同一共享狀態,而不會破壞檢視與其檢視模型之間清晰的一對一邊界。

服務 (Services)

#

服務位於應用的最底層。它們封裝 API 端點並公開非同步響應物件,如 FutureStream 物件。它們僅用於隔離資料載入,且不持有任何狀態。你的應用應為每個資料來源配置一個服務類。服務可以封裝的端點示例包括:

  • 底層平臺,如 iOS 和 Android API。
  • REST 端點。
  • 本地檔案。

經驗法則:當必要的資料存在於你應用的 Dart 程式碼之外時(上述每個例子都是如此),使用服務是最有幫助的。

服務和倉庫之間是多對多關係。單個倉庫可以使用多個服務,而一個服務也可以被多個倉庫使用。

A simplified diagram of the architecture described on this page with the Service object highlighted.

可選:領域層 (Domain layer)

#

隨著應用的增長和功能的增加,你可能需要抽象出那些給檢視模型增加過多複雜性的邏輯。這些類通常被稱為互動器或用例 (use-cases)

用例負責使 UI 層和資料層之間的互動更簡單且更具可重用性。它們從倉庫獲取資料,並使其適用於 UI 層。

MVVM design pattern with an added domain layer object

用例主要用於封裝原本會存在於檢視模型中的業務邏輯,並滿足以下一個或多個條件:

  1. 需要合併來自多個倉庫的資料。
  2. 邏輯過於複雜。
  3. 該邏輯將被不同的檢視模型重用。

此層是可選的,因為並非所有應用或應用內的所有功能都有這些需求。如果你認為你的應用將從這個額外的層級中受益,請考慮其利弊:

優點缺點
✅ 避免檢視模型中的程式碼重複 ❌ 增加了架構的複雜性,添加了更多類並提高了認知負荷
✅ 透過將複雜的業務邏輯與 UI 邏輯分離來提高可測試性 ❌ 測試需要額外的 Mock 物件
✅ 提高檢視模型中的程式碼可讀性 ❌ 為你的程式碼增加了額外的樣板程式碼

使用用例 (Use-cases) 進行資料訪問

#

新增領域層時的另一個考量是:檢視模型是否繼續直接訪問倉庫資料,或者你是否強制檢視模型透過用例來獲取資料?換句話說,你是按需新增用例嗎?也許是在你注意到檢視模型中出現重複邏輯時?還是說,無論邏輯是否簡單,只要檢視模型需要資料,你就建立一個用例?

如果你選擇後者,會強化上述的利弊。你的應用程式碼將極其模組化且易於測試,但也會增加大量不必要的開銷。

一種好的做法是僅在需要時新增用例。如果你發現檢視模型大部分時間都在透過用例訪問資料,你總是可以重構程式碼以完全利用用例。本指南後續使用的示例應用在某些功能中使用了用例,但也擁有直接與倉庫互動的檢視模型。一個複雜的功能最終看起來可能是這樣的:

A simplified diagram of the architecture described on this page with a use case object.

這種新增用例的方法由以下規則定義:

  • 用例依賴於倉庫。
  • 用例和倉庫之間是多對多關係。
  • 檢視模型依賴於一個或多個用例以及一個或多個倉庫。

這種使用用例的方法看起來不像層層疊疊的千層麵,而更像是一頓有兩道主菜(UI 和資料層)和一道配菜(領域層)的正餐。用例只是具有明確輸入和輸出的實用類。這種方法靈活且可擴充套件,但需要更勤勉地維護秩序。

反饋

#

由於網站的這一部分正在不斷完善中,我們歡迎你的反饋