StateFlow
- 현재 상태와 새로운 상태 업데이트를 수집기에 내보내는 관찰 가능한 상태 홀더 흐름
- value 속성으로 현재 상태 값을 읽을 수 있습니다
- 상태를 업데이트 하고 흐름에 전송하려면
MutableStateFlow
클래스의 value 속성에 새 값을 할당합니다
- 관찰 가능한 변경 가능 상태를 유지해야할때 적합합니다
- View 가 UI 상태 업데이트를 Listen 하고 구성 변경에도 기본적으로 화면 상태가 지속되도록
LatestNewsViewModel
에서 StateFlow를 노출할 수 있습니다
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
sealed class LatestNewsUiState {
data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(val exception: Throwable): LatestNewsUiState()
}
- MutableStateFlow 업데이트를 담당하는 클래스가 생산자, StateFlow에서 수집되는 모든 클래스가 소비자
- flow 빌더와는 달리 StateFlow는 생산자 코드가 트리거 되지 않습니다
- StateFLow는 항상 활성 상태이고 메모리 내에 있으며 가비지 컬렉션 루트에서 달리 참조가 없는 경우에만 가비지 컬렉션에 사용할 수 있습니다
- 새로운 소비자가 흐름에서 수집을 시작하면 스트림의 마지막 상태와 후속 상태가 수신됩니다
- LiveData 같이 관찰 가능한 다른 클래스에서 이 동작을 찾을 수 있습니다.
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel =
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
latestNewsViewModel.uiState.collect { uiState ->
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
UI 업데이트 주의
- launch 또는 launchIn 확장 함수로 UI에서 직접 흐름을 수집하면 안됩니다
- 이런경우에는 뷰가 표시되지 않는 경우에도 이벤트를 처리하게 되어 앱이 다운될 수 있습니다
- 이것을 방지하기 위해서는 repeatOnLifeCycle API를 사용합니다
StateFlow, Flow, LivData 의 차이점
StateFlow
의 경우 초기 상태를 생성자에게 전달하지만 LiveData
는 전달하지 않습니다
- 뷰가 Stopped 상태일때
LiveData.observe()
는 소비자를 자동으로 등록 취소
- StateFlow 는 다른 흐름에서 수집하는 경우엔 자동으로 수집을 중지하지 않습니다. 만약 LiveData 처럼 실행되게 하려면
Lifecycle.repeatOnLifecycle
블록에서 흐름을 수집해야 합니다
shareIn
StateFlow
는 흐름이 수집되는 동안 가비지 컬렉션 루트에서 다른 참조가 있는 경우 메모리에 남아있게 합니다
- callbackFlow를 사용하여 firestore에서 가져온 데이터를 shasreIn 을 통해 collect간에 공유할 수 있습니다. 전달되는 종류는 다음과 같습니다.
externalScope
CoroutineScope 공유 흐름을 필요한 만큼 유지하기 위해 이 범위는 소비자 보다 오래 지속되어야 합니다
replay = 1
각 새 수집기로 재생할 항목의 수
started = { }
시작 동작 정책
class NewsRemoteDataSource(...,
private val externalScope: CoroutineScope,
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
...
}.shareIn(
externalScope,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
}
- latestNews 는 마지막으로 내보낸 항목을 새 수집기로 재생하여
externalScope
가 활성 상태이고 활성 수집기가 있는 한 활성 상태로 유지됩니다
sharingStarted.WhileSubscribed()
는 subscriber가 있는 동안 업스트림 생산자를 활성 상태로 유지합니다.
sharingStarted.Eagerly
생산자를 즉시 시작
sharingStarted.Lazyly
첫번째 subscriber를 표시한 후 공유를 시작하고 흐름을 영구적으로 활성 상태로 유지
SharedFlow
- 모든 콘텐츠가 주기적으로 동시에 새로고침 되도록 나머지 부분에 tick을 전송할 수 있습니다
- 최신 뉴스 외 좋아하는 주제 컬렉션으로 사용자 정보 섹션을 새로고침 할 수 있습니다
class TickHandler(
private val externalScope: CoroutineScope,
private val tickIntervalMs: Long = 5000
) {
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 {
tickHandler.tickFlow.collect {
refreshLatestNews()
}
}
}
suspend fun refreshLatestNews() { ... }
...
}
reply
를 사용하면 이전에 내보낸 여러 값을 새 구독자를 위해 다시 보낼 수 있습니다
onBufferOverflow
를 사용하여 버퍼가 전송할 항목으로 가득 찬 경우 적용할 정책을 지정할 수 있습니다
- 기본값으로
BufferedOverflow.SUSPEND
으로 호출자를 정지 합니다
- DROP_LATEST
- DROP_OLDEST
- MutableSharedFlow 에서 활성 수집기의 수가 포함된 subscriptionCount 속성이 있어서 비즈니스 로직을 적절하게 최적화
- MutablesharedFLow 에는 흐름에 전송된 최신 정보를 재생하지 않으려는 경우를 위한 resetReplayCache 를 사용
Reference