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 類別,請參閱下列資源。

說明文件

範例