應用架構指南
接下來的頁面將展示如何使用最佳實踐來構建一個應用。本指南中的建議可以應用於大多數應用,使它們更容易擴充套件、測試和維護。然而,它們是指導原則,而不是嚴格的規則,您應該根據自己的獨特需求進行調整。
本節提供了一個 Flutter 應用如何進行架構的高層概覽。它解釋了應用的分層以及每層中存在的類。下一節將提供具體的程式碼示例,並逐步介紹一個實現了這些建議的 Flutter 應用。
專案結構概覽
#關注點分離 是設計 Flutter 應用時最重要的原則。您的 Flutter 應用應該分為兩個大的層面:UI 層和資料層。
每一層又進一步劃分為不同的元件,每個元件都有明確的職責、定義良好的介面、邊界和依賴關係。本指南建議您將應用劃分為以下元件:
- 檢視
- ViewModel
- Repository
- Service
MVVM
#如果您遇到過 Model-View-ViewModel (MVVM) 架構模式,那麼這會很熟悉。MVVM 是一種架構模式,它將應用程式的一個功能分為三個部分:Model、ViewModel 和 View。View 和 ViewModel 構成了應用程式的 UI 層。Repository 和 Service 代表應用程式的資料,或者 MVVM 中的 Model 層。這些元件中的每一個都在下一節中定義。

應用程式中的每個功能都將包含一個用於描述 UI 的 View,一個用於處理邏輯的 ViewModel,一個或多個 Repository 作為應用程式資料的真相來源,以及零個或多個與外部 API(如客戶端伺服器和平臺外掛)互動的 Service。
應用程式的單個功能可能需要以下所有物件:

頁面結束時,我們將詳細解釋這些物件以及連線它們的箭頭。在本指南中,將使用該圖的簡化版本作為參考。

UI 層
#應用程式的 UI 層負責與使用者互動。它將應用程式的資料展示給使用者,並接收使用者輸入,例如點選事件和表單輸入。
UI 會對資料更改或使用者輸入做出響應。當 UI 從 Repository 接收到新資料時,它應該重新渲染以顯示新資料。當用戶與 UI 互動時,它應該發生變化以反映該互動。
UI 層由兩個架構元件組成,基於 MVVM 設計模式:
- View 描述瞭如何將應用程式資料呈現給使用者。具體來說,它們指的是構成一個功能的Widget 組合。例如,一個 View 通常(但不總是)是一個螢幕,它有一個
ScaffoldWidget,以及 Widget 樹中它下面的所有 Widget。View 還負責響應使用者互動,將事件傳遞給 ViewModel。 - ViewModel 包含將應用程式資料轉換為UI 狀態的邏輯,因為來自 Repository 的資料通常與需要顯示的資料格式不同。例如,您可能需要合併來自多個 Repository 的資料,或者您可能想過濾資料記錄列表。
View 和 ViewModel 應該是一對一的關係。

最簡單的說法是,ViewModel 管理 UI 狀態,View 顯示該狀態。使用 View 和 ViewModel,您的 UI 層可以在配置更改(如螢幕旋轉)期間維護狀態,並且您可以獨立於 Flutter Widget 測試 UI 的邏輯。
應用程式的一個功能是以使用者為中心的,因此由 UI 層定義。成對的View 和 ViewModel 的每個例項定義了您應用中的一個功能。這通常是您應用中的一個螢幕,但不必如此。例如,考慮登入和退出。登入通常在一個特定的螢幕上完成,該螢幕的唯一目的是為使用者提供登入方式。在應用程式程式碼中,登入螢幕將由一個 LoginViewModel 類和一個 LoginView 類組成。
另一方面,退出應用通常不是在一個專用螢幕上完成的。退出應用的功能通常以選單中的按鈕、使用者帳戶螢幕或任何其他不同位置的形式呈現給使用者。它通常在多個位置呈現。在這種情況下,您可能有一個 LogoutViewModel 和一個 LogoutView,它只包含一個可以放入其他 Widget 中的按鈕。
檢視
#在 Flutter 中,View 是應用程式的 Widget 類。View 是渲染 UI 的主要方法,不應包含任何業務邏輯。它們應該從 ViewModel 接收所有渲染所需的資料。

View 應該包含的唯一邏輯是:
- 簡單的 if 語句,根據 ViewModel 中的標誌或可空欄位來顯示和隱藏 Widget。
- 動畫邏輯。
- 基於裝置資訊(如螢幕大小或方向)的佈局邏輯。
- 簡單的路由邏輯。
所有與資料相關的邏輯都應在 ViewModel 中處理。
ViewModel
#ViewModel 暴露了渲染 View 所需的應用程式資料。在本頁面描述的架構設計中,您 Flutter 應用程式中的大部分邏輯都位於 ViewModel 中。

ViewModel 的主要職責包括:
- 從 Repository 檢索應用程式資料,並將其轉換為適合在 View 中呈現的格式。例如,它可能會過濾、排序或聚合資料。
- 維護 View 所需的當前狀態,以便 View 可以在不丟失資料的情況下重建。例如,它可能包含布林標誌來有條件地渲染 View 中的 Widget,或者一個欄位來跟蹤螢幕上輪播圖的哪個部分是活動的。
- 向 View 公開回調(稱為 **Commands**),可以將其附加到事件處理程式,如按鈕點選或表單提交。
Commands 以 Command 模式命名,是 Dart 函式,允許 Views 在不瞭解其實現的情況下執行復雜邏輯。Commands 被編寫為 ViewModel 類中的成員,供 View 類中的手勢處理程式呼叫。
您可以在 應用架構案例研究 的 UI 層 部分找到 View、ViewModel 和 Command 的示例。
要溫和地瞭解 Flutter 中的 MVVM,請檢視 狀態管理基礎知識。
資料層
#應用的資料層處理您的業務資料和邏輯。資料層由兩部分架構組成:Service 和 Repository。這些部分應具有定義明確的輸入和輸出,以簡化其可重用性和可測試性。

