๐ค
๊ฐ๋ 1๏ธโฃ1๏ธโฃ StateFlow & SharedFlow
StateFlow์ SharedFlow๋ flow์์ state update๋ฅผ emitํ๊ณ ์ฌ๋ฌ consumer์๊ฒ ๊ฐ์ emitํ ์ ์๋ Flow API์ด๋ค.
1๏ธโฃ StateFlow
StateFlow๋ observableํ state holder flow๋ก์, ํ์ฌ์ state์ ์๋ก์ด state ์
๋ฐ์ดํธ๋ฅผ collector์ emitํ๋ค. StateFlow์ value ํ๋กํผํฐ๋ฅผ ํตํด์ ํ์ฌ state ๊ฐ์ ์ฝ์ ์๋ ์๋ค. state๋ฅผ ์
๋ฐ์ดํธํ์ฌ flow์ ์ ์กํ๋ ค๋ฉด, MutableStateFlow ํด๋์ค์ value์ ์๋ก์ด ๊ฐ์ ํ ๋นํ๋ฉด ๋๋ค.
Android์์ StateFlow๋ observableํ mutable state๋ฅผ ๊ฐ์ง๊ณ ์์ด์ผํ๋ ํด๋์ค์ ๋งค์ฐ ์ ํฉํ๋ค. ์๋์ ์์์๋ View๊ฐ UI state์ update๋ฅผ ์์ ๋๊ธฐํ๊ณ configuration ๋ณ๊ฒฝ์๋ ๊ธฐ๋ณธ์ ์ผ๋ก UI์ state๊ฐ ์ง์๋๋๋ก LatestNewsViewModel์์ StateFlow๋ฅผ ๋
ธ์ถ์ํค๊ณ ์๋ค.
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// ๋ค๋ฅธ ํด๋์ค๊ฐ state๋ฅผ ์
๋ฐ์ดํธํ์ง ๋ชปํ๊ฒ ํ๊ธฐ ์ํ ๋ฐฑ์
ํ๋กํผํฐ
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// ์ค์ UI์์๋ state๋ฅผ ์
๋ฐ์ดํธํ๊ธฐ ์ํด์ uiState์์ collectํจ
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// latest favorite news๋ก ๋ทฐ๋ฅผ ์
๋ฐ์ดํธํด์ค
// MutableStateFlow์ธ _uiState์ value์ ๊ฐ์ ์ค์ ํจ
// flow์ ์๋ก์ด element๋ฅผ ์ถ๊ฐํด์ฃผ๊ณ ,collector๋ค์๊ฒ ์
๋ฐ์ดํธ!
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
// LatestNews ํ๋ฉด์ state๋ฅผ ๋ํ๋
sealed class LatestNewsUiState {
data class Success(news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(exception: Throwable): LatestNewsUiState()
}
Producer๋ MutableStateFlow ์
๋ฐ์ดํธ๋ฅผ ๋ด๋นํ๋ ํด๋์ค์ด๊ณ , consumer๋ StateFlow์์ collect๋๋ ํด๋์ค์ด๋ค. flow ๋น๋๋ฅผ ์ฌ์ฉํ์ฌ ๋น๋๋๋ cold stream๊ณผ ๋ฌ๋ฆฌ, StateFlow๋ hot stream์ด๋ค. ์ฆ flow์์ collectํด๋ producer ์ฝ๋๊ฐ trigger๋์ง ์๋๋ค. StateFlow๋ ํญ์ active ์ํ์ด๊ณ ๋ฉ๋ชจ๋ฆฌ ๋ด์ ์๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฐ๋น์ง ์ปฌ๋ ์
์ ๋ฃจํธ์์ reference๊ฐ ์๋ ๊ฒฝ์ฐ์๋ง ๊ฐ๋น์ง ์ปฌ๋ ์
์์ ๊ฐ์ ธ๊ฐ ์ ์๋ค.
์๋ก์ด consumer๊ฐ flow์์ collectํ๊ธฐ ์์ํ๋ฉด, ์คํธ๋ฆผ์ ๋ง์ง๋ง state์ ๋ค์ state๊ฐ ์์ ๋๋ค. ์๋์ ์ฝ๋์์ View๋ ๋ค๋ฅธ flow๋ค๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก StateFlow๋ฅผ ์์ ๋๊ธฐ(listen) ํ๋ค.
lass LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
// lifecycle scope์์ ์ฝ๋ฃจํด์ launchํจ
lifecycleScope.launch {
// lifecycle์ด STARTED state ๋๋ ๊ทธ ์์ state๋ค์ด๊ฑฐ๋, STOPPED์ด๋ผ์ cancelํ ๋
// repeatOnLifecycle ์ด ๋ธ๋ก์ launchํจ
repeatOnLifecycle(Lifecycle.State.STARTED) {
// flow๋ฅผ ํธ๋ฆฌ๊ฑฐํ์ฌ value๋ค์ listenํ๊ธฐ ์์ํจ
// lifecycle์ด STARTED์ผ๋ ํธ๋ฆฌ๊ฑฐ๋๊ณ , lifecycle์ด STOPPED์ผ ๋ collect๋ฅผ ๋ฉ์ถค
latestNewsViewModel.uiState.collect { uiState ->
// ์๋ก์ด ๊ฐ์ด ์์ ๋์์ ๋!!
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
โ ๏ธ ์ฃผ์ํด์ผ ํ ์
UI๋ฅผ ์
๋ฐ์ดํธํด์ผํ ๋, UI์์์ launch๋ launchIn์์ repeatOnLifecycle ์์ด ๋ค์ด๋ ํธ๋ก flow๋ฅผ collectํ๋ฉด ์๋๋ค. launch์ launchIn๋ ๋ทฐ๊ฐ ํ์๋์ง ์๋ ๊ฒฝ์ฐ์๋ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ด๋ค. ์ด๋ก์ธํด ์ฑ์ด ๋ค์ด๋ ์๋ ์์ผ๋ฏ๋ก repeatOnLifecycle API๋ฅผ ์ฌ์ฉํด์ ์ฑ์ด ๋ค์ด๋๋ ๊ฒ์ ๋ฐฉ์งํด์ค์ผ ํ๋ค.!!
๐ flow๋ฅผ StateFlow๋ก ๋ณํํ๋ ค๋ฉด?
stateIn์ด๋ผ๋ intermediate ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
๐คผโโ๏ธ StateFlow vs LiveData
โค๏ธ ๊ณตํต์
- observable ๋ฐ์ดํฐ ํ๋ ํด๋์ค์
- ์ฑ ์ํคํ ์ฒ์์ ๋น์ทํ ํจํด์ผ๋ก ์ฐ์
๐ ์ฐจ์ด์
- StateFlow
- ์ด๊ธฐ ์ํ๋ฅผ ์์ฑ์์ ์ ๋ฌํด์ผ ํจ
- ๋ทฐ๊ฐ STOPPED ์ํ๊ฐ ๋์์ ๋ ์๋์ผ๋ก collect๋ฅผ ์ค์งํ์ง ์์. ์๋์ผ๋ก ์ค์ง๋๊ฒ ํ๋ ค๋ฉด Lifecycle.repeatOnLifecycle ๋ธ๋ก์์ flow๋ฅผ collectํด์ผ ํจ
- LiveData
- ์ด๊ธฐ ์ํ๋ฅผ ์์ฑ์์ ์ ๋ฌํ์ง ์์
- ๋ทฐ๊ฐ STOPPED ์ํ๊ฐ ๋์์ ๋ cosumer๋ฅผ ์๋์ผ๋ก unregisterํจ
cold stream โก๏ธ hot stream ๋ณํ? ๐shareIn๐
StateFlow๋ hot stream์ผ๋ก flow๊ฐ collect๋๋ ๋์, ๊ทธ๋ฆฌ๊ณ ๊ฐ๋น์ง ์ปฌ๋ ์
๋ฃจํธ์์ ๋ค๋ฅธ ๋ ํผ๋ฐ์ค๊ฐ ์๋ ๊ฒฝ์ฐ์ ๋ฉ๋ชจ๋ฆฌ์ ๋จ์์๋ค. shareIn ์ฐ์ฐ์๋ฅผ ํ์ฉํ์ฌ cold stream์ hot stream์ผ๋ก ์ ํ์ํฌ ์ ์๋ค.
๊ฐ collector์์ ์๋ก์ด flow๋ฅผ ๋ง๋ค ํ์ ์์ด flow์์ ์์ฑํ callbackFlow๋ฅผ ์ฐ๋ฉด Firestore์์ ๊ฐ์ ธ์จ ๋ฐ์ดํฐ๋ฅผ shareIn์ ํตํด collector๋ค๋ผ๋ฆฌ ๊ณต์ ํ ์ ์๋ค. ์ด๋ฅผ ์ํด์๋ ๋ค์๊ณผ ๊ฐ์ ๋ด์ฉ์ ์ ๋ฌํด์ค์ผ ํ๋ค.
- flow๋ฅผ ๊ณต์ ํ๋๋ฐ ์ฌ์ฉ๋๋
CoroutineScope. ๊ณต์ Flow๋ฅผ ํ์ํ ๋งํผ ์ ์งํ๊ธฐ ์ํด์, ์ด scope๋ consumer๋ณด๋ค ์ค๋ ์ง์๋์ด์ผ ํจ - ๊ฐ๊ฐ์ ์๋ก์ด collector๋ก replayํ item์ ๊ฐ์
- start behavior ์ ์ฑ
class NewsRemoteDataSource(..., private val externalScope: CoroutineScope, ) { val latestNews: Flow<List<ArticleHeadline>> = flow { ... }.shareIn( externalScope, replay = 1, started = SharingStarted.WhileSubscribed() ) }์์ ์ฝ๋์์ lastNews flow๋ ๋ง์ง๋ง์ผ๋ก emitํ item์ ์๋ก์ด collector๋ก replayํ๊ณ ,
externalScope๊ฐ active ์ํ์ด๊ณ activeํ collector๊ฐ ์๋ ํ, active ์ํ๋ก ์ ์ง๋๋ค.SharingStarted.WhileSubscribed()์ ์ฑ ์ activeํ subscriber๊ฐ ์๋ ๋์์๋ upstream producer๋ฅผ active ์ํ๋ก ์ ์งํ๋ค. ์ฌ๊ธฐ์ ๋ค๋ฅธ ์์ ์ ์ฑ๋ ์ฌ์ฉํ ์ ์๋ค. ์๋ฅผ ๋ค์ด์SharingStarted.Eagerly๋ฅผ ์ฌ์ฉํ์ฌ producer๋ฅผ ์ฆ์ ์์ํ ์๋ ์๋ค. ์๋๋ฉดSharingStarted.Lazily๋ฅผ ์ฌ์ฉํด์ ์ฒซ ๋ฒ์งธ subscriber๊ฐ ํ์๋ ํ์์ผ ๊ณต์ ๋ฅผ ์์ํ๊ณ , Flow๋ฅผ ์๊ตฌ์ ์ผ๋ก active ์ํ๋ก ์ ์งํ ์ ์๋ค.
2๏ธโฃ SharedFlow
shareIn ํจ์๋ SharedFlow๋ฅผ ๋ฐํํ๋ค. SharedFlow๋ flow๋ฅผ collectํ๋ ๋ชจ๋ consumer์๊ฒ ๊ฐ์ emitํ๋ hot stream์ด๋ค. SharedFlow๋ StateFlow๊ฐ highly-configurableํ๊ฒ ์ผ๋ฐํ๋ Flow์ด๋ค.
shareIn์ ์ฐ์ง ์๊ณ ๋ SharedFlow๋ฅผ ๋ง๋ค ์๋ ์๋ค. ์๋ฅผ ๋ค์ด ๋ชจ๋ ์ฝํ
์ธ ๊ฐ ์ฃผ๊ธฐ์ +๋์์ ์๋ก๊ณ ์นจ๋๋๋ก ์ฑ์ ํฑ์ ์ ์กํ๋ SharedFlow๋ฅผ ์ฌ์ฉํ ์ ์๋ค. ์๋์ ์ฝ๋์์ TickHandler๋ SharedFlow๋ฅผ exposeํด์ ๋ค๋ฅธ ํด๋์ค๊ฐ ์๋ก๊ณ ์นจํ๋ ํ์ด๋ฐ์ ์ ์ ์๊ฒ ํด์ค๋ค. StateFlow์ฒ๋ผ ํด๋์ค์์ MutableSharedFlow ํ์
์ ์ฌ์ฉํ์ฌ item์ flow๋ก emitํ๋ค.
// ์ฑ์ ์ฝํ
์ธ ๊ฐ ๋ฆฌํ๋ ์ฌ๋์ด์ผํ ๋ centeralize์์ผ์ฃผ๋ TickHandler ํด๋์ค
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 {
// tick update๋ฅผ listen ์ค...
tickHandler.tickFlow.collect {
refreshLatestNews()
}
}
}
suspend fun refreshLatestNews() { ... }
...
}
๐ SharedFlow ๋์์ ์ปค์คํ ํ๋ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ
replay: ์ด์ ์ emitํ ์ฌ๋ฌ ๊ฐ๋ค์ ์๋ก์ด subscriber์๊ฒ ๋ค์ ๋ณด๋ผ ์ ์์onBufferOverflow: ๋ฒํผ์ ์ ์กํ item์ผ๋ก ๊ฐ๋ ์ฐผ์ ๋ ์ด๋ป๊ฒ ํ ๊ฑด์ง์ ๋ํด ์ ์ฑ ์ ์ง์ ํ ์ ์์. ๊ธฐ๋ณธ๊ฐ์ ํธ์ถ์๋ฅผ ์ ์ง์ํค๋BufferOverflow.SUSPEND์. ๋ค๋ฅธ ์ต์ ์ผ๋ก๋DROP_LATEST,DROP_OLDEST๊ฐ ์์ ๊ทธ๋ฆฌ๊ณMutableSharedFlow์ ํ๋กํผํฐ ์ค์subscriptionCount๊ฐ ์๋๋ฐ,subscriptionCount๋ฅผ ํตํด activeํ colletor์ ๊ฐ์๋ฅผ ์ ์ ์๊ธฐ ๋๋ฌธ์ business logic์ ์ต์ ํ์ํฌ ์ ์๋ค.MutableSharedFlow์๋ ๋ํresetReplayCache๋ผ๋ ํจ์๋ ์๋๋ฐ,resetReplayCache๋ flow์ ์ ์ก๋ ์ต์ ์ ๋ณด๋ฅผ replayํ์ง ์๊ณ ์ถ์ ๋ ์ธ ์ ์๋ค.
[์ฐธ๊ณ ์ฌ์ดํธ]
์๋๋ก์ด๋ ๊ณต์ ๋ฌธ์ - StateFlow and SharedFlow
Ella
Flow in Android