본문 바로가기

안드로이드

안드로이드 jetcaster 예제 분석 - Flow와 Combine을 이용한 흐름 제어

반응형

https://github.com/android/compose-samples/blob/main/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt

위에 코드의 flow와 combine을 이용한 흐름들의 변경에 따라 값을 갱신(UI)하는 로직을 분석해보자

1. 코드 개요

1.1 SearchScreenViewModel의 핵심 플로우들

코드를 보면, 몇 가지 StateFlow/MutableStateFlow 들이 모여 최종 uiStateFlow를 만들어 내는 구조입니다.

  1. 사용자가 직접 입력/선택하는 값
    • keywordFlow: 사용자의 검색어 입력.
    • selectedCategoryListFlow: 사용자가 현재 선택한 카테고리 목록.
  2. 외부 데이터(저장소/DB 등)에서 가져오는 값
    • categoryInfoListFlow: DB나 Repository 등에서 불러온 전체 카테고리 목록(카테고리별 팟캐스트 수 정렬 등).
  3. 검색 조건
    • searchConditionFlow: keywordFlow + selectedCategoryListFlow + categoryInfoListFlow 3개를 combine하여 검색 조건을 만든다.
      • 만약 selectedCategoryListFlow가 비어있다면, 전체 categoryInfoListFlow를 사용.
      • 결과적으로 SearchCondition(검색어 + 선택된 카테고리 목록) 객체를 Flow로 관리.
  4. 검색 결과
    • searchResultFlow: searchConditionFlow를 flatMapLatest해서, PodcastsRepository 또는 PodcastStore가 제공하는 “검색 결과”를 가져오는 Flow.
    • 검색 조건이 바뀔 때마다 자동으로 새 검색을 수행해서 결과를 방출.
  5. 카테고리 선택 상태
    • categorySelectionFlow: 전체 카테고리 목록(categoryInfoListFlow)과 사용자가 선택한 목록(selectedCategoryListFlow)을 combine
    • “어떤 카테고리가 선택되었는지”를 Boolean으로 매핑한 CategorySelectionList를 만들어, UI에서 체크박스/토글 표시 등을 제어할 수 있게 함.
  6. 최종 UI 상태
    • uiStateFlow: 위에서 만든 흐름(keywordFlow, categorySelectionFlow, searchResultFlow)을 다시 combine해서, 최종 SearchScreenUiState를 만든다.
    • 로딩 중이면 Loading, 검색 결과가 있다면 HasResult, 아직 결과가 없다면 Ready 등으로 분기.

결국, ViewModel 안에서 여러 Flow를 단계적으로 합성하여 UI가 필요한 최종 상태를 만들어 내고, UI에서는 uiStateFlow만 구독하면 됩니다.


2. MVI(단방향 흐름) 시각에서 본 구조

MVI를 간단히 요약하면:

  1. Intent: 사용자 액션(검색어 변경, 카테고리 선택/해제 등)
  2. Model(State): UI가 관찰할 전체 상태(로딩 여부, 검색어, 결과 목록 등)
  3. View: 실제 화면(Composable 등), StateFlow를 구독해 화면 표시
  4. 단방향 흐름: View → (Intent) → ViewModel → (State) → View

위 SearchScreenViewModel은 Compose 쪽 @Composable 화면에서:

  • setKeyword(keyword: String)
  • addCategoryToSelectedCategoryList(category: CategoryInfo)
  • removeCategoryFromSelectedCategoryList(category: CategoryInfo)

와 같은 메서드를 통해 Intent를 받습니다. 이 메서드들은 내부적으로 keywordFlow나 selectedCategoryListFlow 등 MutableStateFlow에 값을 업데이트합니다.

그 뒤, Flow들의 조합 과정을 거쳐 최종 UI State(uiStateFlow)가 바뀌고, View(Composable)는 해당 Flow를 collect(구독)해서 자동으로 재컴포지션 합니다.

따라서 “사용자 이벤트 → 상태 변경 → UI 업데이트”가 단방향으로 흐르게 되며, 코드가 분산되지 않고 ViewModel에서 “검색 상태/카테고리 선택 상태/결과 상태”를 명확히 관리합니다.


3. 핵심 포인트와 이점

