본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[coroutine] SharedFlow & StateFlow

by 돼지왕왕돼지 2021. 5. 8.
반응형

 

 

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

 

 

 

 

반응형

댓글0