應用程式架構指南

本指南包含最佳做法和建議架構,協助您建構出強大的、高品質應用程式。

行動應用程式使用者體驗

一般 Android 應用程式包含多個應用程式元件,包括活動片段服務內容供應器,以及廣播接收器。您需要在應用程式資訊清單中宣告大部分的應用程式元件。接著,Android 作業系統會使用這個檔案判斷如何將應用程式整合至裝置的整體使用者體驗。由於一般的 Android 應用程式可能包含多個元件,且使用者經常會在短時間內與多個應用程式互動,因此應用程式必須適應不同的使用者導向的工作流程和工作。

請注意,行動裝置也會受到資源限制,因此作業系統隨時可能會終止部分應用程式程序來為新的裝置騰出空間。

考量這個環境的狀況,應用程式元件可以單獨啟動,脫序,且作業系統或使用者隨時可以刪除這些元件。由於這些事件不在您的控管之下,您不應在應用程式元件中儲存或保留任何應用程式資料或狀態,且應用程式元件不應彼此依附。

常見架構原則

如果您不打算使用應用程式元件儲存應用程式資料和狀態,應該如何改為設計應用程式?

隨著 Android 應用程式的規模不斷擴大,請務必定義架構,讓應用程式能夠調度資源、提升應用程式的穩定性,並讓應用程式更容易進行測試。

應用程式架構會定義應用程式各部分之間的邊界,以及各部分應負的責任。為滿足上述需求,在設計應用程式架構時,請務必遵循一些特定原則。

關注點分離

最重要的原則是區隔疑慮。想要在 ActivityFragment 中編寫所有程式碼,是很常見的錯誤。這些 UI 類別應該只包含處理 UI 和作業系統互動的邏輯。請盡可能讓這些類別保持精簡,避免與元件生命週期相關的許多問題,並提高這些類別的可測試性。

請注意,您並非 ActivityFragment 的實作內容;這些只是黏附類別,代表 Android 作業系統與應用程式之間的合約。OS 可根據使用者互動或記憶體不足等系統條件,隨時刪除這些類別。如要提供令人滿意的使用者體驗,以及更易於管理的應用程式維護體驗,建議您盡可能減少依附元件。

透過資料模型使用雲端硬碟使用者介面

另一個重要原則是,您應透過資料模型 (建議使用永久模型) 使用 UI。資料模型代表應用程式的資料。它們與應用程式中的 UI 元素和其他元件無關。這表示它們並未與 UI 和應用程式元件的生命週期相關聯,但當 OS 決定從記憶體中移除應用程式的程序時,仍會刪除該模型。

永久模型非常適合下列原因:

  • 如果 Android OS 刪除您的應用程式以釋出資源,使用者並不會遺失資料。

  • 在網路連線不穩定或無法使用時,您的應用程式仍會繼續運作。

如果您的應用程式架構是以資料模型類別為基礎,就能讓應用程式更具有測試性與穩定性。

單一可靠資料來源

在應用程式中定義新的資料類型時,您應該為該類型指派單一可靠資料來源 (SSOT)。而 SSOT 是該資料的擁有者,只有 SSOT 可以修改或修改資料。為此,SSOT 會使用不可變更的類型公開資料,並為了修改資料,SSOT 會公開函式或接收其他類型可呼叫的事件。

這種模式有多項優點:

  • 可將特定類型的資料的所有變更集中在一處。
  • 這能保護資料,避免其他類型遭到竄改。
  • 對資料進行變更,讓資料更容易追蹤。因此,錯誤更容易發現。

在離線優先應用程式中,應用程式資料的可靠資料來源通常是資料庫。在其他情況下,可靠資料來源可以是 ViewModel,甚至是 UI。

單向資料流

我們的指南中經常會使用單一可靠資料來源原則,搭配單向資料流 (UDF) 模式使用。在 UDF 中,「state」只會朝一個方向流。修改資料流向相反方向的事件

在 Android 中,狀態或資料通常會從較高範圍的階層類型流向較低範圍的階層。事件通常會從範圍較低的類型觸發,直到達到相應資料類型的 SSOT 為止。例如,應用程式資料通常會從資料來源流向 UI。使用者事件 (例如按鈕) 按下資料流從 UI 流至 SSOT,其中應用程式資料會在不可變動的類型中修改及公開。

