Android 架構建議

本頁提供多個架構的最佳做法和建議。採用這些最佳做法和建議,可提升應用程式的品質、穩定性和擴充性,還能更輕鬆地維護及測試應用程式。

以下最佳做法依主題分組。各自的優先順序反映了團隊的建議程度。優先順序清單如下:

  • 強烈建議:除非此做法與您的方法相衝突,否則應採用此做法。
  • 建議:這種做法可改善應用程式。
  • 選用:在某些情況下,這種做法可改善應用程式。

分層架構

建議的分層架構採用關注點分離做法,透過資料模型使用 UI,符合單一可靠資料來源的原則,並維持單向資料流。以下是分層架構的一些最佳做法:

建議 說明
使用明確定義的資料層
強烈建議
資料層會將應用程式資料公開給應用程式的其餘部分,且包含應用程式絕大多數商業邏輯。
  • 即使只有單一資料來源,仍應建立存放區
  • 在小型應用程式中,可以選擇將資料層類型放入 data 套件或模組。
使用明確定義的 UI 層
強烈建議
UI 層會在螢幕上顯示應用程式資料,且為使用者與應用程式互動的主要管道。
  • 在小型應用程式中,可以選擇將資料層類型放入 ui 套件或模組。
進一步瞭解 UI 層最佳做法
資料層應使用存放區公開應用程式資料。
強烈建議

UI 層中的元件 (例如可組合項、活動或 ViewModel) 不應與資料來源直接互動。資料來源如下:

  • Database、DataStore、SharedPreferences、Firebase API。
  • GPS 位置提供者。
  • 藍牙資料供應商。
  • 網路連線狀態提供者。
使用協同程式和資料流
強烈建議
使用協同程式和資料流,在不同層之間進行通訊。

進一步瞭解協同程式最佳做法

使用網域層
建議用於大型應用程式
如果需要重複使用多個 ViewModel 中與資料層互動的商業邏輯,或是想要簡化特定 ViewModel 的商業邏輯複雜性,則使用網域層用途

UI 層

UI 層的作用是在螢幕上顯示應用程式資料,且為使用者與應用程式互動的主要管道。以下是 UI 層的最佳做法:

建議 說明
遵循單向資料流 (UDF)
強烈建議
遵循單向資料流程 (UDF) 原則,其中 ViewModel 使用觀測器模式公開 UI 狀態,並透過方法呼叫從 UI 接收動作。
如果 AAC ViewModel 對您的應用程式有幫助,建議多加利用。
強烈建議
使用 AAC ViewModel 處理商業邏輯,並擷取應用程式資料,以便在 UI 中公開 UI 狀態 (Compose 或 Android 檢視畫面)。

請參閱這裡的 ViewModel 最佳做法瞭解詳情。

請參閱這裡的 ViewModel 優點

使用生命週期感知方法收集 UI 狀態。
強烈建議
使用適當的生命週期感知協同程式建構工具,從 UI 中收集 UI 狀態︰repeatOnLifecycle (檢視畫面系統) 和 collectAsStateWithLifecycle (Jetpack Compose)

進一步瞭解 repeatOnLifecycle

進一步瞭解 collectAsStateWithLifecycle

請勿將事件從 ViewModel 傳送至 UI。
強烈建議
在 ViewModel 中立即處理事件,並引起狀態更新,進而處理事件。請參閱這裡的 UI 事件瞭解詳情。
使用單一活動應用程式。
建議
如果應用程式有多個畫面,請使用導覽片段導覽 Compose 前往不同畫面,以及深層連結至應用程式。
使用 Jetpack Compose
建議
使用 Jetpack Compose 建構適用於手機、平板電腦、折疊式裝置和 Wear OS 的新應用程式。

下列文字片段說明如何以生命週期感知方式收集 UI 狀態:

檢視畫面

class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Process item
                }
            }
        }
    }
}

Compose

@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
}

ViewModel

ViewModel 負責提供 UI 狀態和資料層存取權。以下是一些 ViewModel 最佳做法:

建議 說明
ViewModel 應不受 Android 生命週期的影響。
強烈建議
ViewModel 不應保留任何生命週期相關類型的參照。不要將 Activity, Fragment, ContextResources 做為依附元件進行傳遞。如果 ViewModel 中有任何項目需要 Context,您應密切評估項目是否位於正確的層。
使用協同程式和資料流
強烈建議

ViewModel 透過以下方式與資料或網域層互動:

  • Kotlin 資料流,用於接收應用程式資料。
  • suspend 函式,使用 viewModelScope 執行動作。
在畫面層級使用 ViewModel。
強烈建議

不要在可重複使用的 UI 中使用 ViewModel,您應在以下位置使用 ViewModel:

  • 畫面層級可組合項
  • 檢視畫面中的活動/片段
  • 使用 Jetpack Navigation 時的到達網頁或圖表。
