知識點 | ViewModel 四種集成方式

ViewModel 庫一發布,便成爲了 Jetpack 中的核心組件之一。咱們在 2019 年作的一份開發者問卷顯示,超過 40% 的 Android 開發者已經在本身的應用中使用了 ViewModel。ViewModel 能夠將數據層與 UI 分離,而這種架構不只能夠簡化 UI 的生命週期的控制,也能讓代碼得到更好的可測試性。若是想了解更多,能夠參考 ViewModel: 簡單介紹視頻官方文檔html

因爲 ViewModel 是許多功能實現的基礎,咱們在過去的幾年裏作了許多工做來改進 ViewModel 的易用性,也讓它可以更加簡便地與其餘組件庫相結合。下面的文章中,我將介紹 ViewModel 的四種集成方式:java

  • ViewModel 中的 Saved State —— 後臺進程重啓時,ViewModel 的數據恢復;
  • 在 NavGraph 中使用 ViewModel —— ViewModel 與導航 (Navigation) 組件庫的集成;
  • ViewModel 配合數據綁定 (data-binding) —— 經過使用 ViewModel 和 LiveData 簡化數據綁定;
  • viewModelScope —— Kotlin 協程與 ViewModel 的集成。

ViewModel 的 Saved State —— 後臺進程重啓時,ViewModel 的數據恢復

  • 於 lifecycle-viewmodel-savedstate 的 1.0.0-alpha01 版本時加入
  • 支持 Java 和 Kotlin

onSaveInstanceState 帶來的挑戰android

ViewModel 一發布,執行 onSaveInstanceState 的相關的邏輯時要如何操做 ViewModel,便成爲了一個使人困惑的問題。Activity 和 Fragment 一般會在下面三種狀況下被銷燬:git

  1. 從當前界面永久離開 : 用戶導航至其餘界面或直接關閉 Activity (經過點擊返回按鈕或執行的操做調用了 finish() 方法)。對應 Activity 實例被永久關閉;
  2. Activity 配置 (configuration) 被改變 : 例如,旋轉屏幕等操做,會使 Activity 須要當即重建;
  3. 應用在後臺時,其進程被系統殺死 : 這種狀況發生在設備剩餘運行內存不足,系統又亟須釋放一些內存的時候。當進程在後臺被殺死後,用戶又返回該應用時,Activity 也須要被重建。

在後兩種狀況中,咱們一般都但願重建 Activity。ViewModel 會幫您處理第二種狀況,由於在這種狀況下 ViewModel 沒有被銷燬;而在第三種狀況下, ViewModel 被銷燬了。因此一旦出現了第三種狀況,便須要在 Activity 的 onSaveInstanceState 相關回調中保存和恢復 ViewModel 中的數據。我在 ViewModels: 持久化、onSaveInstanceState()、恢復 UI 狀態與加載器 一文中更加詳細地描述了這兩種狀況的區別。github

Saved State 模塊編程

如今,ViewModel Saved State 模塊將會幫您在應用進程被殺死時恢復 ViewModel 的數據。在免除了與 Activity 繁瑣的數據交換後,ViewModel 也真正意義上的作到了管理和持有全部本身的數據。c#

ViewModel 的這一新功能是經過 SavedStateHandle 實現的。SavedStateHandle 和 Bundle 同樣,以鍵值對形式存儲數據,它包含在 ViewModel 中,而且能夠在應用處於後臺時進程被殺死的狀況下倖存下來。諸如用戶 id 等須要在 onSaveInstanceState 時獲得保存下來的數據,如今均可以存在 SavedStateHandle 中。架構

設置 Save State 模塊app

如今讓咱們看看如何使用 SaveState 組件。注意接下來的代碼會和 Lifecycles Codelab 第六步中的一段代碼十分類似。那段是 Java 代碼,而接下來的是 Kotlin 代碼:框架

第一步: 添加依賴

SaveStateHandle 目前在一個獨立的模塊中,您須要在依賴中添加:

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

注意,本文發佈時 lifecycle 組件的最新穩定版爲 2.2.0,若是您但願持續關注相關組件庫的進展,能夠查看 lifecycle 版本發佈文檔

第二步: 修改調用 ViewModelProvider 的方式

接下來,您須要建立一個持有 SaveStateHandle 的 ViewModel。在 Activity 或 Fragment 的 onCreate 方法中,將 ViewModelProvider 的調用修改成:

