之後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>
接著我們在模擬器上運行它
這是一個計數器的頁面
接著我們點選旋轉
會發現資料被重置了
原因是因為旋轉時生命週期變動 導致資料被重新創建的關係
詳細生命週期變化可參閱下圖
那以往為了保留資料 其中一種方式是從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