在可重複使用的 UI 元件中使用純狀態容器類別
強烈建議
使用純狀態狀態容器類別,處理可重複使用的複雜 UI 元件,這樣就能在外部提升及控制狀態。
不要使用 AndroidViewModel
建議
使用 ViewModel 類別,而非 AndroidViewModel。不應在 ViewModel 中使用 Application 類別,請改為將依附元件移至 UI 或資料層。
公開 UI 狀態。
建議
ViewModel 應透過名為 uiState 的單一屬性,向 UI 公開資料。如果 UI 顯示多個不相關的資料,VM 可能會公開多個 UI 狀態屬性
  • 您應將 uiState 設為 StateFlow
  • 您應使用 stateIn 運算子搭配 WhileSubscribed(5000) 政策 (範例),建立 uiState 做為階層中其他層的資料串流。
  • 如果在更簡單的案例中,沒有任何資料串流來自資料層,則可以使用 MutableStateFlow 做為不可變更的 StateFlow (範例) 公開。
  • 您可以選擇將 ${Screen}UiState 設為資料類別,其中可包含資料、錯誤和載入信號。如果排除不同的狀態,這個類別也可以是密封類別。

以下文字片段概述了如何從 ViewModel 公開 UI 狀態:

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

生命週期

以下是使用 Android 生命週期的一些最佳做法:

建議 說明
不要覆寫活動或片段中的生命週期方法。
強烈建議
不要覆寫活動或片段中的生命週期方法,例如 onResume。請改用 LifecycleObserver。如果應用程式在生命週期達到特定 Lifecycle.State 時需要執行作業,請使用 repeatOnLifecycle API。

下列文字片段概述了如何根據特定生命週期狀態來執行作業:

View

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // ...
            }
            override fun onPause(owner: LifecycleOwner) {
                // ...
            }
        }
    }
}

Compose

@Composable
fun MyApp() {

    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, ...) {
        val lifecycleObserver = object : DefaultLifecycleObserver {
            override fun onStop(owner: LifecycleOwner) {
                // ...
            }
        }

        lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
        }
    }
}

處理依附元件

管理元件之間的依附元件時,應遵守下列幾項最佳做法:

建議 說明
使用依附元件插入
強烈建議
使用依附元件插入最佳做法,主要是建構函式插入 (如適用)。
視需要將範圍限定為元件。
強烈建議
如果類型包含需要共用的可變動資料,或是類型初始化成本高昂並廣泛用於應用程式,則可將範圍限定為依附元件容器
使用 Hilt
建議
在簡易應用程式中使用 Hilt手動依附元件插入。如果您的專案夠複雜,請使用 Hilt。舉例來說,如果出現以下情形:
  • 使用 ViewModel 的多個螢幕 - 整合
  • WorkManager 用量 - 整合
  • 預先使用 Navigation,例如 ViewModels 範圍限定為導覽圖 - 整合。

測試

以下是測試的一些最佳做法:

建議 說明
瞭解測試項目
強烈建議

除非專案大致與 hello world 應用程式一樣,否則請至少測試以下項目:

  • ViewModel 單元測試,包括資料流。
  • 資料層實體單元測試。即存放區和資料來源。
  • 實用的 UI 導覽測試,適合在持續整合 (CI) 中使用迴歸測試。
假的實作優先於模擬。
強烈建議
詳情請參閱在 Android 說明文件中使用測試替身
測試 StateFlows。
強烈建議
測試 StateFlow 時:

詳情請參閱 Android DAC 指南中的測試項目

模型

在應用程式中開發模型時,您應觀察下列最佳做法:

建議 說明
在複雜的應用程式中,為每層建立模型。
建議

請視需要在複雜的應用程式中,以不同的層或元件建立新模型。請見以下範例:

  • 遠端資料來源可以將其透過網路接收的模型,對應至僅包含應用程式所需資料的更簡單的類別
  • 存放區可以將 DAO 模型,對應至僅包含 UI 層所需資訊的更簡單的資料類別。
  • ViewModel 可包含 UiState 類別中的資料層模型。

命名慣例

為程式碼集命名時,您應瞭解下列最佳做法:

建議 說明
命名方法。
選用
方法應為動詞片語。例如:makePayment()
為屬性命名。
選用
屬性必須為名詞片語。例如:inProgressTopicSelection
為資料串流命名。
選用
類別公開資料串流、LiveData 或任何其他串流時,命名慣例為 get{model}Stream()。例如,getAuthorStream(): Flow<Author> 如果函式會傳回模型清單,則模型名稱應採用複數形式:getAuthorsStream(): Flow<List<Author>>
為介面實作命名。
選用
介面實作的名稱應具有意義。如果找不到更合適的名稱,請將 Default 做為前置字串。舉例來說,針對 NewsRepository 介面,您可以使用 OfflineFirstNewsRepositoryInMemoryNewsRepository。如果找不到好的名稱,請使用 DefaultNewsRepository。假的實作應將 Fake 做為前置字串,如 FakeAuthorsRepository 中所示