//下面的 Kotlin 擴展須要依賴如下或更新新版本的 ktx 庫:
//androidx.fragment:fragment-ktx:1.0.0(最新版本 1.2.4) 或
//androidx.activity:activity-ktx:1.0.0 (最新版本 1.1.0)
val viewModel by viewModels { SavedStateViewModelFactory(application, this) }
// 或者不使用 ktx
val viewModel = ViewModelProvider(this, SavedStateViewModelFactory(application, this))
            .get(MyViewModel::class.java)

建立 ViewModel 的類是 ViewModel 工廠 (ViewModel factory),而建立包含 SaveStateHandle 的 View Model 的工廠類是 SavedStateViewModelFactory。經過此工廠建立的 ViewModel 將持有一個基於傳入 Activity 或 Fragment 的 SaveStateHandle。

第三步: 使用 SaveStateHandle

當前面的步驟準備完成時,您就能夠在 ViewModel 中使用 SavedStateHandle 了。下面是一個保存用戶 ID 的示例:

class MyViewModel(state :SavedStateHandle) :ViewModel() {

    // 將Key聲明爲常量
    companion object {
        private val USER_KEY = "userId"
    }

    private val savedStateHandle = state

    fun saveCurrentUser(userId: String) {
        // 存儲 userId 對應的數據
        savedStateHandle.set(USER_KEY, userId)
    }

    fun getCurrentUser(): String {
        // 從 saveStateHandle 中取出當前 userId
        return savedStateHandle.get(USER_KEY)?: ""
    }
}
  1. 構造方法 : SavedStateHandle 做爲構造方法參數傳入 MyViewModel;
  2. 保存 : saveNewUser 方法展現了使用鍵值對的形式保存 USER_KEY 和 userId 到 SaveStateHandle 的例子。每當數據更新時,要保存新的數據到 SavedStateHandle;
  3. 獲取 : 如代碼中所示,調用 savedStateHandle.get(USER_KEY) 方法獲取被保存的 userId。

如今,不管是第二仍是第三種狀況下,SavedStateHandle 均可以幫您恢復界面數據了。

若是您想要在 ViewModel 中使用 LiveData,能夠調用 SavedStateHandle.getLiveData(),示例以下:

// getLiveData 方法會取得一個與 key 相關聯的 MutableLiveData 
// 當與 key 相對應的 value 改變時 MutableLiveData 也會更新。
private val _userId : MutableLiveData<String> = savedStateHandle.getLiveData(USER_KEY)

// 只暴露一個不可變 LiveData 的狀況
val userId : LiveData<String> = _userId

如需瞭解更多,請移步至 Lifecycles Codelab 第六步官方文檔

ViewModel 與 Jetpack 導航: 在 NavGraph 中使用 ViewModel

  • 於 navigation 的 2.1.0-rc01 版本時加入
  • 支持 Java 與 Kotlin

共享 ViewModel 數據所帶來的挑戰

Jetpack 導航組件 (Navigation) 十分適用於那些只有少許或一個 Activity,可是 Activity 中會包含多個 Fragment 的應用。Ian Lake 在他的演講: 單 Activity 架構: 爲何、什麼狀況下以及如何使用中介紹了一些咱們選擇單一 Activity 架構的緣由,而與本文相關的一點,是這種架構容許在多個界面 (destination) 間共享 ActivityViewModel。您能夠用 Activity 建立一個 ViewModel 實例,而後從這個 Activity 中的任一個 Fragment 中得到 ViewModel 的引用:

// 在Fragment的 onCreate 或 onActivityCreated 方法中執行
// 這個Kotlin擴展須要依賴最KTX庫:androidx.fragment:fragment-ktx:1.1.0
val sharedViewModel: ActivityViewModel by activityViewModels()

假設咱們有這樣一個單 Activity 應用,它包含了八個 Fragment,其中四個 Fragment 是購買支付流程:

△ 包含一些購買支付流程的導航圖 (Navigation Graph)

這四個頁面須要共享一些諸如收貨地址、是否使用了優惠券等信息。按照前面所講的作法,須要共享的數據會放在一個 ActivityViewModel 中,但這同時也意味着全部八個頁面都會共享這些數據。支付流程外的界面並不須要關心這些數據,這麼作顯然並不合適。

ViewModel 與 NavGraph 集成

