應用程式架構指南

本指南包含建構最佳做法和建議架構,以用於建構強大的高品質應用程式。

行動應用程式使用者體驗

一般 Android 應用程式含有多個應用程式元件,包括活動片段服務內容供應器廣播接收器。您必須在應用程式資訊清單中宣告大部分的應用程式元件。接著,Android 作業系統會根據這個檔案決定如何將您的應用程式整合至裝置的整體使用者體驗。假設一般的 Android 應用程式可能會包含多個元件,使用者通常在短時間內就會與多個應用程式互動,因此應用程式必須配合各種不同的使用者導向工作流程和工作進行調整。

多種板型規格

應用程式可在多種板型規格上執行,包括手機、平板電腦、折疊式裝置、ChromeOS 裝置等。應用程式無法假設為直向或橫向,因為在單一工作階段中,應用程式可能會以其中一種或兩種方向執行。設定變更 (例如使用者將折疊式裝置變更為桌上型或書本模式) 可能會強制應用程式重新組合 UI,進而影響應用程式資料和狀態。

資源限制

請注意,行動裝置也會受到資源限制,作業系統隨時可能停止部分應用程式處理程序,以騰出空間使用新的裝置。

變數啟動條件

鑑於此環境的條件,您的應用程式元件可能會個別和無序地啟動,並且操作系統或使用者可以隨時將其刪除。由於這些事件不在您的控管之下,您不應在應用程式元件中儲存或保留任何應用程式資料或狀態,而且應用程式元件不應該彼此相依。

常見架構原則

如果不要使用應用程式元件來儲存應用程式資料和狀態,應該如何設計應用程式?

隨著 Android 應用程式的規模不斷擴充,請務必定義架構,讓應用程式能夠調度資源、配合不同形狀和大小的螢幕調整版面配置、提升應用程式的穩定性,並讓應用程式更容易進行測試。

應用程式架構會定義應用程式各部分之間的關聯,以及每個部分應負的責任。為符合所有規範,在設計應用程式架構時,請務必遵循下列特定原則。

關注點分離

最重要的原則是區隔注意事項。常見的做法是在 ActivityFragment 中寫入所有程式碼。這些 UI 類別應該只包含處理 UI 和作業系統互動操作的邏輯。因此,請盡可能讓這些類別保持精簡,避免出現與元件生命週期相關的許多問題,並提高這些類別的可測試性。

請注意,您不需要擁有 ActivityFragment 的實作,而是只是代表 Android OS 與應用程式之間合約的黏附類別。作業系統可能會隨時根據使用者互動或系統狀況 (例如記憶體不足),隨時進行刪除。如要提供令人滿意的使用者體驗和更易於管理的應用程式維護體驗,建議您盡可能減少依附元件。

自動調整式版面配置

應用程式應妥善處理設定變更,例如使用者在直向和橫向裝置螢幕方向之間切換,以及在大螢幕上執行應用程式時,螢幕大小不同等情況。應用程式實作標準自動調整式版面配置後,就能在各種板型規格上提供最佳使用者體驗。

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

另一個重要原則是,您應將使用者介面從資料模型 (建議使用永久模型) 驅動。資料模型代表應用程式的資料。兩者與應用程式中的使用者介面元素和其他元件無關。這表示,並未綁定使用者介和應用程式元件的生命週期,但當作業系統決定從記憶體中移除應用程式的處理程序時,仍會銷毀。

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

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

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

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

單一可靠資料來源

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

這種模式有以下多項優點:

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

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

單向資料流

在我們的指南中,單一可靠資料來源原則通常會與單向資料流 (UDF) 模式搭配使用。在 UDF 中,狀態只會朝單一方向流動。以相反方向修改資料流程的事件

在 Android 中,狀態或資料通常會從階層中範圍較大的型別,流向範圍較小的型別。事件通常會從範圍較小的型別觸發,直到抵達對應資料型別的 SSOT 為止。舉例來說,應用程式資料通常會從資料來源流向 UI。使用者事件 (例如按下按鈕) 會從 UI 傳送至 SSOT,應用程式資料會在 SSOT 中修改,並以不可變更的型別公開。

這個模式能更妥善地維護資料一致性、減少錯誤發生機率、簡化偵錯程序,並帶來 SSOT 模式的所有優點。

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

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

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

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

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

現代化應用程式架構

這項現代應用程式架構建議使用下列技術:

  • 自我調適的分層架構。
  • 應用程式所有層級的單向資料流 (UDF)。
  • 使用者介面層,其中包含用於管理 UI 複雜度的狀態容器。
  • 協同程式和資料流。
  • 依附元件注入最佳做法。

詳情請參閱以下各節、目錄中的其他「架構」頁面,以及建議頁面,其中包含最重要的最佳做法摘要。

UI 層

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

