DEV Community

Cover image for Exploring Android LiveData Usages and Behaviors
Vincent Tsen
Vincent Tsen

Posted on • Edited on • Originally published at vtsen.hashnode.dev

Exploring Android LiveData Usages and Behaviors

Simple app to demonstrate Android lifecycle-aware LiveData usages and behaviors - setValue(), postValue(), observe(), and observeAsState().

This is part of the asynchronous flow series:

Android LiveData is observable and lifecycle-aware data.

Assuming the lifecycle owner is an activity, lifecycle-aware means it will only send updates to the UI when the activity is active. Activity is active means the UI is visible either in the background (started state) or in the foreground (resumed state).

LiveData also automatically removes all Observer objects when the activity is destroyed.

Basic LiveData Usages

Emit LiveData - setValue() / postValue()

First, you need to

Create MutableLiveData

class LiveDataViewModel: ViewModel() {

    private val _liveData = MutableLiveData<Int>()
    val liveData: LiveData<Int> = _liveData
   /*...*/
}

Enter fullscreen mode Exit fullscreen mode

To emit value in LiveData, you can either use

MutableLiveData.setValue() / MutableLiveData.value

viewModelScope.launch {
    repeat(10000) { value ->
        delay(1000)
        _liveData.value = value
    }
}
Enter fullscreen mode Exit fullscreen mode

or

MutableLiveData.postValue()

viewModelScope.launch(Dispatchers.IO) {
    repeat(10000) { value ->
        delay(1000) 
        _liveData.postValue(value)
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Coroutine is used to simulate the asynchronous flow because we don't want to block the UI / main thread.
  • setValue() must be run on main thread, postValue() can be on either main or background thread

Observe LiveData - observe() / observeAsState()

To observe the LiveData, you can either manually observe usingLiveData.observe() or LiveData.observeAsState() API.

LiveData.observe()

  1. Create a MutableState data
  2. Create an Observer object
  3. Observe the LiveData (pass in the Observer object)
  4. Remove the Observer object from LiveData
@Composable
fun LiveDataScreen() {
    val viewModel: LiveDataViewModel = viewModel()
    val lifecycleOwner = LocalLifecycleOwner.current

    //(1) Create a MutableState data
    val manualObserveLiveDataState:MutableState<Int?> = 
        remember { mutableStateOf(null) }

    //(2) Create an observer object
    val liveDataObserver = remember {
        Observer<Int> { value ->
            manualObserveLiveDataState.value = value
        }
    }

    Column {

        Button(onClick = {
            //(3) Observe the LiveData
            viewModel.liveData.observe(lifecycleOwner, liveDataObserver)
        }) {
            Text(text = "Manually Start Observe LiveData")
        }

        Button(onClick = {
            // (4) Remove the observer from LiveData
            viewModel.liveData.removeObserver(liveDataObserver)
        }) {
            Text(text = "Manually Remove Observer")
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

In step 2 above, remember {} is required for creating theObserver object so we don't recreate the Observer object every time during recomposition. I made this mistake. Thus, it causes the memory leak - observers growth.

remember {} is like caching. If you don't know what it is, the article below gives you some examples.

LiveData.observeAsState()

LiveData.observeAsState() returns MutableState<Int?> object, so you don't need to explicitly create it.

@Composable
fun LiveDataScreen() {
    val viewModel: LiveDataViewModel = viewModel()

    // Create MutableState by observing the LiveData
    val observeAsStateLiveData = 
        viewModel.liveData.observeAsState(initial = null)

}
Enter fullscreen mode Exit fullscreen mode

Internally, it calls the DisposableEffect() side-effect. The most important of this effect is the onDispose() function which takes care of removing the Observer object for you automatically when the effect leaves the composition.

@Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
    val lifecycleOwner = LocalLifecycleOwner.current
    val state = remember { mutableStateOf(initial) }

    DisposableEffect(this, lifecycleOwner) {
        val observer = Observer<T> { state.value = it }

        observe(lifecycleOwner, observer)
        onDispose { removeObserver(observer) }
    }
    return state
}
Enter fullscreen mode Exit fullscreen mode

Investigate setValue() vs postValue() Behaviors

To study the behavior setValue() vs postValue(), these are a few scenarios to try:

  • Activity is created/stopped (not visible in background)
  • Activity is started/paused (visible in background)
  • Activity is resumed (visible in foreground)
  • When UI is busy
  • Run postValue() in main thread

Simulate UI is Busy

To simulate UI is busy, you can either emit the value fast (e.g. every 1 ms)

job = viewModelScope.launch {
    repeat(10000) { value ->
         delay(1)
        _liveData.postValue = value
    }
}
Enter fullscreen mode Exit fullscreen mode

or simply call Thread.sleep() - I prefer this method.

Button(onClick = {
    Thread.sleep(3000)
}) {
    Text(text = "Simulate Busy UI")
}
Enter fullscreen mode Exit fullscreen mode

Simulate Activity is Paused (Visible in Background)

To simulate an activity that is paused/loses focus, you can start another transparent activity on top of your current app.

Add Logging in Observer Object

In order to prove the data is sent to the UI, you add the following logging to the Observer object.

val liveDataObserver = remember {
    Observer<Int> { value ->
        Log.d(tag, "[ManualObserver]: Assigning $value to manualObserveLiveDataState.value")
        manualObserveLiveDataState.value = value
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary - setValue() vs postValue()

After performing various scenarios, these are the differences between setValue() and postValue().

Scenarios setValue() postValue()
Can run in main thread? Yes Yes
Can run in background thread? No Yes
Activity is created/stopped (not visible in background) Data is NOT sent to UI Same as setValue()
Activity is started/paused (visible in background) Data is sent to UI Same as setValue()
Activity is resumed (visible in foreground) Data is sent to UI Same as setValue()
When UI is busy Data is queued and is NOT dropped Data is dropped
Run postValue() in main thread N/A No difference, same as postValue() running in background thread - data is still dropped when UI is busy.

This testing above is done by calling the observe() (which doesn't remove the observer automatically). This way we can observe the actual behavior of LiveData respecting the activity lifecycle.

The most important difference is when UI is busy, postValue() drops the data and setValue() doesn't.

Investigate observe() vs observeAsState() Behaviors

If you use observe(), you need to manually call the removeObserver() API. If you use observeAsState()(which uses DisposableEffect internally), it automatically calls the removeObserver() API when the DisposableEffect leaves the composition.

These are the scenarios to try:

  • Activity is created/stopped (not visible in background)
  • Activity is started/paused (visible in background)
  • Activity is resumed (visible in foreground)
  • Activity is destroyed (screen rotation)
  • Leaving composition

Simulate Leaving Composition

To simulate leaving composition, you can implement CommonScreen() composable function below, which consists of buttons to hide and show the actual composable content. When the content is hidden, it simulates the leaving composition.

@Composable
fun CommonScreen(content: @Composable () -> Unit) {
    var showContent by rememberSaveable { mutableStateOf(true) }
    val context = LocalContext.current

    Column(modifier = Modifier.verticalScroll(rememberScrollState())){
        if (showContent) {
            content()

            Button(onClick = {
                showContent = false
            }) {
                Text("Hide Content (Simulate Leaving Composition)")
            }
        }
        else {
            Button(onClick = {
                showContent = true
            }) {
                Text("Show Content")
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Add Logging in observeAsState()

Let's duplicate LiveData<T>.observeAsState to LiveData<T>.observeAsStateWithLogging() extension function with logging information to indicate whether the data is sent to UI.

@Composable
fun <R, T: R> LiveData<T>.observeAsStateWithLogging(initial: R): State<R> {
    /*...*/
    DisposableEffect(this, lifecycleOwner) {
        val observer = Observer<T> {
            Log.d(tag, "[ObserveAsState]: Assigning $it to state.value")
            state.value = it
        }
        /*...*/
    }
    return state
}
Enter fullscreen mode Exit fullscreen mode

Summary - observe() vs observeAsState()

After playing around with different scenarios, here is the summary:

Scenarios observe() observerAsState()
Activity is created/stopped (not visible in background) Observer retains - data is NOT sent to UI (due to LiveData is lifecycle-aware component) Same as observe()
Activity is started/paused (visible in background) Observer retains - data is sent to UI Same as observe()
Activity is resumed (visible in foreground) Observer retains - data is sent to UI Same as observe()
Activity is destroyed Observer is removed - data is NOT sent to UI Same as observe()
Leaving composition Observer retains - data is sent to UI (due to activity is still active/visible) Observer is removed - Data is sent to UI
  • For observe(), since we haven’t put any special code to remove the Observer object, the Observer always retains during leaving composition. It only gets removed when the activity is destroyed.

  • Why we want to remove the Observer when activity is not visible? Because it saves resources by NOT running any execution that doesn't have any impact on UI.

The most important difference of observeAsState() is it removes the Observer when the effects (where the observeAsState() is called) leaves the composition.

LiveData Lifecycle

The LiveData lifecycle is tied to the ViewModel. Since ViewModel is tied to the activity lifecycle (in this example), ViewModel is destroyed when activity is finished / application is exited. The LiveData is destroyed only when ViewModel is destroyed. So as long as the application is alive, the setValue() or postValue() keeps emitting the data even the activity is not active (e.g. activity is not visible in background).

This is the logging that keeps printing in background in this app example:

[ViewModel]: setValue with 1167
Enter fullscreen mode Exit fullscreen mode

Depending on whether navigation component is used, the ViewModel lifecycle doesn't always tie to the activity lifecycle. To understand the details of ViewModel lifecycle, see the following article:

Conclusion

The good thing about LiveData is activity lifecycle-aware, it doesn't waste any resources when the UI is not visible (created/ stopped). LiveData also automatically removes all the observers when activity is destroyed.

However, it doesn't work with leaving composition (because the activity is still active/visible). To save resources, you use observeAsState() (for Jetpack Compose of course) which automatically removes the Observer explicitly when it is no longer in the composition loop. Thus, no update is sent to the UI.

What about using setValue() or postValue()? I prefer to use setValue() because it doesn't drop any data when UI is busy. I can put the data fetching into the background thread and update the LiveData in main thread, so no data is dropped and still remains asynchronous.

Given how fast Android development has evolved, LiveData is probably going to be obsolete eventually and replaced with StateFlow - will be covered in the next topic.

Source Code

GitHub Repository: Demo_AsyncFlow (see the LiveDataActivity)


Originally published at https://vtsen.hashnode.dev.

Top comments (0)