DEV Community

Cover image for 3 reasons why I'm not migrating LiveData to StateFlow
Aldo Wachyudi
Aldo Wachyudi

Posted on

3 reasons why I'm not migrating LiveData to StateFlow

I have a confession to make, after reading through StateFlow and SharedFlow documentation, watching Android Developers's video, and reviewing nowinandroid source codes, I'm not convinced to migrate LiveData to StateFlow or SharedFlow. At least for now. Here's why:

1. Most of the time, I only do one-shot operation

According to Kotlin documentation flow is "An asynchronous data stream that sequentially emits values and completes normally or with an exception."

There are valid use-cases for using flow, like fetching the latest data from server periodically or receiving live-update of query result when using Room.

But, most of the time, I only need to do a one-shot task. Not more, not less. Such as, making a GET request to a server and retrieving a value from a database. In those situations, the nature of my task is not stream. Even though I can convert it to flow:

flow {
    val weather = weatherApi.getWeather(coordinate)
    emit(weather)
}
Enter fullscreen mode Exit fullscreen mode

This works, yet in my opinion, I added an extra line of codes for no specific reason.

We can also create a StateFlow or SharedFlow without flow.

private val _myState = MutableStateFlow(
    MyState(
        title = R.string.title,
        index = 0,
    )
)
val myState: StateFlow<MyState> = _myState.asStateFlow()
Enter fullscreen mode Exit fullscreen mode

But again, if it's not emitting multiple values sequentially, why should I add more code?

This brings us to my second reason.

2. LiveData is lifecycle-aware

StateFlow and SharedFlow are part of Kotlin library. It's designed to be platform agnostic (go KMM! ๐Ÿš€), so it doesn't know about Android framework and its lifecycle. If you want to integrate it Android, you need to ensure it follows Android lifecycle.

// Activity

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { uiState ->
            when (uiState) {
                is UiState.Success -> showSuccess(uiState.data)
                is UiState.Error -> showError(uiState.exception)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If only there is a Jetpack library for that.. ๐Ÿค”

That's what LiveData is made for! Taken from Android documentation, "LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services.".

The same functionality can be achieved with this code.

// Activity

viewModel.uiState.observe(this) { uiState ->
    is UiState.Success -> showSuccess(uiState.data)
    is UiState.Error -> showError(uiState.exception)
}
Enter fullscreen mode Exit fullscreen mode

By using LiveData, we don't need to manually handle the lifecycle, worry about memory leaks, and be sure that the data is always up-to-date.

One caveat, I realize in Compose the difference isn't significant (Kudos to Jetpack team ๐Ÿ‘).

@Composable
fun HomeRoute(
    modifier: Modifier = Modifier,
    viewModel: MyViewModel = getViewModel<MyViewModel>()
) {
    val uiStateLiveData by viewModel.uiState.observeAsState(initial = UiState.Loading)
    val uiStateStateFlow by viewModel.uiState.collectAsStateWithLifecycle()
}
Enter fullscreen mode Exit fullscreen mode

The stable version of Jetpack Compose is just released last year. While I do like writing UI using Compose, I think most companies still need more time to adopt Compose.

3. I don't need StateFlow or SharedFlow features

StateFlow and SharedFlow have many features for configuring the emitted elements. SharedFlow has a reply functionality that can resend previously-emitted values for new subscribers. StateFlow has a time-out mechanism before stopping upstream flow using WhileSubscribed class.

val uiState: StateFlow<UiState> = flow {
    emit(repository.getItem())
}.stateIn(
    scope = viewModelScope, 
    initialValue = UiState.Loading,
    started = WhileSubscribed(5_000),
)
Enter fullscreen mode Exit fullscreen mode

flow also allows you to combine multiple flows into a StateFlow.

val uiState: StateFlow<UiState> = combine(
    userRepository.getUser(),
    weatherRepository.getWeather(),
    statusRepository.getStatusStream(),
) { user, weather, status stream -> {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Whereas, in my projects, the cool features that SharedFlow and StateFlow provides is a nice-to-have, not a must-have. The complex operation is handled on different classes, like on Repository (data layer) or UseCases (domain layer).

For merging multiple data-sources, like network and database, I can use Repository backed with NetworkDataSources and DbDataSources. For data manipulation, Kotlin's collection has many useful functions (map, reduce, groupBy, count, etc.). If I want to ensure a data can only be emitted once, a simple modification to LiveData is enough (SingleLiveData

Summary

So, when should you use StateFlow and SharedFlow? If you need them! Use the right tool for the right job.

If you need to store or buffer flow elements, if you need to support KMM in your project, or if you need first-class support for stream operation, then please use StateFlow and SharedFlow ๐Ÿ™.

If you think LiveData is enough for you, don't migrate to StateFlow just yet ๐Ÿ˜

References

Image taken by Samuel from Unsplash: https://unsplash.com/photos/scUBcasSvbE

Top comments (1)

Collapse
 
aldok profile image
Aldo Wachyudi

Seems like Mitch has the same opinion. Watch his video "Persisting data in ViewModels with MutableState (Jetpack Compose ViewModel Example)"