Navigation 2.1.0 中引入了依託一個導航圖 (navigation graph) 建立 ViewModel 的功能。在使用時,您須要先把一個界面集合 (例如: 登陸流程、支付流程的相關界面),放到一個 嵌套導航圖 (nested navigation graph) 中。此時再經過嵌套導航圖建立出 ViewModel,即可以在相關界面中共享數據了。

想要建立嵌套導航圖,您須要選中對應流程相關的界面,點擊鼠標右鍵,並選擇 Nested Graph → New Graph:

△ 建立嵌套導航圖的截圖

注意嵌套導航圖在 XML 文件中的 id,在這裏是 checkout_graph:

<navigation app:startDestination="@id/homeFragment" ...>
    <fragment android:id="@+id/homeFragment" .../>
    <fragment android:id="@+id/productListFragment" .../>
    <fragment android:id="@+id/productFragment" .../>
    <fragment android:id="@+id/bargainFragment" .../>

    <navigation 
      android:id="@+id/checkout_graph" 
      app:startDestination="@id/cartFragment">

        <fragment android:id="@+id/orderSummaryFragment".../>
        <fragment android:id="@+id/addressFragment" .../>
        <fragment android:id="@+id/paymentFragment" .../>
        <fragment android:id="@+id/cartFragment" .../>

    </navigation>

</navigation>

以上工做完成時,即可以使用 by navGraphViewModels 獲取到對應的 ViewModel:

val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)

Java 中一樣適用,代碼以下:

public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // 設置其餘 fragment 
    NavController navController = NavHostFragment.findNavController(this);

    ViewModelProvider viewModelProvider = new ViewModelProvider(this,
        navController.getViewModelStore(R.id.checkout_graph));

    CheckoutViewModel viewModel = viewModelProvider.get(CheckoutViewModel.class);

    // 使用 Checkout ViewModel
}

須要注意的是,嵌套導航圖相對於導航圖的其餘部分是一個獨立的總體。您沒法導航至嵌套導航圖中包含的某個特定界面;當您導航至一個嵌套導航圖時,打開的只會是其中的開始界面 (startDestination)。這種特性使得嵌套導航圖適合用於封裝特定流程的界面組合,好比前面提到過的登陸和支付流程。

ViewModel 與 NavGraph 的集成,是 2019 年 I/O 大會所發佈的關於 Navigation 框架的新特性之一。

詳細瞭解更多,請參閱:

)

ViewModel 與 Data Binding: 在 Data Binding 中使用 ViewModel 和 LiveData

  • 於 Android Studio 的 3.1 版本時加入
  • 支持 Java 與 Kotlin

移除 LiveData 相關的模板代碼

ViewModel、LiveData 與 Data Binding 的集成方式並非什麼新功能,但它始終很是好用。ViewModel 一般都包含一些 LiveData,而 LiveData 意味着能夠被監聽。因此最多見的使用場景是在 Fragment 中給 LiveData 添加一個觀察者:

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)

    myViewModel.name.observe(this, { newName ->
        // 更新UI,這裏是一個TextView
        nameTextView.text = newName
    })

}

Data Binding 是一個經過觀察數據變化來更新 UI 的組件庫。經過 ViewModel、LiveData 和 Data Binding 的組合,您能夠移除以往給 LiveData 添加觀察者的作法,改成直接在 XML 中綁定 View Model 和 LiveData。

使用 Data Binding、ViewModel 和 LiveData

假設您但願在 XML 佈局文件中引用 ViewModel:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewmodel"
type="com.android.MyViewModel"/>
</data>
<... Rest of your layout ...>
</layout>

調用 binding.setLifecycleOwner(this) 方法,而後將 ViewModel 傳遞給 binding 對象,就能夠將 LiveData 與 Data Binding 結合起來:

class MainActivity : AppCompatActivity() {

    // 這個ktx擴展須要依賴 androidx.activity:activity-ktx:1.0.0
    // 或更新版本
    private val myViewModel: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

         //填充視圖並建立 Data Binding 對象
        val binding: MainActivityBinding = 
            DataBindingUtil.setContentView(this, R.layout.main_activity)

        //聲明這個 Activity 爲 Data Binding 的 lifecycleOwner
        binding.lifecycleOwner = this

        // 將 ViewModel 傳遞給 binding
        binding.viewmodel = myViewModel
    }
}

