UI Layer
- UI Layer의 역할은 화면에 앱 데이터를 표시하는 것
- 따라서, 사용자 상호작용(ex. 버튼 누르기) 또는 외부 입력(ex. 네트워크 응답)으로 인해 데이터가 변할 때마다 변경사항을 반영하도록 UI가 업데이트 되어야 함
- 사실상 UI Layer는 Data Layer에서 가져온 앱 데이터를 시각적으로 보여주는 것에 관심이 있다.
UI Layer의 구성 요소
UI = UIelements + State holders
UIelements
: UI 요소가 갖는 본질적인 상태
- 예시
- TextView의 font, font size, font color
- android: font
- android: fontSize
State holders
: 화면의 UI요소에 데이터를 보여주기 위해 필요한 앱 데이터
- 예시
- NewsUiState
- UI elements
화면에 데이터를 렌더링하는 역할, (ex. View 또는 Jetpack Compose) - State holders
- 데이터를 보유하고 이를 UI에 표시하며 비즈니스 로직을 처리하는 역할 (ex. ViewModel)
- UI 상태를 생성하며, 생성 작업에 필요한 로직을 포함하는 클래스
UI Layer 아키텍처
일반적으로 UI란 데이터를 표시하는 activity, fragment와 같은 UI 요소를 가리킨다.
(반대로, Data Layer의 역할은 앱 데이터를 보유 및 관리하며 앱 데이터에 액세스할 권한을 제공하는 역할을 한다.)
UI 계층에서 하는 일
- 앱 데이터를 화면에 보여주기 쉬운 모양으로 변환하는 작업 실행(앱 데이터 -> UI State로 변환)
- UI 요소에 UI State를 연결하는 작업 실행
- 사용자의 이벤트를 받고 이벤트에 응답하기 위해 UI State를 업데이트 하는 작업 실행
(Data Layer의 역할은 앱 데이터를 보유하고 관리하며 앱 데이터에 액세스할 권한을 제공하는 것이다.)
UI State
- 앱 데이터로부터 화면에 보여주기 쉬운 모양으로 추출되고 변환된 데이터를 의미
- 사용자에게 표시되는 데이터
- UI 상태가 변경되면 변경사항사항이 즉시 UI에 반영되어야 한다.
뉴스 앱에서의 UI State 예시
- NewsUiState와 NewsItemUiState가 존재
data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = listOf(),
val userMessages: List<Message> = listOf()
)
data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
...
)
UI State가 지켜야 하는 원칙
- 불변성(Immutability)
- 변하지 않는 특성
- 외부에서 setXXX()함수를 호출하여 UI State를 변경할 수 없다는 의미(=read only, 읽기 전용)
- Activity, Fragment 클래스에서 UI State를 변경해서는 안된다.
- 불변성 원칙의 장점
- Activity, Fragment의 수명 주기와 상관 없이 UI State를 변하지 않게 유지할 수 있다.
- Activity, Fragment가 UI State를 읽고(쓰지는 않음) 그 값을 UI 요소에 반영하는 역할에만 집중할 수 있게 해준다.
- data class로 선언
- 불변성 원칙을 지키기 위해 보통 kotlin의 data class를 활용하여 UI State를 만든다.
- data class 파라미터는 val로 선언
UI State 클래스 이름 짓기에 대한 컨벤션
- 공식 문서에서는 XXUiState로 짓기를 권장하고 있다.
State holder 클래스
이름 그대로 UI State를 잡아두고 저장하고 관리하는 클래스
- State Holder 클래스의 관심사
- UI State 생산하기
- UI State를 사용해야 하는 로직 정의하기
- ViewModel 클래스
- Android Jetpack 라이브러리가 제공하는 대표적인 State Holder 클래스
- 뉴스 앱에서의 StateHolder 클래스 예시
- NewsViewModel 클래스가 존재할 것
- NewsItemUiState 클래스를 생산하고, 관련된 로직이 정의되어 있을 것
단방향 데이터 플로우로 상태 관리(UDF)
단방향 데이터 플로우(UDF)란?
-> UI State는 아래로 흐르고, evenet는 위로 흐르는 플로우
UI가 data를 소유하고 관리하고 생성하고 가공하는 등의 역할을 하게되면 복잡해지고 코드의 결합성이 높아져 테스트 및 유지보수에 안좋은 영향을 끼칠 수 있다.
따라서, UI 상태가 매우 단순하지 않는 이상 UI의 역할은 오직 UI 상태를 사용 및 표시해야 한다.
UDF가 앱 아키텍처에 미치는 영향
- ViewModel이 UI에 사용될 상태를 보유하고 노출한다. UI상태는 ViewModel에 의해 변환된 애플리케이션 데이터이다.
- UI가 ViewModel에 사용자 이벤트를 알린다.
- ViewModel이 사용자 작업을 처리하고 상태를 업데이트한다.
- 업데이트된 상태가 UI에 다시 적용된다.
- 상태 변경을 야기하는 모든 이벤트에 위의 작업이 반복된다.
UDF 아키텍처 설계 총정리
- ViewModel은 앱 데이터를 UI State 형태로 변환하는 역할을 하며, 해당 UI State를 보유하고 있다.
- UI State는 앱 데이터가 UI에 노출하기 쉬운 형태로 추출된 형태이다.
- UI는 유저의 이벤트를 받으면 ViewModel에게 알린다.
- ViewModel은 유저의 이벤트에 응답하기 위해 UI State를 업데이트 한다.
- UI 요소는 업데이트된 UI State를 UI 요소에 반영한다.
UI 계층과 Data 계층간의 소통
ViewModel은 Repository 또는 Usecase Class와 함께 사용되어 데이터를 가져와 UI상태로 변환하는 동시에 상태 변경을 야기할 수 있는 이벤트를 통합한다.
UI Layer < - > Data Layer
- UI 요소: 북마크 클릭 이벤트를 ViewModel에게 알린다.
- ViewModel: UI State를 업데이트 한다.
- Data 계층에게 앱 데이터 변경 필요를 알린다.
- Data 계층에 의해 외부(서버)에서 앱 데이터가 업데이트 된다. (북마크 체크 여부가 true로 변경)
- 업데이트된 앱 데이터를 ViewModel에게 전달한다.
- ViewModel: 업데이트된 앱 데이터를 새로운 UI State로 생산한다.
- ViewModel: 새로운 UI State를 UI 요소에게 전달한다.
- UI요소: 전달받은 UI State를 UI 요소에 연결하여 보여준다.
로직의 유형(UI, Business)
- 비즈니스 로직
- 앱 데이터에 대한 제품 요구사항에 대한 구현이다.
- 앞서 언급했던 뉴스 기사 북마크는 앱에 value(데이터)를 제공하므로 비즈니스 로직의 예라고 할 수 있다.
- 비즈니스 로직은 일반적으로 Domain Layer 또는 Data Layer에서 처리한다. (UI Layer에서는 처리하면 안된다.)
- UI 로직
- 화면에 상태 변경사항을 표시하는 방법이다.
- 예를들어 Android Resources를 사용하여 화면에 표시할 올바른 텍스트를 가져오거나, 사용자가 버튼을 클릭할 때 특정 화면으로 이동하거나, 토스트 메시지 또는 스낵바를 사용하여 화면에 사용자 메시지를 표시한다.
- 특히, Context 같은 UI 유형의 경우 UI 로직은 ViewModel이 아닌 UI에 있어야한다.
- UI State와 Logic의 관계
- UI State는 고정된 속성값이 아니다
- 앱 데이터의 변경이나 사용자 이벤트에 의해 바뀔 수 있는 속성값이다
- 비즈니스 로직에 의해 앱 데이터가 변경된다
- UI로직에 의해 화면에 보이는 것들이 변경된다
- 결론
: Logic에 의해 UI State가 변경된다(=새로운 UI State가 생산된다)
UDF로 설계해야 하는 이유
UDF는 다음과 같이 관심사를 분리 시킨다.
- UI State가 생산되는 곳
- UI State 변경이 발생하는 곳
- UI State가 소비되는 곳
상태 변경사항을 관찰하여 정보를 표시하고 변경사항을 ViewModel에 전달하는식으로 동작을 한다.
UDF 설계의 장점
- 데이터 일관성: 특정 UI와 연관되어 있는 UI State는 단 하나다.
- 테스트 용이성: 상태 소스가 분리되므로 UI와 별개로 테스트가 가능하다.
UI State 생산하기
Observable Data Holder 사용
- 종류
- LiveData: Android Jetpack에서 제공 중인 관찰 가능한 데이터 홀더(Google 제공)
- StateFlow: Kotlin Coroutine에서 제공 중인 관찰 가능한 데이터 홀더(JetBrain에서 제공)
- 사용해야 하는 이유
- UI 요소가 UI State가 새로 생성되는 것(=업데이트)을 알아서 직접 감지할 수 있기 때문
(ViewModel에서 데이터를 직접 가져오지 않고도 UI 상태 변경사항을 반영 할 수 있음 )
-> 최신 버전의 UI 상태를 유지할 수 있음
- UI 요소가 UI State가 새로 생성되는 것(=업데이트)을 알아서 직접 감지할 수 있기 때문
Observable Data Holder 정의 예시 코드
- 공식 문서에서는 StateFlow를 사용한 예시 코드를 제공하고 있다.
- LiveData를 사용한 예시는 별도 코드랩(Use LiveData with ViewModel (android.com))으로 제공중
잘 알려진 well-known 방법
- 변경 불가능한 Observable 데이터 홀더를 1개 만든다.
- UI 요소에 전달할(노출할) 용도
- Public 접근 제한자 (외부에서 접근가능)
- 변경 가능한 Observable 데이터 홀더를 1개 만든다.
- ViewModel 클래스 내부에서 사용할 용도
- private 접근 제한자 (외부에서 접근 불가능)
class NewsViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()
...
}
UI State 생산(업데이트)하는 예시 코드
- 앱 데이터를 서버로부터 가져와야 하는 경우
- 비동기 처리를 위해 viewModelScope를 시작
- 앱 데이터를 가져왔으면, 새로운 UI State 생산
: kotlin의 data class가 제공하는 copy()함수 사용 - 생산한 UI State를 변경 가능한(Mutable) Observable 데이터 홀더에 업데이트
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 State 사용(소비, Consume)하는 방법
UI 요소가 Observable 데이터 홀더 관찰하고 있기
- 관찰하고 있다가 Observable 데이터 홀더가 새로운 UI State 생산됨을 알려주면,
- ViewModel이 잡고 있는 변경 불가능한 데이터 홀더의 UI State에 접근해서 가져온 다음
- UI 요소에 연결해주면 된다.
UI 코드 예시
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
}
}
}
}
}
UI에서 UiState 객체의 스트림을 사용하려면 Observable 데이터 유형에 터미널 연산자를 사용한다.
LiveData의 경우 observe() 메소드를 사용하고 Kotlin Flow의 경우 collect()메서드를 사용한다.
UI State가 업데이트 되는 일반적인 과정
- 안드로이드 UI 계층 아키텍처에서 UI State는 보통 '원인(input) -> 처리(process) -> 결과(output)'의 과정을 거쳐 업데이트 된다.
UI State 는 화면에 보여질 최종적인 데이터이다. 사용자 이벤트나 서버 호출 등으로 기존 데이터에서 새로운 데이터로 업데이트 되는데, 이 때 비즈니스 로직이나 UI 로직등을 먼저 처리해주고 UI State를 업데이트 시킨 후 UI에 반영해주면 된다.
출처
UI 레이어 | Android 개발자 | Android Developers
[공식문서] 2021년 모던 앱 아키텍처 > UI 계층 (tistory.com)
'Android > Architecture' 카테고리의 다른 글
안드로이드 Clean Architecture - 예제 (Rxjava) (0) | 2023.05.27 |
---|---|
안드로이드 Clean Architecture - 개념 (0) | 2023.05.27 |
[Android] 앱 아키텍처 가이드 (4) - Domain Layer (0) | 2023.05.15 |
[Android] 앱 아키텍처 가이드 (3) - Data Layer (0) | 2023.05.01 |
[Android] 앱 아키텍처 가이드 (1) - 개요 (0) | 2023.04.12 |