使用 MVVM 語言,Service 和 Repository 構成了您的Model 層。
Repository
#Repository 類是您的 Model 資料的真相來源。它們負責從 Service 拉取資料,並將原始資料轉換為領域模型 (domain models)。領域模型代表應用程式所需的資料,並以 View Model 類可以消費的方式進行格式化。您的應用中處理的每種不同型別的資料都應該有一個 Repository 類。
Repository 處理與 Service 相關的業務邏輯,例如:
- 快取
- 錯誤處理
- 重試邏輯
- 資料重新整理
- 輪詢 Service 獲取新資料
- 基於使用者操作重新整理資料

Repository 輸出應用程式資料為領域模型。例如,一個社交媒體應用可能有一個 UserProfileRepository 類,它公開一個 Stream<UserProfile?>,每當使用者登入或登出時就會發出一個新值。
Repository 輸出的模型被 ViewModel 消費。Repository 和 ViewModel 之間存在多對多關係。一個 ViewModel 可以使用多個 Repository 來獲取它需要的資料,一個 Repository 也可以被多個 ViewModel 使用。
Repository 之間永遠不應該相互瞭解。如果您的應用程式的業務邏輯需要來自兩個 Repository 的資料,您應該在 ViewModel 或領域層中組合資料,特別是如果您的 Repository-to-ViewModel 關係很複雜。
Service
#Service 位於您應用程式的最底層。它們封裝 API 端點並公開非同步響應物件,如 Future 和 Stream 物件。它們僅用於隔離資料載入,並且不包含狀態。您的應用應該為每個資料來源都有一個 Service 類。Service 可能封裝的端點示例包括:
- 底層平臺,如 iOS 和 Android API。
- REST 端點。
- 本地檔案。
經驗法則,當所需資料位於您的應用程式的 Dart 程式碼之外時,Service 最有幫助——前面所有示例都是如此。
Service 和 Repository 之間存在多對多關係。單個 Repository 可以使用多個 Service,一個 Service 可以被多個 Repository 使用。

可選:領域層
#隨著您的應用程式的增長和功能的增加,您可能需要抽象掉那些給 ViewModel 帶來過多複雜性的邏輯。這些類通常稱為 Interactor 或Use-case。
Use-case 負責使 UI 層和資料層之間的互動更簡單、更可重用。它們從 Repository 獲取資料,並使其適用於 UI 層。

Use-case 主要用於封裝原本會留在 ViewModel 中且滿足以下一個或多個條件的業務邏輯:
- 需要合併來自多個 Repository 的資料。
- 非常複雜。
- 該邏輯將被不同的 ViewModel 重用。
這一層是可選的,因為並非所有應用程式或應用程式中的功能都有這些要求。如果您認為您的應用程式會從這個額外的層中受益,請考慮其優缺點:
| 優點 | 缺點 |
|---|---|
| ✅ 避免 ViewModel 中的程式碼重複。 | ❌ 增加了架構的複雜性,增加了更多的類和更高的認知負荷。 |
| ✅ 透過將複雜的業務邏輯與 UI 邏輯分離來提高可測試性。 | ❌ 測試需要額外的 Mock。 |
| ✅ 提高 ViewModel 中程式碼的可讀性。 | ❌ 為您的程式碼增加了額外的樣板程式碼。 |
透過 Use-case 進行資料訪問
#新增領域層的另一個考慮因素是 ViewModel 是否將繼續直接訪問 Repository 資料,還是您將強制 ViewModel 透過 Use-case 獲取資料。換句話說,您是根據需要新增 Use-case 嗎?也許當您注意到 ViewModel 中存在重複邏輯時?或者,您是每次 ViewModel 需要資料時都建立一個 Use-case,即使 Use-case 中的邏輯很簡單?
如果您選擇後者,它會加劇前面概述的優缺點。您的應用程式程式碼將高度模組化和可測試,但它也會增加大量的開銷。
一個好的方法是僅在需要時新增 Use-case。如果您發現您的 ViewModel 大部分時間都透過 Use-case 訪問資料,您可以隨時重構程式碼以專門使用 Use-case。本指南後面使用的示例應用程式為某些功能提供了 Use-case,但也讓 ViewModel 直接與 Repository 互動。一個複雜的功能最終可能看起來像這樣:

這種新增 Use-case 的方法由以下規則定義:
- Use-case 依賴於 Repository。
- Use-case 和 Repository 之間存在多對多關係。
- ViewModel 依賴於一個或多個 Use-case *和* 一個或多個 Repository。
這種使用 Use-case 的方法看起來不像分層的千層麵,更像是一盤餐盤,有兩道主菜(UI 層和資料層)和一個配菜(領域層)。Use-case 只是具有定義明確的輸入和輸出的實用工具類。這種方法是靈活且可擴充套件的,但它需要更高的嚴謹性來保持秩序。
反饋
#隨著本網站部分的不斷發展,我們歡迎您的反饋!