這個模式更有效保證資料一致性、較不容易發生錯誤,也比較容易偵錯,並在單一登入 (SSOT) 模式中提供所有好處。

本節說明如何按照建議的最佳做法建構應用程式。

以上一節提到的一般架構原則為考量,每個應用程式至少應有兩層:

  • UI 層用於在螢幕上顯示應用程式資料。
  • 資料層包含應用程式的商業邏輯,並公開應用程式資料。

您可以新增名為網域層的額外層,以簡化和重複使用 UI 和資料層之間的互動。

在一般應用程式架構中,使用者介面層會從資料層或選用網域層取得應用程式資料 (位於 UI 層和資料層之間)。
圖 1 一般應用程式架構的圖表。

現代化應用程式的架構

這個「現代化應用程式架構」鼓勵您採用下列技術:

  • 回應式及分層架構。
  • 應用程式所有層的單向資料流 (UDF)。
  • UI 層含有狀態容器,用於管理 UI 的複雜度。
  • 協同程式和資料流。
  • 依附元件插入最佳做法。

如需詳細資訊,請參閱以下各節、目錄中的其他架構頁面,以及包含重要最佳做法摘要的建議頁面

UI 層

UI 層 (或「呈現層」) 是在螢幕上顯示應用程式資料。只要資料因使用者互動 (例如按下按鈕) 或外部輸入 (例如網路回應) 而改變,UI 都應更新以反映變更。

UI 層包含兩件事:

  • 在螢幕上顯示資料的 UI 元素。您可以使用 View 或 Jetpack Compose 函式建構這些元素。
  • 狀態持有物件 (例如 ViewModel 類別) 可保存資料、向 UI 公開資料及處理邏輯。
在一般架構中,UI 層的 UI 元素是取決於狀態持有者,而後者則取決於資料層或選用網域層的類別。
圖 2. UI 在應用程式架構中的角色。

如要進一步瞭解這個圖層,請參閱 UI 層頁面

資料層

應用程式的資料層包含商業邏輯。商業邏輯可為應用程式帶來價值,由決定應用程式建立、儲存及變更資料的規則組成。

資料層由「存放區」組成,每個存放區可包含零至多個資料來源。您應針對應用程式中處理的各種資料類型建立存放區類別。舉例來說,您可以為電影相關資料建立 MoviesRepository 類別,或是為款項相關資料建立 PaymentsRepository 類別。

在一般架構中,資料層存放區可提供資料給應用程式其餘部分,而且依附於資料來源。
圖 3. 資料層在應用程式架構中的角色。

存放區類別負責以下工作:

  • 向應用程式的其餘部分公開資料。
  • 集中處理資料的變更。
  • 解決多個資料來源之間的衝突。
  • 抽象化應用程式其餘部分的資料來源。
  • 涵蓋商業邏輯。

每個資料來源類別都應只負責處理一個資料來源 (可以是檔案、網路來源或本機資料庫)。資料來源類別是應用程式與系統之間處理資料運算的橋樑。

如要進一步瞭解這個圖層,請參閱資料層頁面

網域層

網域層是 UI 和資料層之間的選用層。

網域層負責封裝複雜的商業邏輯,或是多個 ViewModel 重複使用的簡易商業邏輯。此層為選用性質,因為並非所有應用程式都有上述要求。建議您只在必要時使用,例如處理複雜作業或需要可重複使用。

如果包含在內,選用的網域層就可提供 UI 層的依附元件,並依附於資料層。
圖 4. 網域層在應用程式架構中的角色。

這個資料層中的類別通常稱為用途互通性。每個用途都應對單一功能負責。舉例來說,如果多個 ViewModel 依賴時區在螢幕上顯示適當的訊息,則您的應用程式可能具有 GetTimeZoneUseCase 類別。

如要進一步瞭解這個圖層,請參閱網域層頁面

管理元件之間的依附元件

應用程式中的類別需要其他類別才能正常運作。您可以使用下列任一設計模式來收集特定類別的依附元件:

  • 依附元件插入 (DI):依附元件插入功能可讓類別在不需建構依附元件的情況下定義其依附元件。在執行階段,另一個類別負責提供這些依附元件。
  • 服務定位器:服務定位器模式提供了註冊資料庫,可讓類別取得依附元件,而不是建構依附元件。