3.1 combine과 flatMapLatest를 이용한 “동적” 상태 계산

  • combine(A, B, C) { a, b, c -> ... }
    • 여러 Flow(A, B, C)가 각각 emit할 때마다 즉시 새 결과를 만들어 내는 Flow를 생성합니다.
    • 예: 검색어가 바뀌거나, 사용자가 선택한 카테고리가 바뀌어도, 혹은 DB에서 가져온 카테고리 목록이 바뀌어도 → 실시간으로 searchConditionFlow가 재계산됨.
  • flatMapLatest { ... }
    • 상위 Flow의 값이 바뀌면, 이전에 진행 중이던 작업을 취소하고 가장 최근 조건으로 새 Flow를 구독.
    • 예: 검색어가 빠르게 바뀔 때(사용자가 타이핑 중), 직전 검색 작업을 취소하고 새로운 검색 조건으로 다시 탐색을 수행하는 데 유용.

3.2 Sealed interface로 UI 상태를 캡슐화

sealed interface SearchScreenUiState { 
    data object Loading : SearchScreenUiState 
    data class Ready(...) : SearchScreenUiState data class             
    HasResult(...) : SearchScreenUiState 
}
  • UI에서 “로딩 중인지”, “결과가 있는지”, “아직 결과가 없는지” 등 화면 상태를 명시적으로 표현.
  • Composable 측에서 when(uiState)로 분기 처리하면, 각 상태마다 적절히 화면을 그릴 수 있음.

3.3 데이터 흐름이 명확해 유지보수성↑

  • ViewModel 내부에서 keywordFlow, selectedCategoryListFlow 등 “사용자 입력”을 위한 Flow와, “DB/Repo에서 불러오는” Flow를 분리해둠.
  • Combine/flatMapLatest 순서가 잘 드러나 있으므로, 새로운 조건을 추가해야 할 때(예: “에피소드 길이 범위” 필터)도 Flow만 하나 더 만든 후, combine에 추가하면 됩니다.

4. 실전 적용 시 Tips

  1. Flow의 스레드 정책
    • DB나 네트워크 작업은 IO 디스패처에서 진행하도록 적절히 flowOn 또는 withContext 처리.
    • 예제 코드에서는 Repository(혹은 Room DAO)에서 이미 비동기로 처리하고 있을 가능성이 큼.
  2. stateIn vs shareIn
    • stateIn(...)으로 “최초 값”을 지정하고, 구독 시점마다 최신값을 받을 수 있도록 합니다.
    • SharingStarted.WhileSubscribed(5_000)는 구독자가 없을 경우 5초 뒤 Flow를 중단하는 설정.
    • 필요 시 앱 전체 라이프사이클(혹은 화면 전환) 등에 맞춰 다른 SharingStarted 옵션을 선택할 수도 있습니다.
  3. 에러 처리
    • 여기서는 예시로 별도 에러 상태 처리 없이 로딩, 성공, 빈 결과 등을 분기하고 있습니다.
    • 실제 앱에서는 Repository/Store에서 에러가 발생하면 Flow가 어떻게 행동할지(emit, throw 등) 설계가 중요합니다.
  4. 테스트
    • 각 Flow를 테스트하거나, Intent(사용자 이벤트)를 순서대로 호출한 뒤 uiStateFlow가 예상대로 변하는지 Unit TestIntegration Test로 검증하기 쉽습니다.

5. 결론

이 코드는 단방향 상태 흐름을 위해 Jetpack Compose + Coroutines Flow의 combine, flatMapLatest, stateIn 등을 적극 활용한 예시입니다.

  • 사용자가 변화시키는 값(keywordFlow, selectedCategoryListFlow)과 DB/Repo에서 가져오는 값(categoryInfoListFlow)을 모아서 검색 조건을 실시간으로 갱신.
  • 그 조건이 바뀌면 flatMapLatest로 즉시 새로운 검색을 수행해 검색 결과 Flow를 업데이트.
  • 최종적으로는 UI State(SearchScreenUiState) 한 곳에 모아 UI와 연결.

이를 통해, MVI(혹은 MVVM과 유사하지만 단방향 흐름을 강조한) 스타일로 “입력 → 상태 변경 → UI 반영” 과정을 일관성 있게 관리할 수 있습니다.
Compose 환경에서 이러한 Flow 기반 상태 관리는 리컴포지션과도 잘 맞물려, 앱 규모가 커질수록 이점을 크게 체감하게 됩니다.

  • MVI든 MVVM이든 중요한 건 단방향 데이터 흐름과 “하나의 상태(UiState)를 중심”으로 관리하는 것입니다.
  • Flow의 combine, flatMapLatest, stateIn 덕분에 무수히 많은 이벤트/조건/데이터 소스를 단계적으로 조합해 최종 결과를 UI에 전달할 수 있습니다.
  • 이 예시는 Jetcaster 프로젝트 코드 중 SearchScreenViewModel이지만, 목록 필터링, 검색 기능, 선택 상태 관리가 필요한 모든 Compose UI 설계에 참고할 만한 패턴입니다.
반응형