iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 4
0

之後data binding時會使用到ViewModel與LiveData
所以今天就來介紹這兩個的用法

今天的專案solution會貼在本文最下方

首先先新增一個ChronoActivity

ChronoActivity.kt

import kotlinx.android.synthetic.main.activity_chrono.*

class ChronoActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_chrono)
        chronometer.start()
    }
}

activity_chrono.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".main.ChronoActivity">
    <Chronometer
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:id="@+id/chronometer"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

然後去manifest.xml將預設開啟的activity改為ChronoActivity

AndroidManifest.xml

    <application
           ...
        <activity android:name=".main.ChronoActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

接著我們在模擬器上運行它
https://ithelp.ithome.com.tw/upload/images/20190919/20120279WdhywMSeH0.png
這是一個計數器的頁面

接著我們點選旋轉
https://ithelp.ithome.com.tw/upload/images/20190919/20120279GGWgJoAgSc.png
會發現資料被重置了

原因是因為旋轉時生命週期變動 導致資料被重新創建的關係
詳細生命週期變化可參閱下圖
https://ithelp.ithome.com.tw/upload/images/20190919/20120279JcYOdedhKN.png

那以往為了保留資料 其中一種方式是從savedInstanceState去讀取
或是存在db之類的
那現在有個更簡單的方法 就是使用ViewModel
數據統一由ViewModel來管理
也能避免memoryleak的情況發生

馬上來新增一個ViewModel來試試看
ChronometerViewModel.kt

class ChronometerViewModel : ViewModel() {
    var startTime: Long = SystemClock.elapsedRealtime()
}

activity也跟著調整

ChronoActivity.kt

import kotlinx.android.synthetic.main.activity_chrono.*

class ChronoActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
       ...
        val chronometerViewModel = ViewModelProviders.of(this).
        get(ChronometerViewModel::class.java)
        chronometer.base = chronometerViewModel.startTime
        chronometer.start()
    }
}

這次點選旋轉時會發現計時器就沒有跟著一起被重置了

接著來試試看LiveData

這邊我們先將chronometer移除 改用timer與text顯示資訊
activity_chrome.xml

...
    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:id="@+id/text_time"/>

更新ViewModel

class ChronometerViewModel : ViewModel() {
    companion object {
        private val ONE_SECOND = 1000
    }
    
    // MutableLiveData是用來通知數據更新的類別
    //如果在background 使用postValue  在main thread 使用setValue
    private val mElapsedTime = MutableLiveData<Long>()
    private val mInitialTime: Long
    val elapsedTime: LiveData<Long>
        get() = mElapsedTime

    init {
        mInitialTime = SystemClock.elapsedRealtime()
        val timer = Timer()

        // 更新經過的時間
        timer.scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                val newValue = (SystemClock.elapsedRealtime() - mInitialTime) / 1000
                //通知資料更新 newValue的值 等同下面的aLong
                mElapsedTime.postValue(newValue)
            }
        }, ONE_SECOND.toLong(), ONE_SECOND.toLong())

    }
}

更新Activity
ChronoActivity.kt

class ChronoActivity : AppCompatActivity() {
    private lateinit var chronometerViewModel: ChronometerViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_chrono)
        chronometerViewModel = ViewModelProviders.of(this).get(ChronometerViewModel::class.java)
        subscribe()
    }
    
    //訂閱
    private fun subscribe() {
        //數據變更時的行為
        val elapsedTimeObserver = Observer<Long> { aLong ->
            val newText = aLong.toString() + " seconds elapsed"
            (findViewById<View>(R.id.text_time) as TextView).setText(newText)
            Log.d("ChronoActivity", "Updating timer")
        }
        chronometerViewModel.elapsedTime.observe(this, elapsedTimeObserver)
    }
}

如果有用過rxjava的話 應該會發現兩者很相似
主要就差在LiveData僅在activity生命週期處在STARTED or RESUMED時
才會通知訂閱對象數據更新
明天預計會接著講如何用在databinding

代碼
https://github.com/mars1120/jetpackMvvmDemo/tree/ViewModelAndLivedata

參考資料
https://codelabs.developers.google.com/codelabs/android-lifecycles/#0


上一篇
Day3 coroutines
下一篇
Day5 dataBinding - 1
系列文
Android × CI/CD 如何用基本的MVVM專案實現 CI/CD 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言