ViewModel 總覽 (Android Jetpack 的一部分)

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

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

ViewModel 優點

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

ViewModel 類別有兩項主要優點:

  • 持續保留 UI 狀態。
  • 提供商業邏輯存取。

持續保留資料

透過 ViewModel 保留的狀態和觸發的作業,ViewModel 可持續保留資料。有了這項快取功能,當發生螢幕旋轉等常見的設定變更時,就不必再次擷取資料。

範圍

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

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

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

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

SavedStateHandle

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

商業邏輯存取

雖然絕大部分商業邏輯都存在於資料層,但 UI 層也可以包含商業邏輯。這類情況包括合併多個存放區的資料來建立螢幕 UI 狀態,或特定類型的資料不需要資料層時。

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

Jetpack Compose

使用 Jetpack Compose 時,ViewModel 是向可組合項公開畫面 UI 狀態的主要方式。在混合式應用程式中,活動和片段只會代管可組合函式。這與過去的方法不同,過去建立包含活動和片段的可重複使用 UI 時,較不簡單直觀,導致 UI 遠比 UI 控制器更活躍。

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

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

詳情請參閱 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 DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

接著,您可以從活動中存取 ViewModel,如以下程式碼片段所示:

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

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(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.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // 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 元件,切勿將 ViewModel 用做這類元件的狀態容器。否則會在相同的 ViewModelStoreOwner 下,同一個 UI 元件的不同用法中,取得相同的 ViewModel 執行個體。
  • ViewModel 不應瞭解 UI 實作詳細資料。請盡可能保留 ViewModel API 公開的方法名稱,以及 UI 狀態欄位的名稱。這樣一來,ViewModel 就能支援任何類型的 UI:手機、摺疊式裝置、平板電腦,甚至是 Chromebook!
  • 由於 ViewModel 的存續時間可能比 ViewModelStoreOwner 長,因此 ViewModel 不應保留生命週期相關 API (例如 ContextResources) 的任何參照,以免發生記憶體流失。
  • 切勿將 ViewModel 傳遞給其他類別、函式或 UI 元件。由於平台會管理這些元件,因此請盡可能保持靠近。靠近活動、片段或畫面層級可組合函式。這樣可以防止較低層級的元件,存取不必要的資料和邏輯。

其他資訊

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

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

其他資源

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

說明文件

範例