如今,您能夠像下面這樣使用 ViewModel:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="viewmodel" 
                  type="com.android.MyViewModel"/>
    </data>
    <TextView
            android:id="@+id/name"
            android:text="@{viewmodel.name}"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"/>
</layout>

注意,這裏的 viewmodel.name 既能夠是 String 類型,也能夠是 LiveData。若是它是 LiveData,那麼 UI 將根據 LiveData 值的改變自動刷新。

ViewMode 與 Kotlin 協程: viewModelScope

  • 於 Lifecycle 的 2.1.0 版本時加入
  • 只支持 Kotlin

Android 平臺上的協程

一般狀況下,咱們使用回調 (Callback) 處理異步調用,這種方式在邏輯比較複雜時,會致使回調層層嵌套,代碼也變得難以理解。Kotlin 協程 (Coroutines) 一樣適用於處理異步調用,它讓邏輯變得簡單的同時,也確保了操做不會阻塞主線程。若是您不瞭解協程,這裏有一系列很棒的博客 《在 Android 開發中使用協程》 以及 codelab: 在 Android 應用中使用 Kotlin 協程 以供參考。

一段簡單的協程代碼:

// 下面是示例代碼,真實情景下不要使用 GlobalScope 
GlobalScope.launch {
    longRunningFunction()
    anotherLongRunningFunction()
}

這段示例代碼只啓動了一個協程,但咱們在真實的使用環境下很容易建立出許多協程,這就不免會致使有些協程的狀態沒法被跟蹤。若是這些協程中恰好有您想要中止的任務時,就會致使任務泄漏 (work leak)。

爲了防止任務泄漏,您須要將協程加入到一個 CoroutineScope 中。CoroutineScope 能夠持續跟蹤協程的執行,它能夠被取消。當 CoroutineScope 被取消時,它所跟蹤的全部協程都會被取消。上面的代碼中,我使用了 GlobalScope,正如咱們不推薦隨意使用全局變量同樣,這種方式一般不推薦使用。因此,若是想要使用協程,您要麼限定一個做用域 (scope),要麼得到一個做用域的訪問權限。而在 ViewModel 中,咱們可使用 viewModelScope 來管理協程的做用域。

viewModelScope

當 ViewModel 被銷燬時,一般都會有一些與其相關的操做也應當被中止。

例如,假設您正在準備將一個位圖 (bitmap) 顯示到屏幕上。這種操做就符合咱們前面提到的一些特徵: 既不能在執行時阻塞主線程,又要求在用戶退出相關界面時中止執行。使用協程進行此類操做時,就應當使用 viewModelScope.viewModelScope:kotlinx.coroutines.CoroutineScope)。

viewModelScope 是一個 ViewModel 的 Kotlin 擴展屬性。正如前面所說,它能在 ViewModel 銷燬時 (onCleared()) 方法調用時) 退出。這樣一來,只要您使用了 ViewModel,您就可使用 viewModelScope 在 ViewModel 中啓動各類協程,而不用擔憂任務泄漏。

示例以下:

class MyViewModel() : ViewModel() {

    fun initialize() {
        viewModelScope.launch {
            processBitmap()
        }
    }

    suspend fun processBitmap() = withContext(Dispatchers.Default) {
        // 在這裏作耗時操做
    }

}

詳細瞭解更多,請參閱:

總結

本文中,咱們講了:

  1. ViewModel 使用 SaveStateHandle 組件處理 onSaveInstanceState 相關邏輯;
  2. 經過配合 View Model 和導航圖來精確限定數據在 Fragment 中的共享範圍;
  3. 使用 DataBinding 庫時,能夠將 ViewModel 傳遞給數據綁定 (binding),若是同時有在 ViewModel 中使用 LiveData,則能夠經過 binding.setLifecycleOwner(lifecycleOwner) 讓 UI 根據 LiveData 自動更新;
  4. 在 ViewModel 中使用 Kotlin 協程時,使用 viewModelScope 來讓協程在 ViewModel 被銷燬時自動取消。

以上這些功能不少都來自社區提交的請求和反饋,若是您正在尋找 ViewModel 相關的功能,能夠留意 功能需求列表 或者考慮 提交本身的需求

若是您想了解架構組件和 Android Jetpack 的最新進展,請關注 Android 開發者博客,並留意 AndroidX 發佈文檔

若是您對這些功能仍有疑問,能夠在下方留言。感謝閱讀!

相關文章
相關標籤/搜索