Android Jetpack - ViewModel

ViewModel 簡述

ViewModel 旨在以生命周期感知的形式存儲和管理 UI 控制器(Activity/Fragment 等)相關(guān)的數(shù)據(jù),可以解決 UI 控制器中數(shù)據(jù)無法正確保留以及數(shù)據(jù)在其復(fù)雜的生命周期中難以維護(hù)的痛點(diǎn),它的生命周期感知能力需要配合 Lifecycles 組件才能實(shí)現(xiàn),本文聚焦于 ViewModel 所以先不講 Lifecycles ,關(guān)于 Lifecycles 我會在其它文章詳細(xì)介紹

為什么使用 ViewModel ?

我覺得這個(gè)問題很重要,當(dāng)我們使用任何一個(gè)新工具的時(shí)候都需要弄清楚這個(gè)問題,要結(jié)合實(shí)際情況而非盲目跟隨,接下來我會逐一嘗試說明 ViewModel 對比傳統(tǒng)方案的優(yōu)劣

只要你接觸 Android 開發(fā)一段時(shí)間,都不可避免的會遇到 “轉(zhuǎn)屏” 問題

好好的數(shù)據(jù)在你轉(zhuǎn)屏的瞬間,莫名其妙的消失了

發(fā)生以上情況和 Activity 的配置更改有關(guān), 屏幕旋轉(zhuǎn)屬于配置更改(Activity 生命周期內(nèi)自行處理的配置更改)的情況之一,其它類似的還包括接入外置鍵盤、檢測到了 SIM 并更新了 MNC、布局方向發(fā)生了變化等十幾種情況,發(fā)生這些情況時(shí)系統(tǒng)默認(rèn)會關(guān)閉并重建 Activity ,這就導(dǎo)致了上面數(shù)據(jù)莫名其妙消失的問題。而我們傳統(tǒng)的處理辦法就是在配置變更期間保留對象和自行處理配置變更這兩種,這兩種方式都有很多坑(看看官方文檔就知道了),尤其是需要恢復(fù)的數(shù)據(jù)比較多的時(shí)候,而 ViewModel 就非常適合處理這些情況

在下圖中,你可以看到一個(gè) Activity 旋轉(zhuǎn)過程的生命周期,綠色部分是與此 Activity 相關(guān)聯(lián)的 ViewModel 的生命周期,圖例中只展示了 Activity ,而 ViewModel 也同樣可以和 Fragment 配合使用

ViewModel 會從你第一次創(chuàng)建(通常在 onCreate 時(shí))直到此 Activity 完成并銷毀,Activity 在生命周期中可能會多次銷毀創(chuàng)建 ,但 ViewModel 始終存活

如何使用 ViewModel ?

我用一個(gè)非常簡單的 Demo 來展示它的基礎(chǔ)用法,通常我們?yōu)?app 集成 ViewModel 遵循如下幾個(gè)步驟:

1、創(chuàng)建一個(gè)繼承 ViewModel 的類來分離出 UI 控制器中的數(shù)據(jù)

2、建立 ViewModel 和 UI 控制器之間的通信

3、在 UI 控制器中使用 ViewModel

1、創(chuàng)建 ViewModel

創(chuàng)建 MainActivityViewModel 并繼承 ViewModel

class MainActivityViewModel : ViewModel(){}

以上面的計(jì)時(shí)器為例,我們需要 UI 保持持續(xù)更新時(shí)間的狀態(tài),所以在 ViewModel 添加一個(gè) startTime 變量用于存儲不斷累計(jì)的時(shí)間

class MainActivityViewModel : ViewModel(){
    private val _startTime = null
    var startTime:Long? = _startTime
}

2、關(guān)聯(lián) UI 控制器和 ViewModel

UI 控制器必須知道自己和哪個(gè) ViewModel 進(jìn)行關(guān)聯(lián),這樣它才能知道去哪里取回?cái)?shù)據(jù),注意,不要在 ViewModel 中持有任何 Activity、Fragment 或 View 的引用,因?yàn)榇蟛糠智闆r ViewModel 的生命周期比它們都長,持有一個(gè)已經(jīng)銷毀對象的引用意味著內(nèi)存泄露,對于必須使用 Context 的 ViewModel 可以繼承 AndroidViewModel 類,AndroidViewModel 中包含 Application 的引用

class MainActivity : AppCompatActivity() {
    private val viewModel by lazy {
        ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
       
        cm.start()
    }
}

3、在 UI 控制器中使用 ViewModel

我們在計(jì)時(shí)開始之前先將系統(tǒng)當(dāng)前時(shí)間存入 viewModel.startTime 變量,而后每次 onCreate 被調(diào)用時(shí),都會先取出 viewModel.startTime 賦予 Chronometer.base ,然后再啟動計(jì)時(shí)器,因?yàn)?ViewModel 不受 Activity 生命周期影響,所以它會一直持有 startTime ,這樣即使 Activity 被重建,計(jì)時(shí)器也能基于正確的時(shí)間啟動計(jì)時(shí)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
  
    if (viewModel.startTime == null) {
        val startTime = SystemClock.elapsedRealtime()
        viewModel.startTime = startTime
        cm.base = startTime
    } else {
        cm.base = viewModel.startTime!!
    }
  
    cm.start()
}