使用者介面層包含兩個部分:

  • 在畫面上顯示資料的 UI 元素。您可以使用檢視區塊或 Jetpack Compose 函式建構這些元素。Views 和 Jetpack Compose 都支援自動調整式版面配置。
  • 狀態持有者 (例如 ViewModel 類別) 保存資料、將其公開至使用者介面並處理邏輯。
在一般架構中,UI 層的 UI 元素是取決於狀態持有者,而後者則取決於資料層或選用網域層的類別。
圖 2. UI 層在應用程式架構中的角色。

如要進一步瞭解這個層,請參閱使用者介面層頁面

資料層

應用程式的資料層包含商業邏輯。商業邏輯可為應用程式提供價值,且會根據決定應用程式建立、儲存及變更資料的規則組成。

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

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

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

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

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

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

網域層

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

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

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

這個圖層中的類別通常稱為「用途」或「互通者」。每次使用時,都必須對單一功能負責。舉例來說,如果多個 ViewModel 仰賴時區以在螢幕上顯示適當的訊息,則應用程式可能擁有 GetTimeZoneUseCase 類別。

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

管理元件之間的依附元件

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

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

這些模式可讓您擴充程式碼,因為該格式提供了管理依附元件的明確模式,而不必複製程式碼或增加複雜度。此外,這些模式還可讓您快速切換測試和實際工作環境實作。

一般最佳做法

程式設計是廣告素材欄位,建構 Android 應用程式也不例外。解決問題的方法有很多種;您可能會在多個活動或片段之間傳輸資料、擷取遠端資料,並在本機儲存資料以供離線模式使用,或處理任何不常見的應用程式遇到的任何其他常見情況。

雖然下列建議做法並非必要,但在大多數情況下,這些程式碼能讓您更穩固、長期地測試及維護程式碼:

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

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

減少 Android 類別的依附元件。

您的應用程式元件應成為僅仰賴 Android 架構 SDK API (例如 ContextToast) 的類別。排除應用程式中的其他類別,可幫助測試及減少應用程式中的組合

在應用程式中,定義模組之間的明確責任範圍。

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

每個單元盡可能不要曝光。

舉例來說,請勿試圖新建一個從模組公開內部實作的快捷方式。雖然短期內可能會出現一些時間,但隨著程式碼基礎的發展,您可能需要支付技術債務。

將重點放在應用程式的獨特核心,讓應用程式脫穎而出。

請勿藉由重複撰寫相同的樣板程式碼來做無謂的重複作業。因此,請將時間與精力投注在應用程式的獨特之處,並讓 Jetpack 程式庫和其他推薦的資料庫處理重複的樣板。

使用標準版面配置和應用程式設計模式。

Jetpack Compose 程式庫提供功能強大的 API,可建構適應性使用者介面。在應用程式中使用標準版面配置,提升多種板型規格和螢幕尺寸的使用者體驗。請參閱應用程式設計模式的圖庫,選出最適合您用途的版面配置。

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

舉例來說,假設 API 是針對網路擷取資料的明確定義 API,便可更輕鬆地測試在本機資料庫中保留該資料的模組。或者,假如您將這兩個模組的邏輯放在同一個位置,或是將網路程式碼分配給整個程式碼集,那麼這樣的測試將變得困難許多,甚至無法做到。

型別負責其並行政策。

如果某個型別執行長時間的封鎖作業,就必須負責將該運算作業移至正確的執行緒。該特定型別知道自己執行的運算類型,以及應執行的執行緒。型別應對主執行緒沒有威脅,意味著可安全地從主執行緒呼叫,不會阻斷主執行緒。

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

這樣一來,即使裝置處於離線模式,也能享有應用程式的功能性。提醒您,並非所有使用者都能享有穩定快速的高速連線,即使他們使用這些功能,也不會在擁擠場所中收到不良訊號。

架構的優點

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

  • 這有助於提升應用程式整體的維護性、品質和穩定性。
  • 這可讓應用程式調整大小。更多人與團隊可以為同一個程式碼集貢獻心力,且程式碼衝突情況極少。
  • 有助於新手上路。架構可為專案帶來一致性,因此團隊新成員能快速上手,在較短的時間內提高效率。
  • 測試更輕鬆。良好的架構會鼓勵使用較簡單的型別,這類型別通常更容易測試。
  • 透過定義完善的程序,有條不紊地調查錯誤。

投資架構也會直接影響使用者。工程團隊效率提升後,他們就能享有更穩定的應用程式和更多功能。不過,架構也需要預先投入時間。如要向公司其他成員說明這段時間的必要性,請參閱這些個案研究,瞭解其他公司在應用程式中採用良好架構後,如何獲得成功。

範例

以下 Google 範例展現了良好的應用程式架構。歡迎查看這些範例,瞭解實務做法: