Day163 — ViewModels, LiveData and Lifecycles
Here records the notes I took when watching “droidcon NYC 2017 — ViewModels, LiveData and Lifecycles, oh my!”. The talk is fairly old. Some of the mentioned methods are outdated and the code here is translated into Kotlin.
Problems to be solved:
- interruptions at any time
- rotation
- asynchronous tasks + destroyed UI = Memory Leaks
- OS killing apps
Reactive UI
ViewModels are objects that provide data for UI components and survive configuration changes.
LiveData is a data holder class that is lifecycle aware. It keeps a value and allows this value to be observed.
class MainActivityViewModel : ViewModel() {
private val _scoreTeamA = MutableLiveData<Int>()
init {
_scoreTeamA.setValue(0)
}
fun addToTeamA(amount: Int) {
_scoreTeamA.setValue(_scoreTeamA.value?.plus(amount))
}
fun getScoreTeamA(): LiveData<Int> {
return _scoreTeamA
}
}
class MainActivity : ComponentActivity() {
private var mViewModel: MainActivityViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
mViewModel?.getScoreTeamA()?.observe(this) { newScore ->
displayForTeamA(newScore)
}
}
private fun displayForTeamA(score: Int) {
// display score
}
}
- Reactive because of data observation
- UI data survives configuration changes
- Clearer class responsibility
ViewModel Lifecycle
Be note that if the process is killed due to memory restrictions, the ViewModel is destroyed.
Create a factory to pass values for instantiating ViewModel. It is by design to be hard to pass value to ViewModel.
ViewModels should not store Context / Activity / Fragment.
Lifecycle
Lifecycle — An object that defines an Android Lifecycle.
LifecycleOwner — LifecycleOwner is an interface: Objects with Lifecycles e.g. Activities and Fragments
LifecycleObserver — Observes LifecycleOwner
- Can check state
- automatically clean-up
class MyLocationListener : LifecycleObserver {
var lifecycle: Lifecycle
constructor(context: Context, lifecycle: Lifecycle, callback: Callback) {
this.lifecycle = lifecycle
this.lifecycle.addObserver(this)
}
var enabled = false
fun enable() {
enabled = true
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
// connect if not connected
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
enabled = false
// disconnect if connected
}
}
Observer
mViewModel?.getScoreTeamA()?.observe(this) { newScore ->
displayForTeamA(newScore)
}
Observer only forwards events if Lifecycle is STARTED or RESUMED.
Observer cleans itself up when activity is finished. If Observer connects or becomes active again, intelligently sends the most recent data.
Transformation
Transformation.map() — applies function to each value emitted by source LiveData and returns a LiveData that emits the resulting values.
class MainActivityViewModel(startingScore: Int) : ViewModel() {
private val _scoreTeamA: MutableLiveData<Int> = MutableLiveData<Int>()
val scoreStringTeamA: LiveData<String>
init {
scoreStringTeamA = Transformations.map(_scoreTeamA) { it.toString() }
_scoreTeamA.setValue(0)
}
}
MutableLiveData exposes setValue() (main thread)
and postValue() (background thread)
LiveData is “ready only”
Therefore, we should only expose LiveData to outside, while MutableLiveData is used inside the ViewModel.
LiveData is synchronous, which ensures destroyed observer will not get updates. It is not a stream so it does not have a history, only the current state. (Don’t use it for chat history stream)
Summary
- All Activity does is update UI
- ViewModel holds and processes data for UI. 1. use map to help 2. proper encapsulation
- LiveData makes data observable, also lifecycle-aware.
Outdated codes
replace
private var mViewModel: MainActivityViewModel? = null
mViewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
with
private val mViewModel: MainActivityViewModel by viewModels()
replace
class MainActivityViewModel : ViewModel() {
private val _scoreTeamA = MutableLiveData<Int>()
init {
_scoreTeamA.setValue(0)
}
fun addToTeamA(amount: Int) {
_scoreTeamA.setValue(_scoreTeamA.value?.plus(amount))
}
fun getScoreTeamA(): LiveData<Int> {
return _scoreTeamA
}
}
with
class MainActivityViewModel : ViewModel() {
private val _scoreTeamA = MutableLiveData<Int>()
val scoreTeamA: LiveData<Int>
get() = _scoreTeamA
init {
_scoreTeamA.setValue(0)
}
fun addToTeamA(amount: Int) {
_scoreTeamA.setValue(_scoreTeamA.value?.plus(amount))
}
}
@OnLifecycleEvent
is deprecated
class MyLocationListener : LifecycleObserver {
var lifecycle: Lifecycle
constructor(context: Context, lifecycle: Lifecycle, callback: Callback) {
this.lifecycle = lifecycle
this.lifecycle.addObserver(this)
}
var enabled = false
fun enable() {
enabled = true
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
// connect if not connected
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
enabled = false
// disconnect if connected
}
}
replace with DefaultLifecycleObserver
class MyLocationListener2 : DefaultLifecycleObserver {
var lifecycle: Lifecycle
constructor(context: Context, lifecycle: Lifecycle, callback: Callback) {
this.lifecycle = lifecycle
this.lifecycle.addObserver(this)
}
var enabled = false
fun enable() {
enabled = true
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
// connect if not connected
}
}
override fun onStop(owner: LifecycleOwner) {
enabled = false
// disconnect if connected
}
}
OR LifecycleEventObserver
private val lifecycleEventObserver = LifecycleEventObserver { source, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> onAppInForeground()
Lifecycle.Event.ON_PAUSE -> onAppInBackground()
Lifecycle.Event.ON_DESTROY-> onAppDestroyed()
Lifecycle.Event.ON_CREATE -> {}
Lifecycle.Event.ON_START -> {}
Lifecycle.Event.ON_STOP -> {}
Lifecycle.Event.ON_ANY -> {}
}
}
Common error
Don’t do this
class MainViewModel : ViewModel() {
fun search(searchTerm: String): LiveData<List<Song>> {
return songRepository.search(searchTerm)
}
}
A new LiveData is created when another keyword “Shake” is searched, which is redundant.
The solution is to use Transformations.switchMap()
. It switches the observed LiveData based off of another LiveData’s value
class MainViewModel : ViewModel() {
val mSongs: LiveData<List<Song>>
private val mSearchTerm: MutableLiveData<String> = MutableLiveData()
init {
mSongs = Transformations.switchMap(mSearchTerm) { newTerm ->
songRepository.search(newTerm)
}
}
fun search(searchTerm: String) {
if (searchTerm.isNotBlank()) mSearchTerm.setValue(searchTerm)
}
}
map()
and siwtchMap()
run on the main thread.
Don’t use map()
to perform a database lookup or network sync.
You can create LiveData for switchMap on the main thread and then post updates to them on a background thread.
Memory related process death
use onSaveInstanceState()
to recover. “Plan B”.
The smallest amount of data to restore state.
Bundle too big = TransactionTooLargeException
we have SavedStateHandle
now.
Activity death
- finish()
- Swipe off recents
- Navigate back
- Configuration change
- Process death in case of low memory, if app is in background
Come back to a new state
- finish()
- Swipe off recents
- Navigate back
Come back to the same state
- Configuration change
- Process death in case of low memory, if app is in background
In the comment section,
LiveData should be replaced with MutableStateFlow from Kotlin coroutines. To not update the UI when the lifecycle is not started you do, flowWithLifecycle().
I do not verify above statement.