UI 레이어 아키텍처
UI 라는 용어
- 사용하는 API와 관계없이 데이터를 표시하는 활동 및 프래그먼트와 같은 UI 요소를 가리킨다.
데이터 레이어의 역할
- 앱 데이터를 사용하고, UI 에서 쉽게 렌더링할 수 있는 데이터로 변환합니다.
- UI 렌더링 가능 데이터를 사용하고 사용자에게 표시할 UI 요소로 변환합니다.
- 이렇게 조합된 UI 요소의 사용자 입력 이벤트를 사용하고 입력 이벤트의 결과를 필요에 따라 UI 데이터를 반영합니다.
- 1~3단계를 필요한 만큼 반복한다.
위의 데이터 레이어의 역할을 알면, UI 와 데이터 레이어의 상호작용이 어떤식으로 이루어지는 지를 알 수 있다.
UI 상태 정의
사용자가 보는 항목이 UI 라면 UI 상태는 앱에서 사용자가 봐야 한다고 지정하는 항목을 말한다.
동전의 양면과 마찬가지로 UI 는 UI 상태를 시각적으로 나타냅니다.
UI 상태가 변경되면 변경사항이 즉시 UI에 반영됩니다.
불변성
불변성의 주요 이점은 변경 불가능한 객체가 순간의 애플리케이션 상태를 보장한다는 점이다.
덕분에 UI 는 상태를 읽고 이에 따라 UI 요소를 업데이트하는 한 가지 역할에 집중할 수 있다.
따라서,
UI 자체가 데이터의 유일한 소스인 경우를 제외하고, UI 에서 UI 상태를 직접 수정해서는 안된다.
이 원칙이 위반되면, 동일한 정보가 여러 정보 소스에서 비롯되어 데이터 불일치와 미세한 버그가 발생될 것이다.
단방향 데이터 흐름으로 상태 관리
UI 상태가 매우 단순하지 않은 이상 UI 의 역할은 오직 UI 상태를 사용 및 표시하는 것이어야 한다.
상태 홀더
UI 상태를 생성하는 역할을 담당하고 생성 작업을 필요한 로직을 포함하는 클래스를 상태 홀더라고 한다.
상태 홀더의 크기는 같은 단일 위젯부터 전체 화면이나 탐색 대상에 이르기까지 관리 대상 UI요소의 범위에 따라 다양한다.
전체화면이나 탐색 대상의 경우 일반적인 구현은 ViewModel 의 인스턴스이지만, 애플리케이션의 요구사항에 따라 간단한 클래스로도 충분히 가능하다.
* ViewModel
ViewModel 유형은 데이터 레이어에 엑세스할 권한이 있는 화면 수준 UI상태를 관리하는데 권장되는 구현이다.
또한, 구성이 변경되어도 자동으로 유지된다는 점이 있따.
ViewModel 클래스는 앱의 이벤트에 적용할 로직을 정의하고 결과로 업데이트되는 상태를 생성한다.
UI와 상태 생성자 간의 상호 종속을 모델링하는 방법은 다양하다.
하지만, UI와 ViewModel 클래스 사이의 상호작용은 대체로 이벤트 입력과 입력의 후속 상태인 출력으로 간주된다.
아래는 UDF의 작동 방식으로 ViewModel 작동 방식을 보여주는 다이어그램이다.
상태가 아래로 향하고, 이벤트는 위로 향하는 패턴을 단방향 데이터 흐름(UDF) 라고 한다.
이 패턴이 앱 아키텍처에 미치는 영향은 다음과 같다.
- ViewModel 이 UI 에 사용될 상태를 보유하고 노출한다. (UI 상태는 ViewModel 에 의해 변환된 애플리케이션 데이터다)
- UI 가 ViewModel 에 사용자 이벤트를 알린다.
- ViewModel 이 사용자 작업을 처리하고, 상태를 업데이트한다.
- 업데이트된 상태를 렌더링할 UI에 다시 제공된다.
- 상태 변경을 야기하는 모든 이벤트에 위의 작업이 반복된다.
로직의 유형
- 비즈니스 로직 : 상태 변경에 따라 진행해야 할 작업이다. 비즈니스 로직은 일반적으로 도메인 또는 데이터 레이어에 배치되고, UI 레이어에는 배치 되지 않는다.
- UI 동작 로직 : 상태 변경사항을 표시하는 방법이다.
특히 Context 같은 UI 유형의 경우, UI 로직은 ViewModel 이 아닌 UI에 있어야 한다.
- 테스트 가능성을 높이고 문제 구분에 도움되도록 UI 로직을 다른 클래스에 위임하고자 한다.
- UI가 점점 복잡해지는 경우 간단한 클래스를 상태 홀더로 만들 수 있다.
- UI에서 생성된 간단한 클래스는 UI의 수명주기를 따르기 때문에 Andorid SDK 종속 항목을 사용할 수 있다.
- ViewModel 객체 수명은 더 깁니다.
UDF 를 사용하는 이유
- 데이터 일관성 : UI 용 정보 소스가 하나다.
- 테스트 가능성 : 상태 소스가 분리되므로 UI와 별개로 테스트할 수 있다.
- 유지 관리성 : 상태 변경은 잘 정의된 패턴(ex. ViewModel)을 따른다. 즉, 변경은 사용자 이벤트 및 데이터를 가져온 소스 모두의 영향을 받습니다.
UDF 는 상태 생성 주기를 모델링한다.
또한, 여러가지 위치를 구분한다.
- 상태 변경이 발생하는 위치
- 변환되는 위치
- 최종적으로 사용되는 위치
이렇게 구분하면 UI 가 이름에 드러난 의미 그대로 동작할 수 있다.
즉, 상태변경사항을 관차랗여 정보를 표시하고, 변경사항을 ViewModel 에 전달하여 사용자 인텐트에 전달한다.
UI 상태 노출
UI 상태를 정의하고 이 상태의 생성을 관리할 방법을 결정한 후에는 생성된 상태를 UI에 표시하는 단계를 진행한다.
UDF를 사용하여 상태 생성을 관리하므로 생성된 상태를 스트림으로 간주할 수 있다.
즉, 시간 경과에 따라 여러 버전의 상태가 생성된다.
따라서, LiveData, StateFlow 와 같이 관잘 가능한 데이터 홀더에 UI 상태를 노출해야 한다.
그 이유는,
ViewModel 에서 데이터를 직접 가져오지 않고도 UI가 상태 변경사항에 반응할 수 있도록 하기 위해서다.
이러한 유형은 항상 최신 버전의 UI 상태를 캐시한다는 이점도 있다. 이는 구성 변경 후 빠른 상태 복원에 유용하다.
class NewsViewModel(...) : ViewModel() {
val uiState: StateFlow<NewsUiState> = …
}
UI 에 노출되는 데이터가 비교적 간단할 때는 UI 상태 유형으로 데이터를 래핑하는 것이 좋은 경우가 많다.
내보낸 상태 홀더와 관련 UI 요소 간의 관게를 전달하기 때문이다.
또한, UI 요소가 더 복잡해질 때 언제나 간편하게 UI 상태 정의를 추가하여 요소를 렌더링하는 데 필요한 더 많은 정보를 포함시킬 수 있다.
UIState 스트림을 만드는 일반적인 방법은 ViewModel 에서 지원되는 변경 가능한 스트림을 변경 불가능한 스트림으로 노출 하는 것이다.
예를 들어 MutableStateFlow<UiState>를 StateFlow<UiState>로 노출합니다.
class NewsViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
...
}
그런 다음 ViewModel 은 상태를 내부적으로 변경하는 메서드를 노출하여, UI 에 사용되도록 업데이트를 게시한다.
예를 들어 비동기 작업을 실행해야 하는 경우 viewModeScope를 사용하여 코루틴을 실행하고 코루틴이 완료되면 변경 가능한 상태를 업데이트할 수 있습니다.
class NewsViewModel(
private val repository: NewsRepository,
...
) : ViewModel() {
private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
private var fetchJob: Job? = null
fun fetchArticles(category: String) {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
try {
val newsItems = repository.newsItemsForCategory(category)
_uiState.update {
it.copy(newsItems = newsItems)
}
} catch (ioe: IOException) {
// Handle the error and notify the UI when appropriate.
_uiState.update {
val messages = getMessagesFromThrowable(ioe)
it.copy(userMessages = messages)
}
}
}
}
}
위 코드에서 보면 NewsViewModel 클래스는 특정 카테고리의 기사를 가져오려고 시도한 후에 결과에 따라 UI 가 적절하게 반응할 수 있도록 시도의 성공 또는 실패 결과를 UI 상태에 반영한다.
추가 고려사항
UI 상태 객체는 서로 관련성 있는 상태를 처리해야 한다.
- 불일치가 줄어들고, 코드를 이해하기가 더 쉽다.
UI 상태 : 단일 스트림인지 여러 스트림인지
UI 상태 노출 대상을 단일 스트림과 여러 스트림 중에서 선택할 때, 내보낸 항목간의 관계이다. 단인 스트림 노출의 가장 큰 장점은 편의성과 데이터 일관성이다. 즉, 상태 사용자가 언제나 즉시 최선 저보를 확인할 수 있다.
하지만, ViewModel 상태의 스트림이 별개일 때 적합한 경우도 있다.
- 관련 없는 데이터 유형 : UI를 렌더링하는데 필요한 일부 상태는 서로 완전히 별개일 수 있다. 이 때, 서로 다른 상태를 함께 번들로 묶는 데 드는 비용이 이점보다 더 클 수 있으며 이는 상태 주 하나가 다른 상태보다 더 자주 업데이트되는 경우가 그렇다.
- UiState 비교 : UiState 객체에 필드가 많을수록 필드 중 하나를 업데이트하면 스트림이 내보내질 가능성이 크다. 따라서, Flow API 또는 LiveData의 distinctUnitlChanged() 와 같은 메서드를 사용하여 완화 작업이 필요해보인다.
UI 상태 사용
UI에서 UiState 객체의 스트림을 사용하려면 사용 중인 관찰 가능한 데이터 유형에 터미널 연산자를 사용한다.
예를 들어 LiveData의 경우 observe() 메서드를 사용하고 Kotlin 흐름의 경우 collect() 메서드나 이 메서드의 변형을 사용합니다.
UI에서 관찰 가능한 데이터 홀더를 사용할 때는 UI의 수명 주기를 고려해야 합니다.
수명 주기를 고려해야 하는 이유는 사용자에게 뷰가 표시되지 않을 때 UI가 UI 상태를 관찰해서는 안 되기 때문입니다.
LiveData를 사용하면 LifecycleOwner가 수명 주기 문제를 암시적으로 처리합니다.
흐름을 사용할 때는 적절한 코루틴 범위와 repeatOnLifecycle API로 처리하는 것이 가장 좋습니다.
class NewsActivity : AppCompatActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
참고
https://developer.android.com/topic/architecture/ui-layer?hl=ko
'Mobile App' 카테고리의 다른 글
[안드로이드] - Data Layer (데이터 레이어) (0) | 2022.07.26 |
---|---|
[안드로이드] - 도메인 레이어 (Use Case) (0) | 2022.07.25 |
[안드로이드] - Android SharedPreferences (0) | 2022.07.21 |
[안드로이드] - Android Preferences data storage (0) | 2022.07.21 |
[안드로이드] - Android Data & File Repository (안드로이드 데이터) (0) | 2022.07.18 |