DEV Community

Cover image for Convert Flow to SharedFlow and StateFlow
Vincent Tsen
Vincent Tsen

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

Convert Flow to SharedFlow and StateFlow

Explore different ways of converting Flow to SharedFlow and StateFlow using SharedFlow.emit(), StateFlow.value, Flow.ShareIn() and Flow.StateIn()

This is part of the asynchronous flow series:

Flow is a cold stream. It emits value only when someone collects or subscribes to it. So it does NOT hold any data or state.

SharedFlow is a hot stream. It can emit value even if no one collects or subscribes to it. It does NOT hold any data too.

StateFlow is also a hot steam. It does NOT emit value, but it holds the value/data.

Flow Type Cold or Hot Stream Data Holder
Flow Cold No
SharedFlow Hot (by default) No
StateFlow Hot (by default) Yes

The reason why SharedFlow and StateFlow are hot streams by default, is they can also be cold streams depending on how you create them. See shareIn and stateIn sections below.

What is data holder?

Data holder (can also be called as state holder) means it holds data. It retains and stores the last data of the stream, The data is also observable which allows subscribers to subscribe to it.

There are 3 types of data holders in Android development

  • LiveData

  • StateFlow

  • State (Jetpack Compose)

3 of them are pretty similar, but they have differences. See the table below.

Data Holder Type Android or Kotlin Library? Lifecycle Aware? Required Initial Value?
LiveData Android Yes No
StateFlow Kotlin No Yes
State (Compose) Android No Yes

StateFlow is Platform Independent

LiveData is Android-specific and eventually will be replaced by StateFlow. Compose state is similar to StateFlow in my opinion. However, compose State is very specific to Android Jetpack Compose. So it is platform specific, whereas StateFlow is more generic and platform independent.

StateFlow could be life-cycle aware

LiveData itself is life-cycle aware and StateFlow is NOT. StateFlow could be life-cycle aware, depending on how you collect it. Compose State itself is NOT life-cycle aware. Since it is used by the composable functions, when a composable function leaves composition, it automatically unsubscribes from compose State.

StateFlow requires Initial Value

Creating LiveData does NOT require an initial value

// live data - data holder 
val liveData = MutableLiveData<Int>()
Enter fullscreen mode Exit fullscreen mode

but, StateFlow and compose State require an initial value.

// state flow - data holder
val stateFlow = MutableStateFlow<Int?>(null)
// compose state - data holder
val composeState: MutableState<Int?> = mutableStateOf(null)
Enter fullscreen mode Exit fullscreen mode

Convert Flow to SharedFlow

The following example is based on this flow in your View Model class

val flow: Flow<Int> = flow {
    repeat(10000) { value ->
        delay(1000)
        emit(value)
    }
}
Enter fullscreen mode Exit fullscreen mode

and this sharedFlow variable defined.

private var sharedFlow = MutableSharedFlow<Int>()
Enter fullscreen mode Exit fullscreen mode

1. Flow.collect() and SharedFlow.emit()

This converts the Flow to SharedFlow using Flow<T>.collect and manually call the SharedFlow<T>.emit().

viewModelScope.launch { 
    flow.collect { value 
        -> sharedFlow.emit(value) 
    } 
}
Enter fullscreen mode Exit fullscreen mode

This is a hot stream. So it emits the value regardless anyone collects it.

2. Flow.shareIn()

You can also use Flow<T>.shareIn() to achieve the same result.

sharedFlow = flow.shareIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly
)
Enter fullscreen mode Exit fullscreen mode

However, if you change SharingStarted.Eagerly to SharingStarted.WhileSubscribed(), the SharedFlow becomes a cold stream.

Convert Flow to StateFlow

So we have this stateFlow variable defined in the view model.

private val stateFlow = MutableStateFlow<Int?>(null)
Enter fullscreen mode Exit fullscreen mode

1. Flow.collect() and StateFlow.value

This converts the Flow to StateFlow.

viewModelScope.launch { 
    flow.collect { value -> 
        stateFlow.value = value 
    } 
}
Enter fullscreen mode Exit fullscreen mode

Similar to SharedFlow, this is a hot stream. The difference is StateFlow is a data holder and SharedFlow is not.

2. Flow.stateIn()

You can also use Flow<T>.stateIn() to achieve the same result.

stateFlow = flow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly,
    initialValue = null)
Enter fullscreen mode Exit fullscreen mode

