https://developer.android.com/kotlin/flow/stateflow-and-sharedflow
StateFlow
and SharedFlow
are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers.
StateFlow
StateFlow
is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value
property. To update state and send it to the flow, assign a new value to the value
property of the MutableStateFlow
class.
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Update View with the latest favorite news
// Writes to the value property of MutableStateFlow,
// adding a new element to the flow and updating all
// of its collectors
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
data class Success(news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(exception: Throwable): LatestNewsUiState()
}
The class responsible for updating a MutableStateFlow
is the producer, and all classes collecting from the StateFlow
are the consumers. Unlike a cold flow built using the flow
builder, a StateFlow
is hot: collecting from the flow doesn’t trigger any producer code. A StateFlow
is always active and in memory, and it becomes eligible for garbage collection only when there are no other references to it from a garbage collection root.
When a new consumer starts collecting from the flow, it receives the last state in the stream and any subsequent states. You can find this behavior in other observable classes like LiveData
.
Flow는 cold이지만 StateFlow는 hot이다.
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
// Start a coroutine in the lifecycle scope
lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// Note that this happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
latestNewsViewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
activity에서 코루틴 사용할때는 lifecycleScope 를 사용한다. viewModel과 다르게 repeatOnLifecycle(Lifecycle.State.STARTED) 를 통해 lifecycle이 STARTED이상의 상태에서 작동하게 한다. StateFlow, Flow는 lifecycle에 자동으로 맞추어져서 소멸되지 않고 background에서도 계속 작동하게된다. 이를 방지 하기 위해서는 repeatOnLifecycle를 앞에서 언급한것과 같이 사용해서 특정 상태의 경우에만 작동하게 한다. viewModel의 경우는 Activity, fragment와는 다르게 lifecycle이 없으므로 그안에서 사용하는 경우는 좀 다르다.
StateFlow
or any other flow does not stop collecting automatically. To achieve the same behavior,you need to collect the flow from a Lifecycle.repeatOnLifecycle
block.
Warning: Never collect a flow from the UI directly from
launch
or the launchIn
extension function if the UI needs to be updated. These functions process events even when the view is not visible. This behavior can lead to app crashes. To avoid that, use the repeatOnLifecycle
API as shown above.
To convert any flow to a StateFlow
, use the stateIn
intermediate operator.
StateFlow, Flow, and LiveData
StateFlow
and LiveData
have similarities. Both are observable data holder classes, and both follow a similar pattern when used in your app architecture.
Note, however, that StateFlow
and LiveData
do behave differently:
StateFlow
requires an initial state to be passed in to the constructor, whileLiveData
does not.LiveData.observe()
automatically unregisters the consumer when the view goes to theSTOPPED
state, whereas collecting from aStateFlow
or any other flow does not stop collecting automatically. To achieve the same behavior,you need to collect the flow from aLifecycle.repeatOnLifecycle
block.
Making cold flows hot using shareIn
StateFlow
is a hot flow—it remains in memory as long as the flow is collected or while any other references to it exist from a garbage collection root. You can turn cold flows hot by using the shareIn
operator.
callbackFlow에 관해서는 https://developer.android.com/kotlin/flow를 확인해보면 정보를 얻을수 있다.
Using the callbackFlow
created in Kotlin flows as an example, instead of having each collector create a new flow, you can share the data retrieved from Firestore between collectors by using shareIn
. You need to pass in the following:
- A
CoroutineScope
that is used to share the flow. This scope should live longer than any consumer to keep the shared flow alive as long as needed. - The number of items to replay to each new collector.
- The start behavior policy.
SharedFlow
The shareIn
function returns a SharedFlow
, a hot flow that emits values to all consumers that collect from it.
You can create a SharedFlow
without using shareIn
. As an example, you could use a SharedFlow
to send ticks to the rest of the app so that all the content refreshes periodically at the same time.
// Class that centralizes when the content of the app needs to be refreshed
class TickHandler(
private val externalScope: CoroutineScope,
private val tickIntervalMs: Long = 5000
) {
// Backing property to avoid flow emissions from other classes
private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
val tickFlow: SharedFlow<Event<String>> = _tickFlow
init {
externalScope.launch {
while(true) {
_tickFlow.emit(Unit)
delay(tickIntervalMs)
}
}
}
}
class NewsRepository(
...,
private val tickHandler: TickHandler,
private val externalScope: CoroutineScope
) {
init {
externalScope.launch {
// Listen for tick updates
tickHandler.tickFlow.collect {
refreshLatestNews()
}
}
}
suspend fun refreshLatestNews() { ... }
...
}