ViewModel 總覽 (Android Jetpack 的一部分)

ViewModel 類別是商業邏輯或畫面層級的狀態容器。這會向 UI 公開狀態,並封裝相關的商業邏輯。其主要優點在於可快取狀態,並透過設定變更保留。因此,在活動之間導覽或執行設定變更 (例如旋轉畫面) 時,UI 不必再次擷取資料。

如要進一步瞭解狀態容器,請參閱狀態容器指南。同樣,如要進一步瞭解 UI 層,請參閱 UI 層指南。

ViewModel 優點

ViewModel 的替代方案為純類別,用於保留您在 UI 中顯示的資料。在活動或 Navigation 到達網頁之間導覽時,可能會發生問題。如未使用儲存執行個體狀態機制來儲存資料,則系統會將其刪除。ViewModel 提供便捷的 API 來保留資料,以解決此問題。

ViewModel 類別的優點主要有兩個方面:

  • 可讓您保留 UI 狀態。
  • 提供商業邏輯存取。

保留

ViewModel 允許以 ViewModel 保留的狀態,以及 ViewModel 觸發的作業存續。這項快取表示您不必再次透過常見的設定變更 (例如畫面旋轉) 來擷取資料。

範圍

將 ViewModel 執行個體化時,必須傳遞一個實作 ViewModelStoreOwner 介面的物件。這可以是 Navigation 到達網頁、Navigation 圖表、活動、片段,或實作介面的任何其他類型。ViewModel 的範圍則會限定於 ViewModelStoreOwner生命週期內。因此會保留在記憶體中,直到其 ViewModelStoreOwner 永久消失。

各種類別包括 ViewModelStoreOwner 介面的直接或間接子類別。直接子類別為 ComponentActivityFragmentNavBackStackEntry。如需間接子類別的完整清單,請參閱 ViewModelStoreOwner 參考資料

當 ViewModel 限定範圍所在的片段或活動遭到刪除時,非同步工作會繼續在限定範圍的 ViewModel 中繼續執行。這是要保留的金鑰。

詳情請參閱下方「ViewModel 生命週期」一節。

SavedStateHandle

SaveStateHandle 不僅可在設定變更期間保留資料,在程序重建期間亦是如此。因此,即使使用者關閉應用程式,稍後再次開啟,您仍然可以保持 UI 狀態。

商業邏輯存取

雖然絕大部分商業邏輯都保留在資料層,但 UI 層也可以包含商業邏輯。例如,合併多個存放區的資料來建立畫面 UI 狀態,或者特定類型的資料不需要資料層。

ViewModel 是處理 UI 層中商業邏輯的適當位置。ViewModel 還負責處理事件,並在需要套用商業邏輯以修改應用程式資料時,將事件委派給階層的其他層。

Jetpack Compose

使用 Jetpack Compose 時,ViewModel 是向可組合項公開畫面 UI 狀態的主要方式。在混合式應用程式中,活動和片段只會代管可組合函式。這是從過去的方法轉變而來,因為使用活動和片段建立可重複使用的 UI 片段並不簡單和直觀,這樣的做法則會讓 UI 控制器更加有效。

將 ViewModel 與 Compose 搭配使用時,請務必注意,不可將 ViewModel 的範圍限定為可組合項。這是因為可組合項並非 ViewModelStoreOwner。在 Composition 中,具有相同可組合項的兩個執行個體,或者在同一 ViewModelStoreOwner 下存取相同 ViewModel 類型的不同可組合項,都會收到 ViewModel 的相同執行個體,這通常不是預期行為。

為了在 Compose 中獲得 ViewModel 的優點,請在片段或活動中代管每個畫面;或者使用 Compose Navigation,並且在盡可能靠近 Navigation 到達網頁的可組合函式中使用 ViewModel。這是因為您可以將 ViewModel 的範圍限定為 Navigation 到達網頁、Navigation 圖表、活動及片段。

詳情請參閱 State 與 Jetpack Compose 指南。

實作 ViewModel

以下是 ViewModel 的實作範例:用途會顯示使用者清單。

Kotlin

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Java

public class MyViewModel extends ViewModel {

    // Expose screen UI state
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<User>>();
            loadUsers();
        }
        return users;
    }

    // Handle business logic
    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

