-
ViewModel에서 UI State를 어떻게 처리하고 있나 ?
- UI State
- 앱에서 사용자에게 표시되는 정보
- 사용자가 보는 항목이 UI라면, UI 상태는 앱에서 사용자가 봐야한다고 지정되는 항목
- 뷰모델에서 활용하고있는 stateFlow / liveData 들이 UI State라고 볼 수 있음
- UIState를 활용하고 있는건 사람마다 / 회사마다 다 다름
- 깃헙 sample도 나중에 전달 해 줌
-
Sample 앱에서는 로딩이 있고 / 리스트를 보여주고 / 오류를 보여주거나 / 아이템이 비어있는 상태를 보여주는 앱을 제작함
-
UI State 여러개로 처리하기 ( General method )
// 로딩 보여주기 ...
private val _isLoading = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
// 아이템 보여주기 ...
- 위와 같이 변수륾 만들어 놓고 binding 혹은 observing
- 각 상태들이 다른 상태에 영향을 주지 않는다는 장점이 있음
- 데이터바인딩과도 / 컴포즈와도 함께 활용 할 수도 있음
- 장점
- 각자의 상태가 영향을 주지 않음
- 코드 작성 난이도가 다소 쉬움
- 단점
- 변경을 누락해서 개발자 실수로 이어지기 쉬움
- 특정 이벤트가 어떤 상태 변경을 만드는지 파악이 어려움
- 코드 가독성이 좋지 않음
-
data class 하나로 만들기
data class NewsUiState (
val isLoading: Boolean
val isError: Boolean
val newsItems: List<News>
) {
val isEmpty: Boolean = !isLoading && newsItems.isEmpty()
companion object {
val UnInitialized = NewsUiState( ... )
}
}
private val _state = MutableSharedFlow(NewsUiState)
val _state = _state.asStateFlow()
- 위 state 변수를 data binding / observe를 통해 view 갱신
- 마찬가지로 compose와 함께 활용 가능
- data 클래스 하나만 활용하기 때문에, copy 메소드를 활용하여 쉽게 상태 변경 가능
- 장점
- data class 특성을 활용 할 수 있음
- 부분적인 업데이트가 편리하다
- 상태에 필요한 로직을 클래스에 위임 할 수 있음
- 단점
- 하나의 상태만 변경해도 모든곳에 전파 됨
- 클래스로 분리하기 위한 설계 능력이 요구됨
-
sealed class로 상태 분리하기
sealed class NewsUiState {
object Uninitialized : NewsUiState()
object Loading : NewsUiState()
...
}
- sealed 클래스로 데이터 바인딩을 활용 할 때엔, 특정 변수 타입과 함께 활용할 때엔 추가적인 코드가 많이 생산 될 수 있는 단점이 있긴 함
sealed class NewsUiState {
open val newsItems: List<NewsItemUiState> = emptyList
object Uninitialized : NewsUiState()
object Loading : NewsUiState()
...
}
- compose에서는 xml에서 활용하는것이 아니기 때문에 좀 더 UIState 코드가 간편해짐
- compose 에서는 when 표현식을 활용 할 수 있다는 강점이 있음
- MVI에서는 reducer 개념을 활용하기도 함
- 타입 관련된 처리가 많아짐 ( if ( it is Success ) ... )
- 장점
- sealed class를 활용한다
- 가독성이 좋고, 표현하고자 하는 상태가 클래스와 1:1 대응함
- 단점
- 상태의 복잡도와 클래스 갯수가 비례함
- 화면 전체를 다루는 경우보다, 아이템 상태를 다루는데 좀 더 유리함
- xml로 뷰를 짤경우 데이터바인딩에 복잡한 바인딩 어댑터가 추가 될 수 있음
-
UIState 일반화 하기 ( Modeling, 추상화 하기 )
data class UIState<T> (
val isLoading: Boolean = false,
val cause: Throwable? = null,
val value: T? = null
) {
val isError: Boolean = cause != null
companion object {
fun <T> success(value: T) = Success(value)
...
}
}
- sealed 클래스로도 동일하게 활용 할 수 있음
- state를 mutableStateFlow로 담아놓고, 이 stateFlow를 map 등을 활용하여 변수를 만들어놓고, 만들어진 변수를 xml에서 binding 해줄 수 있음
- 추상화 된 것이라 baseViewModel 등을 만들어 놓고, 공통적으로 활용 할 수있음
UIState
라는 단순 interface를 만들고, 활용하기도 함
- 장점
- 시스템을 만들기 좋고, 아키텍처와 같이 활용할 수 있음 ( 매버릭스 / TCA 아키텍처 ... )
- 가독성이 좋음
- 단점
- 구현 난이도가 높음
- 시스템과 약간만 달라도 충돌이 발생하고, 도입하기 어려워짐
- 사용하기 위해서 팀원들을 설득해야함