본문 바로가기

IT/kotlin언어

[flow] coroutine flow 문서 읽기 -2

안녕하세요 남갯입니다.

 

https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ko 

 

StateFlow 및 SharedFlow  |  Android 개발자  |  Android Developers

StateFlow 및 SharedFlow StateFlow와 SharedFlow는 흐름에서 최적으로 상태 업데이트를 내보내고 여러 소비자에게 값을 내보낼 수 있는 Flow API입니다. StateFlow StateFlow는 현재 상태와 새로운 상태 업데이트를

developer.android.com

 

StateFlow

StateFlow는 현재상태와 새로운 상태를 업데이트 수집하는 관찰 가능한 상태 홀더 flow입니다.

상태를 업데이트를 하기 위해서는 MutableStateFlow클래스의 value 속성에 새 값을 할당합니다.

 

따라서 뭔가 ui상태와같이 변경 가능상태를 유지해야하는 클래스에 적합합니다.

즉 뷰가 UI 상태 업데이트를 수신 대기하고 구성변경에 화면상태가 지속되도록 하기위해 StateFlow를 노출할 수 있습니다.

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)
                }
        }
    }
}

 

여기서 MutableStateFlow 업데이트를 담당하는 클래스가 생산자이고, StateFlow에 수집되는 모든 클래스가 소비자입니다. flow 빌더와는 다르게 Hot Flow 입니다. 흐름에서 수집해도 생산자의 코드가 트리거되지 않습니다.

 

항상 활성상태고 참조가 없을경우 GC가 동작하게 할 수 있습니다.

소비자측에서 흐름을 수집하게 되면 마지막 상태와 후속 상태가 수신됩니다.

        // Start a coroutine in the lifecycle scope
        lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // Note that this happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                latestNewsViewModel.uiState.collect { uiState ->
                    // New value received
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }

LiveData와 같이 관찰 가능한 클래스에서 동작을 수신할 수 있습니다.

 

 

단 위와같이 launch 나 launchIn 과 같이 직접 흐름을 수집하지 말고 onStart 이후부터 동작하도록 변경해야한다.

기존 Flow를 StateFlow로 변환하기 위해서는 stateIn 중간 연산자를 사용해야한다.

 

StateFlow와 LiveData, Flow

StateFlow와 LiveData는 비슷한점이 있는데, 둘 다 관찰 가능한 데이터 홀더 클래스이고, 아키텍처 내부에 사용할 때

비슷한 패턴을 따르게 됩니다.

 

1. StateFlow는 무조건 초기생성자 전달을 해야하지만, LiveData의 경우는 전달하지 않습니다.

2. STOPPED 상태에 LiveData는 자동으로 Observe를 자동으로 취소하는 반면 StateFlow는 수집하는 흐름이 있는경우 자동으로 수집을 중지하지 않아서, Lifecycle.repeatOnLifecycle 블록에서 수집을 진행해야합니다.

 

shareIn을 사용해서 Cold Flow -> Hot Flow

stateFlow는 hot Flow로 흐름이 수집되는동안 참조가 있는경우 GC에 동작하지 않습니다. shareIn연산자를 사용해서 

ColdFlow -> HotFlow로 전환이 가능합니다. 

 

이전장에서 설명햇던 callbackFlow를 사용해서 가져온 데이터를 shareIn을 통해 공유가 가능합니다.

class NewsRemoteDataSource(...,
    private val externalScope: CoroutineScope,
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}

shareIn에 사용할 externalScope, 반복횟수 , 시작 정책

externalScope가 활성상태이고, 수집기가 있는동안에서는 활성상태로 유지가 됩니다.

SharingStarted.WhileSubscribed() 시작 정책은 활성 구독자가 있는 동안 업스트림 생산자를 활성 상태로 유지합니다.

SharingStarted.Eagerly 를 사용해 즉시시작하거나, SharingStarted.Lazily를 사용해서 구독자가 표시 된 후 흐름을 영구적으로 활성상태로 유지할 수 있습니다.

SharedFlow

위에서 사용한 shareIn 함수는 수집하고 있는 소비자에게 SharedFlow를 반환하게 됩니다. (HotFlow)

shareIn을 사용하지 않고 SharedFlow를 만들 수 있습니다. SharedFlow를 사용하면 콘텐츠가 주기적으로 새로고침 되도록 앱의 나머지 부분에 틱을 전송할 수 있습니다.

 

// 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() { ... }
    ...
}

collect가 불리는 순간부터 5초마다 refreshLatestNews를 업데이트 하게 됩니다.

replay를 통해 이전에 보낸 여러개의 값을 구독자로 보낼 수 있고, onBufferOverflow를 사용하면 

버퍼에 데이터가 가득 찼을경우 정책을 지정가능합니다.

 

MutableSharedFlow에는 subscriptionCount 속성을 통해 비즈니스 로직을 최적화 가능하게 하고,

MutableSharedFlow에는 Flow에 전송된 최신정보를 재생하지 않으려는 경우 resetReplayCahce 함수도 있음.

 

추가 

liveData를 Flow로 마이그레이션

https://medium.com/androiddevelopers/migrating-from-livedata-to-kotlins-flow-379292f419fb

 

Migrating from LiveData to Kotlin’s Flow

In this post you’ll learn how to expose Flows to a view, how to collect them, and how to fine-tune it to fit specific needs.

medium.com

sharedIn stateIn

https://medium.com/androiddevelopers/things-to-know-about-flows-sharein-and-statein-operators-20e6ccb2bc74