관리 메뉴

나만을 위한 블로그

[Android] StateFlow vs SharedFlow 본문

Android

[Android] StateFlow vs SharedFlow

참깨빵위에참깨빵 2023. 3. 14. 20:45
728x90
반응형

StateFlow와 SharedFlow를 설명하는 공식문서가 어제 날짜인 3월 13일에 업데이트됐다.

 

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

 

StateFlow 및 SharedFlow  |  Kotlin  |  Android Developers

StateFlow 및 SharedFlow 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. StateFlow와 SharedFlow는 흐름에서 최적으로 상태 업데이트를 내보내고 여러 소비자에게 값을

developer.android.com

 

이 공식문서 내용을 기준으로 StateFlow, SharedFlow에 대해 확인하고 둘의 차이도 같이 확인한다.

 

StateFlow

 

StateFlow는 현재 상태와 새 상태 업데이트를 수집기에 내보내는 관찰 가능한 상태 홀더 흐름(Flow)이다. value 속성을 통해서도 현재 상태 값을 읽을 수 있다. 상태를 업데이트하고 Flow에 전송하려면 MutableStateFlow 클래스의 value 속성에 새 값을 할당한다. 안드로이드에서 StateFlow는 관찰 가능한 변경 상태를 유지해야 하는 클래스에 아주 적합하다. View가 UI 상태 업데이트를 listen하고 구성 변경(configuration change)에도 기본적으로 화면 상태가 지속되도록 LatestNewsViewModel에서 StateFlow를 노출할 수 있다
class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    // 다른 클래스의 상태 업데이트를 방지하기 위한 backing property
    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
    // UI는 상태 업데이트를 얻기 위해 이 StateFlow에서 수집한다
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                // 최신 인기 뉴스로 View 업데이트
                // MutableStateFlow의 value 프로퍼티에 기록하여 흐름에 새 요소를 추가하고 모든 수집기를 업데이트한다
                .collect { favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
        }
    }
}

// LatestNews 화면의 다양한 상태를 나타낸다
sealed class LatestNewsUiState {
    data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(val exception: Throwable): LatestNewsUiState()
}
MutableStateFlow 업데이트를 담당하는 클래스가 생산자(producer)고 StateFlow에서 수집되는 모든 클래스가 소비자(consumer)다. flow 빌더를 써서 빌드된 Cold Flow와 달리 StateFlow는 Hot Flow다. Flow에서 수집해도 생산자 코드가 트리거되지 않는다. StateFlow는 항상 활성 상태고 메모리 안에 있으며 가비지 컬렉션 루트에서 이에 대한 다른 참조가 없는 경우에만 가비지 수집 대상이 된다. 새 소비자가 Flow에서 수집을 시작하면 스트림의 마지막 상태와 후속 상태가 수신된다. LiveData 같이 관찰 가능한 다른 클래스에서 이 동작을 찾을 수 있다. View는 다른 Flow와 마찬가지로 StateFlow를 listen한다
class LatestNewsActivity : AppCompatActivity() {
    private val latestNewsViewModel = // getViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // 생명주기 범위에서 코루틴 시작
        lifecycleScope.launch {
            // repeatOnLifecycle은 수명 주기가 STARTED 상태(또는 그 이상)에 있을 때마다 새 코루틴에서 블록을 시작하고 STOPPED일 때 취소한다
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Flow를 트리거하고 값 수신을 시작한다
                // 생명주기가 STARTED일 때 발생하고 STOPPED일 때 수집이 중지된다
                latestNewsViewModel.uiState.collect { uiState ->
                    // 새 값을 받음
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }
    }
}
UI를 업데이트해야 하는 경우 launch 또는 launchIn 확장 함수로 UI에서 직접 흐름을 수집하면 안 된다. 이런 함수는 뷰가 표시되지 않을 때에도 이벤트를 처리한다. 이 동작으로 앱이 다운될 수 있다. 이걸 방지하려면 repeatOnLifecycle API를 사용한다. 이 API는 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 이상 버전에서만 쓸 수 있다
Flow를 StateFlow로 바꾸려면 stateIn 중간 연산자를 사용한다

 

stateIn을 모른다면 아래 공식문서를 참고한다.

 

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html

 

stateIn

Converts a coldFlow into a hotStateFlow that is started in the given coroutine scope, sharing the most recently emitted value from a single running instance of the upstream flow with multiple downstream subscribers. See the StateFlow documentation for the

kotlinlang.org

Cold Flow를 주어진 coroutineScope에서 시작되는 Hot State Flow로 바꿔서 업스트림 Flow의 단일 실행 인스턴스에서 가장 최근에 방출된 값을 여러 다운스트림 구독자와 공유한다...(중략)...이 연산자는 일부 상태 값에 대한 업데이트를 제공하는 Cold Flow가 있고 생성 및 / 또는 유지 관리 비용도 많이 들지만 가장 최근 상태 값을 수집해야 하는 여러 구독자가 있을 때 유용하다. 예를 들어 비용이 많이 드는 네트워크 연결을 통해 백엔드에서 오는 상태 업데이트 Flow를 고려하면 설정하는 데 많은 시간이 걸린다...(중략)...stateIn을 쓰면 Flow의 모든 수집기 간에 단일 연결이 공유되며 연결이 필요할 때 이미 설정됐을 가능성이 있다

 

StateFlow, Flow, LiveData

 

StateFlow, LiveData는 비슷한 점이 있다. 둘 다 관찰 가능한 데이터 홀더 클래스고 앱 아키텍처에 사용할 때 비슷한 패턴을 따른다. 그러나 둘은 다르게 작동한다

