StateFlow
-
StateFlow 는 observable flow 로 collector 에게 현재값과 업데이트 되는 새로운 값을 전달하는 녀석이다.
현재 값을 value property 를 통해 읽을 수도 있다.
state 를 update 하고 flow 에게 그 값을 보내기 위해서는 MutableStateFlow 에 value 값을 설정해주면 된다.
flow { } builder 로 만든 녀석들은 cold flow 이지만, StateFlow 는 hot flow 이다.
따라서 collect 하는 순간 최신 값을 전달받는다.
-
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()
}
-
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
// This coroutine will run the given block when the lifecycle
// is at least in the Started state and will suspend when
// the view moves to the Stopped state
lifecycleScope.launchWhenStarted {
// Triggers the flow and starts listening for values
latestNewsViewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
-
launchWhen() 함수를 사용하는 것이 항상 안전한 것은 아니다.
View 가 bg 상태가 되었을 때, coroutine 은 suspend 가 되지만, producer 는 계속 active 상태로 view(consumer)가 소비하지 않는 값을 발행한다.
이런 동작은 CPU & memory 측면에서 낭비가 될 수 있다.
그러나 ViewModel scope 에 있는 StateFlow 는 launchWhen 과 함께 사용하는 것이 안전하다.
그러므로 launch 나 launchIn 으로 UI 사이드에서 collect 하는 것은 "하지 말아라!"
bg 상태에서도 event 를 처리할 수 있기 때문에, app crash 로 이어지기 쉽다.
* StateFlow, Flow, and LiveData
-
StateFlow 와 LiveData 는 비슷한 점이 많다.
차이점은 다음과 같다.
StateFlow 는 constructor 로 초기값을 필요로 한다. LiveData 는 그렇지 않다.
LiveData.observe() 는 view 가 STOPPED 상태가 되면 자동으로 unregister 가 된다, 반면 StateFlow 는 그렇지 않다.
-
Flow 의 collect 를 수동으로 멈추려면 아래와 같이 해야 한다.
class LatestNewsActivity : AppCompatActivity() {
...
// Coroutine listening for UI states
private var uiStateJob: Job? = null
override fun onStart() {
super.onStart()
// Start collecting when the View is visible
uiStateJob = lifecycleScope.launch {
latestNewsViewModel.uiState.collect { uiState -> ... }
}
}
override fun onStop() {
// Stop collecting when the View goes to the background
uiStateJob?.cancel()
super.onStop()
}
}
또 다른 방법은 asLiveData 를 사용해서 observe 하는 방법.
class LatestNewsActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
latestNewsViewModel.uiState.asLiveData().observe(owner = this) { state ->
// Handle UI state
}.
}
}
Making cold flows hot using shareIn
-
shareIn 을 사용하여 cold flow 를 hot flow 로 만들 수 있다.
shareIn 에는 다음 것들을 param 으로 전달해야 한다.
CoroutineScope. 이 scope 는 consumer 보다 더 오래 사는 녀석이어야 한다.
새로운 collector 에게 replay 해야 하는 item 숫자
start behavior policy
class NewsRemoteDataSource(...,private val externalScope: CoroutineScope)) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
...
}.shareIn(
externalScope,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
}
이 예제에서는 latestNews 는 가장 마지막 item 을 새로운 collector 에게 전달하고, externalScope 가 살아있는 한 함께 살아있는다.
SharingStarted.WhileSubscribed() 는 active subscribe 가 있는 한 살아있도록 하는 정책이다.
다른 옵션도 가능한데 SharingStarted.Eagerly 는 producer 를 바로 시작시키고, SharingStarted.Lazily 는 첫번재 subscriber 가 등장했을 때에 살아나서 영원히 active 로 유지된다.
SharedFlow
-
shareIn 함수는 SharedFlow 를 return 하는데, 이 녀석은 hot flow 이다.
SharedFlow 는 StateFlow 의 일반화된 형태로 다양한 설정이 가능하다.
-
SharedFlow 는 shareIn 없이도 만들 수 있다.
// 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() { ... }
...
}
-
sharedFlow 는 다음 값들을 customize 할 수 있다.
replay : 새로운 subscribe 에게 몇 개의 기존 value 를 emit 할 것인지
onBufferOverflow : buffer 가 full 일 때 어떻게 할 것인가. 기본값은 BufferOverflow.SUSPEND 이며 caller 를 suspend 시킨다. 다른 옵션은 DROP_LATEST, DROP_OLEST 가 있다.
-
MutableSharedFlow 는 subscriptionCount property 가 있어 active collector 값을 알 수 있다.
resetReplayCache 함수가 있어 latest info 를 초기화 시킬 수도 있다.
-
참고자료 : https://developer.android.com/kotlin/flow/stateflow-and-sharedflow
끝
'프로그래밍 놀이터 > Kotlin, Coroutine' 카테고리의 다른 글
[Coroutine] Coroutine scope functions (0) | 2022.01.29 |
---|---|
[coroutine] Flow vs RxJava (0) | 2021.05.10 |
[kotlin] LazyThreadSafeMode SYNCHRONIZED vs. PUBLICATION (0) | 2021.01.28 |
[Kotlin] Coroutine 소개 (0) | 2020.08.24 |
[Kotlin] Coroutine 은 어떻게 동작하는가? (0) | 2020.08.23 |
댓글