UI 레이어에서 사용되는 UI 상태를 관리하기 위해 ViewModel 클래스 안에서 UI 상태 홀더 클래스를 관리하고 있다. 요즘은 UI 상태 홀더 클래스로 LiveData 보다는 StateFlow를 일반적으로 사용한다. UI 레이어에서 LiveData보다 StateFlow를 많이 사용하는 이유 중 하나는 Flow가 제공하는 많은 함수들을 이용할 수 있기 때문이라고 생각한다.
나는 이전에 LiveData를 사용하는 것처럼 StateFlow를 거의 유사하게 사용해왔다.
// LiveData
private val _selelctedSido = MutableLiveData<Sido?>(null)
val selectedSido: LiveData<Sido?> = _selelctedSido
// StateFlow
private val _selectedSido = MutableStateFlow<Sido?>(null)
val selectedSido = _selectedSido.asStateFlow()
// or val selectedSido: StateFlow<Sido?> = _selectedSido
기존에는 사용되는 모든 UI 상태에 대해서 private, public 변수 쌍을 만들어서 각각의 UI 상태들을 화면 단위에서 사용했었다.
// FilterViewModel
private val _selectedSido = MutableStateFlow<Sido?>(null)
val selectedSido = _selectedSido.asStateFlow()
private val _selectedSigungu = MutableStateFlow<Sigungu?>(null)
val selectedSigungu = _selectedSigungu.asStateFlow()
private val _sidoList = MutableStateFlow<List<Sido>>(emptyList())
val sidoList = _sidoList.asStateFlow()
private val _sigunguList = MutableStateFLow<List<Sigungu>>(emptyList())
val sigunguList = _sigunguList.asStateFlow()
// FilterScreen
@Composable
fun FilterScreen(
filterViewModel: FilterViewModel = hiltViewModel(),
onNavigateToHome: () -> Unit
) {
val selectedSido by filterViewModel.selectedSido.collectAsStateWithLifecycle()
val selectedSigungu by filterViewModel.selectedSigungu.collectAsStateWithLifecycle()
val sidoList by filterViewModel.sidoList.collectAsStateWithLifecycle()
val sigunguList by filterViewModel.selectedSido.collectAsStateWithLifecycle()
}
Composable 함수에서 각각의 state를 수집하여 각각 state를 필요한 곳에 사용하였다.
그렇지만 Composable 함수에서 4개의 state를 수집하여 이를 활용하기 보다는 1개 또는 최소한의 state를 수집하여 관리하고 싶었다.
combine 함수는 각각의 타입을 갖고 있는 여러 개의 flow를 한 개의 새로운 타입의 flow를 리턴하는 함수이다.
public fun <T1, T2, T3, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
@BuilderInference transform: suspend (T1, T2, T3) -> R
): Flow<R> = combineUnsafe(flow, flow2, flow3) { args: Array<*> ->
transform(
args[0] as T1,
args[1] as T2,
args[2] as T3
)
}
combine 함수를 이용해서 앞선 코드의 stateflow 변수들을 하나의 flow로 합칠 수 있다.
selectedSido -> 선택한 시/도 (상위) 지역
selectedSigungu -> 선택한 시/군/구 (하위) 지역
sidoList -> 시/도 (상위) 지역 리스트
sigunguList -> 시/군/구 (하위) 지역 리스트
처음 시/도 (상위) 지역 리스트 정보를 받아온 후에, 시/도 지역을 선택하면 선택한 시/도 지역의 하위 지역인 시/군/구 리스트를 받아온다. 시/군/구 리스트 중 하나의 하위 지역을 선택하면 선택된 정보를 바탕으로 또 다른 API 호출에 사용할 것이다.
{
private val _selectedSido = MutableStateFlow<Sido?>(null)
private val _selectedSigungu = MutableStateFlow<Sigungu?>(null)
private val _sidoList = MutableStateFlow<List<Sido>>(emptyList())
val filterUiState: StateFlow<FilterUiState> = combine(
_selectedSido,
_selectedSigungu,
_sidoList
) { selectedSido, selectedSigungu, sidoList ->
val sigunguList = selectedSido?.let {
regionRepository.fetchSigungu(it.code).getOrThrow()
} ?: emptyList()
FilterUiState.Success(
filter = Filter(
sido = sidoList,
sigungu = sigunguList,
selectedSido = selectedSido,
selectedSigungu = selectedSigungu
)
)
}
.onStart {
regionRepository.fetchSido()
.onSuccess { sidoList ->
_sidoList.update { sidoList }
_selectedSido.update { sidoList.first() }
}
}.catch {
FilterUiState.Error
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = FilterUiState.Loading
)
}
sealed interface FilterUiState {
data class Success(val filter: Filter) : FilterUiState
data object Loading : FilterUiState
data object Error : FilterUiState
}
data class Filter(
val sido: List<Sido>,
val sigungu: List<Sigungu>,
val selectedSido: Sido?,
val selectedSigungu: Sigungu?
)
combine 함수를 이용하여 FilterUiState 타입의 StateFlow를 생성하였다. 이를 통해서 FilterScreen에서 FilterUiState 타입의 StateFlow를 사용해 상태를 수집할 수 있게 하였다.
좌측에는 상위 지역을 선택할 수 있고, 상위 지역을 선택하면 API 호출을 통해서 우측의 하위 지역들을 불러와 하위 지역을 선택할 수 있도록 구현할 수 있었다.
combine 함수를 사용해 하나의 state만 노출시켜 사용하였는데 잘못 사용하고 있음을 느꼈다.
첫 번째로 노출하는 stateflow는 각 세 개의 flow 중 어느 하나의 flow에 변경사항이 발생하여도 stateFlow에서 값이 방출되어서 api 호출 결과를 리턴하는 fetchSigungu 함수가 호출된다. 시/군/구가 선택되는 상황에서는 다시 API가 호출될 필요가 없는데, 불필요하게 API 가 호출되었다.
두 번째로 combine을 통해 합쳐지는 모든 flow는 서로의 영향을 크게 받는다고 느꼈다. 단순히 상위 지역을 선택하는 것 자체는 UI에 바로 반영될 수 있지만, combine을 통해서 하나의 상태를 수집하여 UI에 반영한다면 곧바로 반영되지 않아 스마트폰 앱이 정상적으로 작동하지 않는다고 느낄 수 있다. 하나의 상위 지역을 선택하면 그로 인해서 API를 호출하고, API 호출 결과를 기다리기까지 선택되었다는 표시가 바로 반영되지 않는다.
combine 함수는 각 노출되어야 하는 flow가 합쳐질 각 flow에 모두 영향을 받는 경우에 쓰는 것이 적절하다는 것을 느꼈다. 그래서 매번 각 flow의 값이 방출될 때마다 새롭게 API 호출이 필요한 경우에 combine 함수를 쓰는 것이 좋을 것 같다.