- StateFlow는 초기 상태를 생성자에 전달해야 하지만 LiveData는 그렇지 않다
- 뷰가 STOPPED 상태가 되면 LiveData.observe()는 소비자를 자동으로 등록 취소하지만 StateFlow 또는 다른 Flow에서 수집하는 경우 자동으로 수집을 중지하지 않는다. 같은 동작을 실행하려면 Lifecycle.repeatOnLifecycle {}에서 Flow를 수집해야 한다

 

ShareIn을 써서 Cold Flow를 Hot Flow로 만들기

 

StateFlow는 Hot Flow로, Flow가 수집되는 동안 또는 가비지 컬렉션 루트에서 다른 참조가 있는 경우 메모리에 남아 있다. shareIn 연산자를 써서 Cold Flow를 Hot Flow로 바꿀 수 있다. 각 수집기에서 새 Flow를 만들 필요 없이 Kotlin 흐름 링크에서 예시로 생성한 callbackFlow를 쓰면 가져온 데이터를 shareIn을 통해 수집기 간에 공유할 수 있다. 다음을 전달해야 한다

- 흐름을 공유하는 데 사용되는 coroutineScope. SharedFlow를 필요한 만큼 유지하기 위해 이 scope는 소비자보다 오래 지속돼야 한다
- 각 새 수집기로 재생할 항목의 수
- 시작 동작 정책
class NewsRemoteDataSource(...,
    private val externalScope: CoroutineScope,
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}
위 예시에서 latestNews Flow는 마지막으로 내보낸 항목을 새 수집기로 재생하며 externalScope가 활성 상태고 활성 수집기가 있는 한 활성 상태로 유지된다. SharingStarted.WhileSubscribed() 시작 정책은 활성 구독자가 있는 동안 업스트림 생산자를 활성 상태로 유지한다. 다른 시작 정책도 쓸 수 있다. SharingStarted.Eagerly를 써서 생산자를 즉시 시작하거나 SharingStarted.Lazily를 써서 첫 번째 구독자가 표시된 후 공유를 시작하고 Flow를 영구적으로 활성 상태로 유지할 수 있다

 

SharedFlow

 

shareIn 함수는 모든 소비자에게 값을 내보내는 Hot Flow인 SharedFlow를 만든다. SharedFlow는 StateFlow의 유연한 구성 일반화다. shareIn을 쓰지 않고 SharedFlow를 만들 수도 있다. 예를 들어 SharedFlow를 쓰면 모든 컨텐츠가 주기적으로 동시에 새로고침되도록 앱의 나머지 부분에 틱을 전송할 수 있다. 최신 뉴스를 가져오는 것 외에도 좋아하는 주제 컬렉션으로 사용자 정보 섹션을 새로고침할 수도 있다. 아래 코드 스니펫에서 TickHandler는 다른 클래스가 컨텐츠를 새로고침할 시기를 알 수 있게 SharedFlow를 노출한다. StateFlow의 경우처럼 클래스에서 MutableSharedFlow 타입의 지원 속성을 써서 아이템을 Flow로 보낸다

참고) SharedFlow 인터페이스는 Flow 인터페이스를 구현하고, StateFlow 인터페이스는 SharedFlow 인터페이스를 구현한다. Flow -> SharedFlow -> StateFlow 순서로 구현하는 것이다.

// 앱의 콘텐츠를 새로 고쳐야 할 때 중앙 집중화하는 클래스
class TickHandler(
    private val externalScope: CoroutineScope,
    private val tickIntervalMs: Long = 5000
) {
    // 다른 클래스의 흐름 배출을 피하기 위한 Backing property
    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 동작을 커스텀할 수 있다

- replay를 쓰면 이전에 내보낸 여러 값을 새 구독자에게 다시 보낼 수 있다
- onBufferOverflow를 쓰면 버퍼가 전송할 아이템으로 가득 경우에 적용할 정책을 지정할 수 있다. 기본값은 호출자를 정지시키는 BufferOverflow.SUSPEND다. 다른 옵션은 DROP_LATEST, DROP_OLDEST다

또한 MutableSharedFlow에는 활성 수집기의 수가 포함된 subscriptionCount 속성이 있어서 비즈니스 로직을 적절하게 최적화할 수 있다. MutableSharedFlow에는 Flow에 전송된 최신 정보를 재생하지 않으려는 경우를 위한 resetReplayCache 함수도 있다

 

본문은 이걸로 끝이고 나머지는 추가로 보면 좋은 링크들이다. 처음부터 끝까지 쭉 읽어보면 프로젝트에 적용할 때 도움되는 내용들이 많다.

Flow와 MVVM, Retrofit을 섞어 쓰는 방법이 궁금하다면 아래 포스팅을 확인한다. stateIn, shareIn도 사용하지 않았고 어디까지나 예제 수준의 코드니 대충 이런 느낌이라는 걸 확인하고 다른 글들을 보면서 발전시켜 나가면 좋을 것이다.

 

https://onlyfor-me-blog.tistory.com/478

 

[Android] Coroutine Flow란? MVVM + Flow + Retrofit 예제

최근 안드로이드 진영에서 비동기 처리에 자주 사용했던 라이브러리인 RxJava가 걷어내지고 그 자리를 코루틴의 flow라는 것이 대신한다고 들었다. 그래서 구글에서 Compose를 비롯해 여러 방식으로

onlyfor-me-blog.tistory.com

 

반응형
Comments