๐ค
๊ฐ๋ 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