StateFlow vs SharedFlow in Compose

March 1, 2022

If you write primarily Android code in Kotlin, it’s time to adopt StateFlow for tasks you used LiveData for.

So in your ViewModel, your LiveData code:

private val _myLiveData = MutableLiveData<String>()
val myLiveData = _myLiveData

can be replaced with:

private val _myStateFlow = MutableStateFlow("")
val myStateFlow = _myStateFlow.asStateFlow()

This post will describe when it makes to use Kotlin’s StateFlow vs SharedFlow.

StateFlow

StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors. As the name suggest, StateFlow is suitable for showing current state and preserving it during configuration changes such as screen rotations.

So in a Compose app, if your ViewModel contains:

class MainViewModel : ViewModel() {
    private val messageList = listOf(
        "apple",
        "tomato",
        "banana"
    )

    private val _stateFlowMessage = MutableStateFlow("")
    val stateFlowMessage = _stateFlowMessage.asStateFlow()

    fun onButtonClicked() {
        viewModelScope.launch {
            val message = messageList.random()
            _stateFlowMessage.emit(message)
        }
    }
}

You can observe your StateFlow in a Composable like so:

class MainActivity : ComponentActivity() {
    private val viewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeFlowsTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MainScreen(viewModel)
                }
            }
        }
    }
}

@Composable
fun MainScreen(viewModel: MainViewModel) {

    val message by viewModel.stateFlowMessage.collectAsState()

    Text(
        text = message
     )
}

Flow<T>.collectAsState() establishes a subscription to a Flow maintained by our ViewModel. The ViewModel survives configuration changes and therefore will preserve the contents of the StateFlow, a behavior similar to LiveData.

SharedFlow

SharedFlow is a hot flow that emits values to all consumers that collect from it. SharedFlow can be used for one-time events. It can achieve similar effect as a custom SingleLiveEvent (which was never considered to be a good practice—yet exists in many code bases out there).

Your ViewModel may contain the following code:

class MainViewModel : ViewModel() {
    private val messageList = listOf(
        "apple",
        "tomato",
        "banana"
    )

    private val _sharedFlowMessage = MutableSharedFlow<String>()
    val sharedFlowMessage = _sharedFlowMessage.asSharedFlow()

    fun onButtonClicked() {
        viewModelScope.launch {
            val message = messageList.random()
            _sharedFlowMessage.emit(message)
        }
    }
}

Which can be accessed from your Composable like so:

class MainActivity : ComponentActivity() {
    private val viewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeFlowsTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    MainScreen(viewModel)
                }
            }
        }
    }
}

@Composable
fun MainScreen(viewModel: MainViewModel) {

    val context = LocalContext.current

    LaunchedEffect(true) {
        viewModel.sharedFlowMessage.collect { toastMessage ->
            Toast.makeText(context, "Random fruit: $toastMessage", Toast.LENGTH_SHORT).show()
        }
    }
}

Note that we cannot use collectAsState() in our Composable since this is a one time event that does not get assigned an initial value and therefore won’t even compile.

We have to collect the SharedFlow instead. Note that if we don’t use LaunchedEffect, the Flow will be collect ed every time this Composable is recomposed. Therefore, we use LaunchedEffect(true) and pass true as key to ensure that side effect gets called only once when the Composable gets composed initially. At that point we start observing the SharedFlow and will react to it when it changes by showing a Toast.

We only start observing the StateFlow when the Composable leaves the composition.

Visualizing it

Here is a gif comparing both StateFlow and SharedFlow.

Data rendered by the StateFlow (Text Composable) gets preserved after rotation. On the other hand, when using SharedFlow, the Toast does not get shown again after screen rotation.

And as always, code for this blog post can be found on GitHub. https://github.com/jshvarts/ComposeFlows

 
Previous
Previous

Compose remember vs remember mutableStateOf

Next
Next

Bottom Nav with Nav Graphs in Compose