這些模式可讓您調整程式碼,因為它們提供明確的模式來管理依附元件,而不會複製程式碼或增加複雜度。此外,這些模式可讓您快速切換測試和實際工作環境實作。

建議您遵循依附元件插入模式,並在 Android 應用程式中使用 Hilt 程式庫Hilt 會自動建構物件,方法是依序執行依附元件樹狀結構、提供依附元件的編譯時間保證,以及建立 Android 架構類別的依附元件容器。

一般最佳做法

程式設計是一個創意欄位,建構 Android 應用程式也不例外。解決問題的方法有很多種;您可以在多個活動或片段之間通訊資料、擷取遠端資料並在本機保留資料以供離線模式使用,或是處理任何少數應用程式遇到的任何其他常見情境。

雖然下列建議並非必要,但在大部分情況下,這些建議可以讓程式碼集長期運作更穩定、更易於測試及維護:

不要將資料儲存在應用程式元件中。

避免將應用程式的進入點 (例如活動、服務和廣播接收器) 指定為資料來源。而是應該與其他元件協調,才能擷取與該進入點相關的部分資料。每個應用程式元件都不是短期,具體取決於使用者與裝置的互動情形,以及系統的整體健康狀態。

減少 Android 類別的依附元件。

應用程式元件應是唯一仰賴 Android 架構 SDK API 的類別,例如 ContextToast。將應用程式中的其他類別拉出,有助於提高可測試性,並減少應用程式中的耦合

在應用程式中建立不同模組之間的明確責任界線。

例如,請勿將程式碼從網路載入資料的程式碼分散到程式碼集的多個類別或套件中。同樣地,請勿在相同類別中定義多個不相關的責任,例如資料快取和資料繫結。遵循建議的應用程式架構可協助您完成這項作業。

每個單元盡可能不要公開。

舉例來說,請勿試圖建立會從模組公開內部實作詳細資料的捷徑。您在短期內可能會有一些時間,但隨著程式碼集的發展,您可能需要多次產生技術債。

將重點放在應用程式的獨特核心,以便從其他應用程式中脫穎而出。

請勿藉由重複編寫相同的樣板程式碼來做重新規劃。因此,請將時間和心力放在應用程式的獨特之處,讓 Jetpack 程式庫和其他推薦的程式庫處理重複的樣板。

考慮讓應用程式的每個部分獨立測試。

舉例來說,假設有明確定義的 API 從網路擷取資料,就能更輕鬆地測試在本機資料庫中保留該資料的模組。或者,您將這兩個模組的邏輯放在同一個位置,或是將網路程式碼分配給整個程式碼集,以有效進行測試將變得困難許多。

不同類型應遵守本身的並行政策。

如果某個類型執行長時間執行的封鎖工作,應負責將該運算移至正確的執行緒。這種特定類型知道正在執行的運算類型,以及應在哪個執行緒中執行。類型應具備 main-safe,也就是可以安全地從主執行緒呼叫,而不會封鎖。

請盡可能保留相關且最新的資料。

這樣一來,即使裝置處於離線模式,使用者也能享有應用程式的功能。提醒您,並非所有使用者都能享有穩定高速的高速連線,即使他們使用這些功能,也不會在人多的地方收訊不良。

架構的優點

在應用程式中實作良好的架構,可為專案和工程團隊帶來許多好處:

  • 改善了整個應用程式的可維護性、品質和穩定性。
  • 可讓應用程式調度資源。越來越多人員和團隊為相同的程式碼集貢獻心力,並盡可能減少程式碼衝突。
  • 有助於新手上路。由於架構可提升專案的一致性,新進團隊成員可以快速上手,更有效率地提升效率。
  • 測試起來比較簡單。良好的架構可鼓勵通常更易於測試的較簡單類型。
  • 透過明確定義的程序,可以有系統地調查錯誤。

對架構投注資源也對使用者有直接的影響。這些工程師因此受益於更穩定的應用程式,且工程團隊也提升了更多功能。不過架構也須預先投入心力。為了協助您向公司的其他成員說明這個過程,歡迎參考這些個案研究,看看其他公司如何分享出色的應用程式架構。

範例

下列 Google 範例示範良好的應用程式架構。去探索他們,以瞭解實務做法: