Introduction
Recently, I’ve been working on a codebase where I had to write a bridging code between a data layer using Flows
and a UI layer that still relied on the state exposed as LiveData
.
Luckily, there is a function in the androidx.lifecycle
called asLiveData()
that allows you to convert a Flow
to a LiveData
effortlessly. However, there is one caveat to keep in mind when using a LiveData
created this way. It will only emit data when it has at least one active observer. If there is an update to the upstream flow and the LiveData
is inactive, it will not have the latest value.
Let me show you a potential problem we might encounter, with an example below:
Example
We have a simple activity that keeps a reference to an AAC ViewModel
:
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
The MainViewModel
looks like this:
class MainViewModel : ViewModel() {
private val repository = Repository()
val state: LiveData<Int> = repository.state.asLiveData()
}
We have a reference to a repository that will act as our trivial data layer. The ViewModel
also exposes a state as a LiveData
object converted from a StateFlow
kept inside the repository using the asLiveData()
function mentioned before.
Here is what the repository looks like:
class Repository {
private val _state = MutableStateFlow(-1)
val state: StateFlow<Int> = _state
suspend fun update() {
_state.emit(Random.nextInt(until = 1000))
}
}
It’s a simple class with a StateFlow
wrapping an integer (that starts with an initial value of -1) that also has a method allowing clients to update the state with a new random number between 0 and 1000.
Let’s imagine we want to schedule the update as soon as our activity is created. We could do this by creating a method inside the MainViewModel
called init()
that we would call inside the onCreate()
of our activity:
// MainViewModel
fun init() {
// update() is suspending, so we launch a new coroutine here
viewModelScope.launch {
repository.update()
}
}
// MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.init()
}
Now, during the creation of our activity, a new coroutine will be launched that will eventually call update()
on the repository, generating a random number and emitting it to the state.
Additionally, let’s suppose that there is a requirement to send an analytical event containing the newly generated number in our ViewModel
. We could write a sendAnalyticalEvent()
method in our ViewModel
that we will run right after calling the update()
method on the repository:
// MainViewModel
fun init() {
viewModelScope.launch {
repository.update()
sendAnalyticalEvent() // <-- NEW
}
}
private fun sendAnalyticalEvent() {
// Typically, we would schedule a network request here
val liveDataValue = state.value
val flowValue = repository.state.value
Log.d("Current number in LiveData", "$liveDataValue")
Log.d("Current number in StateFlow", "$flowValue")
}
Inside this method, we would typically schedule a network request to our backend servers but in this case, let’s see both values from the LiveData
and Flow
in the Logcat instead:
Now, that is quite unexpected. You could argue that the LiveData
doesn’t have the newest value because there wasn’t enough time to collect it from the upstream flow, and you might be right. But in this case, not only does the LiveData
contain an incorrect value, but it’s null! And remember, the initial value of the state in the repository was -1. It can only mean one thing - our LiveData
didn’t collect anything from the StateFlow
.
The reason is that we haven’t started observing the LiveData
anywhere. Therefore, it’s considered inactive. And, according to the documentation of the asLiveData()
function, in this state, LiveData
won’t be collecting any values from the upstream flow:
Creates a
LiveData
that has values collected from the originFlow
.The upstream flow collection starts when the returned
LiveData
becomes active (LiveData.onActive
). If theLiveData
becomes inactive (LiveData.onInactive
) while the flow has not completed, the flow collection will be cancelled aftertimeoutInMs
milliseconds unless theLiveData
becomes active again before that timeout (to gracefully handle cases like Activity rotation).
Once we start observing the state in our activity (hence making the LiveData
active), it will contain the correct (newest) value:
// MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.init()
viewModel.state.observe(this) { // <-- NEW
Log.d("Current number in MainActivity", "$it")
}
}
And this is the output from the Logcat:
In this example, we use StateFlow
, but the same rules apply to SharedFlow
. Furthermore, it would be even worse because any events sent to a SharedFlow
while the LiveData
is inactive would be permanently lost (SharedFlow
, by default, doesn’t replay any values to new subscribers).
Summary
Keep in mind that LiveData
converted from a Flow
using the asLiveData()
function will behave slightly differently than expected. It will emit data only when there are active observers. To me, this behavior makes sense because we generally wouldn’t be interested in LiveData
values if we didn’t observe it anywhere.
Regardless, there might be some use cases when you want to access the current value of your LiveData
in your ViewModel
before you start observing it. After reading this post, I hope you won’t be surprised when encountering seemingly incorrect values in such situations.