再次運(yùn)行,你會看到時(shí)間重置的問題得到解決

ViewModel 結(jié)合 LiveData

ViewModel 如果不結(jié)合 LiveData 來用的話就失去了它的靈魂,正如人與人之間的默契配合才能發(fā)揮出整個(gè)團(tuán)隊(duì)的潛能,架構(gòu)組件本著開放靈活的原則,允許你單獨(dú)集成使用它們其中的任何一個(gè),但我強(qiáng)烈推薦你綜合使用整套架構(gòu)組件,除非你的項(xiàng)目有嚴(yán)格限制或其它特殊情況

前面的 Demo 為了快速理解 ViewModel 的用法所以寫的非常簡單,接下來我們將使用 Timer + LiveData 來替代 Chronometer 控件實(shí)現(xiàn)一個(gè)計(jì)時(shí)器

1、新建 CustomTimer 布局、Activity、ViewModel

custom_timer.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <TextView android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:textColor="@color/colorPrimary"
              android:textSize="24sp"
              android:id="@+id/tv_timer" app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

CustomTimerViewModel

class CustomTimerViewModel : ViewModel() {
    private var startTime: Long? = null
    private val _elapsedTime = MutableLiveData<Long>()
    var elapsedTime: LiveData<Long> = _elapsedTime
}

CustomTimerActivity

class CustomTimerActivity : AppCompatActivity() {
    private val viewModel by lazy {
        ViewModelProviders.of(this).get(CustomTimerViewModel::class.java)
    }
    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.custom_timer)
        val tvTimer = findViewById<TextView>(R.id.tv_timer)
    }
}

2、在 ViewModel 中初始化 Timer

我們直接在初始化模塊啟動 Timer,讓它每秒執(zhí)行一次 timerTask 并在 timerTask 內(nèi)部更新 elapsedTime 的值為當(dāng)前時(shí)間距離 startTime 的秒數(shù),此處 elapsedTime 為 LiveData 類型,它會隨著 ViewModel 初始化開始通過 Timer 自動更新,下一步我們只需要在 Activity 中訂閱它即可實(shí)時(shí)更新數(shù)據(jù)到 UI

class CustomTimerViewModel : ViewModel() {
    private var startTime: Long? = null
    private val _elapsedTime = MutableLiveData<Long>()
    var elapsedTime: LiveData<Long> = _elapsedTime
    init {
        startTime = SystemClock.elapsedRealtime()
        Timer().scheduleAtFixedRate(timerTask {
            val newValue = (SystemClock.elapsedRealtime() - startTime!!) / 1000
            _elapsedTime.postValue(newValue)
        }, ONE_SECOND, ONE_SECOND)
    }
    companion object {
        const val ONE_SECOND = 1000L
    }
}

3、在 Activity 中訂閱 elapsedTime

如下代碼,我們使用 viewModel.elapsedTime.observe(owner,Observer) 將 elapsedTime 訂閱到 owner

class CustomTimerActivity : AppCompatActivity() {
    private val viewModel by lazy {
        ViewModelProviders.of(this).get(CustomTimerViewModel::class.java)
    }
    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.custom_timer)
        val tvTimer = findViewById<TextView>(R.id.tv_timer)

        viewModel.elapsedTime.observe(this, Observer {
            tvTimer.text = "$it seconds elapsed"
        })
    }
}

這樣 elapsedTime 在變更時(shí)就會立即通知 owner 并回調(diào) Observer 接口,我們只要在 onChanged 回調(diào)中將數(shù)據(jù)綁定到 TextView 即可,這就是數(shù)據(jù)驅(qū)動 ?UI

Observer 接口

/**
 * A simple callback that can receive from {@link LiveData}.
 *
 * @param <T> The type of the parameter
 *
 * @see LiveData LiveData - for a usage description.
 */
public interface Observer<T> {
    /**
     * Called when the data is changed.
     * @param t  The new data
     */
    void onChanged(T t);
}

運(yùn)行 app,計(jì)時(shí)器正常工作并且不會因?yàn)檗D(zhuǎn)屏等操作重置

完整示例代碼

https://github.com/realskyrin/jetpack_viewmodel

參考

https://codelabs.developers.google.com/codelabs/android-lifecycles

https://medium.com/androiddevelopers/viewmodels-a-simple-example-ed5ac416317e

https://developer.android.com/topic/libraries/architecture/viewmodel

Jetpack 系列文章

Android Jetpack - Lifecycles

Android Jetpack - ViewModel

Android Jetpack - DataBinding

Android Jetpack - Room

Android Jetpack - LiveData

Android Jetpack - WorkManager(待更)

Android Jetpack - Paging(待更)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容