Similar to Flow<T>.shareIn() above, if you change SharingStarted.Eagerly to SharingStarted.WhileSubscribed(), the StateFlow becomes a cold stream.

In practice, it is advisable to use SharingStarted.WhileSubscribed(5000) instead of SharingStarted.WhileSubscribed() to account for screen rotation and prevent flow emission from restarting.

Important note: If you call Flow<T>.shareIn() and Flow<T>.stateIn() multiple times, it creates multiple flows which emit the value in the background. This eventually causes unnecessary resource leaks that you want to prevent.

Collect from SharedFlow and StateFlow

Collecting from SharedFlow and StateFlow is the same as collecting from the Flow. Refer to the following article on different ways of collecting flow.

1. Collect using RepeatOnLifecycle()

The recommended way to collect Flow by Google is using LifeCycle.RepeatOnLifecycle(). So, we're going to use it as an example.

This example below converts the stateFlow to compose state, which is the data holder for composable function.

@Composable 
fun SharedStateFlowScreen() { 
    val viewModel: StateSharedFlowViewModel = viewModel() 
    val lifeCycle = LocalLifecycleOwner.current.lifecycle

    /* compose state - data holder */ 
    var composeStateValue by remember { mutableStateOf<Int?>(null) }

    /* collect state flow and convert the data to compose state */     
    LaunchedEffect(true) { 
        lifeCycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
            viewModel.stateFlow.collect { composeStateValue = it } 
        } 
    } 
}
Enter fullscreen mode Exit fullscreen mode

2. Collect using collectAsStateWithLifecycle()

If you use Android lifecycle Version 2.6.0-alpha01 or later, you can reduce the code significantly using Flow<T>.collectAsStateWithLifecycle() API

First, you need to have this dependency in your build.gradle file.

dependencies { 
    implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha02' 
}
Enter fullscreen mode Exit fullscreen mode

Then, you can reduce the code to the following. Please note that you need to specify the @OptIn(ExperimentalLifecycleComposeApi::class)

@OptIn(ExperimentalLifecycleComposeApi::class) 
@Composable 
fun SharedStateFlowScreen() { 
    val viewModel: StateSharedFlowViewModel = viewModel()

    /* compose state - data holder */ 
    val composeStateValue by   
        viewModel.stateFlow.collectAsStateWithLifecycle() 
}
Enter fullscreen mode Exit fullscreen mode

Best Practices?

Honestly, what I find difficult in Android development is there are just way too many options to accomplish the same thing. So which one we should use? Now, we have LiveData, Flow, Channel, SharedFlow, StateFlow and compose State. In what scenario, we should use which one?

So I document the best practices based on various sources and my interpretations. I do not know whether they make sense. Things like this are likely very subjective too.

  1. Do NOT use LiveData especially if you're working on a new project. LiveData is legacy and eventually will be replaced by StateFlow.

  2. Do NOT expose Flow directly in View Model, convert it to StateFlow instead. This can avoid unnecessary workload on the main UI thread. Flow is a cold stream, it emits data (or restarts the data emission) every time you collect it.

  3. Expose StateFlow in your view model instead of compose State. Since StateFlow is platform independent, it makes your view model platform independent which allows you easy migration to KMM (which allows you to target both IOS and Android) for example. StateFlow is also more powerful (e.g. it allows you to combine multiple flows into one etc.)

  4. Collect StateFlow in your UI elements (either in activity or composable function) and convert the data to compose State. Compose State should be created and used only within the composable functions.

Whether ViewModel should hold StateFlow or compose State is questionable. I have been using compose State but it seems like StateFlow might be a better option here based on more complex use cases such as combining flow?

On the other hand, if I convert Flow to compose State in ViewModel directly, I don't need to convert it again in the composable function. Why do I need 2 state/data holders and collect twice?

What about one-time event?

I do not sure of the use case of SharedFlow and Channel. It appears to be used as a one-time event. If you have one subscriber, you use Channel. If you have multiple subscribers, you use SharedFlow.

However, this article here by the Google team kind of imply using SharedFlow or Channel as a one-time event is not recommended. It is mainly because they're hot stream, which runs into a risk of missing the events when the app is in the background or during configuration.

It seems to me we should probably just use StateFlow for everything. Let's forget the rest! Maybe this is easier this way...

Source Code

GitHub Repository: Demo_AsyncFlow (see the SharedStateFlowActivity)


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

Latest comments (0)