然後,您可以從下列活動中存取清單:

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

import androidx.lifecycle.ViewModelProvider;

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

Jetpack Compose

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

將協同程式與 ViewModel 搭配使用

ViewModel 包括支援 Kotlin 協同程式。能夠以與保留 UI 狀態相同的方式保留非同步工作。

詳情請參閱「Kotlin 協同程式與 Android 架構元件搭配使用」。

ViewModel 的生命週期

ViewModel 的生命週期與範圍具有直接關聯。ViewModel 會保留在記憶體中,直到限定範圍的 ViewModelStoreOwner 消失為止。以下是可能的發生情境:

  • 若是活動,則會在完成時。
  • 若是片段,則會在卸離時。
  • 若是 Navigation 項目,則從返回堆疊中移除時。

因此,ViewModels 非常適合用於儲存在設定變更後仍然有效的資料。

圖 1 說明活動在進行旋轉然後完成時的不同生命週期狀態。上圖也在關聯的活動生命週期旁顯示 ViewModel 的生命週期。這張特殊圖表說明活動的狀態。相同的基本狀態適用於片段的生命週期。

說明 ViewModel 在活動變更狀態時的生命週期。

系統第一次呼叫活動物件的 onCreate() 方法時,您通常會要求 ViewModel。在活動的整個生命週期中 (例如裝置畫面旋轉時),系統可能會多次呼叫 onCreate()ViewModel 存在的時間是從您首次要求 ViewModel 時起算,直到活動完成並刪除為止。

清除 ViewModel 依附元件

ViewModelStoreOwner 在生命週期內刪除 ViewModel 時,ViewModel 會呼叫 onCleared 方法。如此一來,您就能清除以 ViewModel 生命週期為依據的任何工作或依附元件。

以下範例顯示 viewModelScope 的替代方案。viewModelScope 是內建的 CoroutineScope,會自動遵循 ViewModel 生命週期。ViewModel 會根據這個範圍觸發業務相關作業。如果您想使用自訂範圍 (而非 viewModelScope) 簡化測試程序,ViewModel 可以接收 CoroutineScope 做為建構函式中的依附元件。一旦 ViewModelStoreOwner 在生命週期結束時清除 ViewModel,ViewModel 也會取消 CoroutineScope

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

在 Lifecycle 2.5 以上版本中,您可以傳遞一或多個 Closeable 物件到 ViewModel 的建構函式,該函式會在 ViewModel 執行個體清除時自動關閉。

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

最佳做法

以下是實作 ViewModel 時,應遵循的幾項主要最佳做法:

  • 由於範圍限定,請使用 ViewModel 做為畫面層級狀態容器的實作詳細資料。請不要做為可重複使用 UI 元件 (例如方塊群組或表單) 的狀態容器使用。否則會在相同的 ViewModelStoreOwner 下,同一個 UI 元件的不同用法中,取得相同的 ViewModel 執行個體。
  • ViewModel 不應瞭解 UI 實作詳細資料。請盡可能保留 ViewModel API 公開的方法名稱,以及 UI 狀態欄位的名稱。這樣一來,ViewModel 就能支援任何類型的 UI:手機、折疊式裝置、平板電腦,甚至是 Chromebook!
  • 由於 ViewModel 可能保留的時間比 ViewModelStoreOwner 長,因此 ViewModel 不應保留生命週期相關 API 的任何參照 (例如 ContextResources),以免發生記憶體流失。
  • 不要將 ViewModel 傳遞給其他類別、函式或其他 UI 元件。由於平台會管理這些元件,因此請盡可能保持靠近。靠近活動、片段或畫面層級可組合函式。這樣可以防止較低層級的元件,存取不必要的資料和邏輯。

其他資訊

隨著資料越趨複雜,您可能會選擇使用獨立的類別來載入資料。ViewModel 的用途是封裝使用者介面控制器的資料,讓資料於設定變更後仍然有效。如要瞭解如何在設定變更時載入、保留及管理資料,請參閱「儲存 UI 狀態」。

Android 應用程式架構指南」建議建立存放區類別,以處理這些功能。

其他資源

如要進一步瞭解 ViewModel 類別,請參閱下列資源。

說明文件

範例