Android App architecture: About the UI layer

안드로이드 App architecture: About the UI layer를 알아보겠습니다.



출처:
https://developer.android.com/topic/architecture/ui-layer


UI의 역할은 앱 data를 화면에 나타내는 것이고, 또한 사용자와 상호작용하는 첫 번째 지점입니다.

데이터의 변화를 항상 추적하여 반영하여야 합니다.

하지만, 보통 data layer에 있는 구조와 화면에 필요한 구조가 다릅니다. 예를 들어 두 군대의 data를 합쳐서 UI를 그려야 할 수도 있습니다.

UI layer는 앱 data를 UI로 표현할 수 있게 변화시키는 pipline(파이프라인)입니다.






⊙ A basic case study; 기본 사례 공부:

기사를 불러와 사용자에게 보여주는 앱을 생각해 봅시다.

• 읽을 수 있게 기사 제공.

• 카테고리 별로 기사 제공.

• 로그인 후, 기사 북마크 설정.

• 가능하다면 프리미엄 기능 제공.





⊙ UI layer architecture; UI layer 구조:

1. 앱 data를 읽어서 UI를 그리기 편한 data로 변환.

2. UI로 그리기 편한 data를 읽어서 UI 요소로 변환하여 사용자에게 보여줌.

3. 입력 event(이벤트)를 받아서 UI 요소와 결합 후 UI로 그리기 편한 data 최신화.

4. 1~3 과정을 필요에 따라 계속 반복.

위의 과정들은 아래의 작업과 개념을 포함합니다.

• UI state를 어떻게 정의할 것인지?

• Unidirectional data flow (UDF)가 UI state 관리에서 가지는 의미.

• UI state를 UDF 원리에 따라 추적되는 data로 어떻게 전달할 것인지?

• 추적되는 UI state를 어떻게 UI로 사용할 것인지?







⊙ Define UI state; UI state 정의:

위에서 예로든 기사의 경우 UI는 기사의 목록을 metadata(메타데이터)를 기반으로 보여줍니다. 이렇게 사용자에게 보여주는 정보가 UI state입니다.

다른 말로 하면 UI가 사용자가 보는 것이라면 UI state는 무엇을 보여줄지를 나타냅니다.






예를 들어 기사 앱의 NewsUiState data class는 이런 식이 되겠죠.





• Immutability; 불변성:

위의 코드에서 봤듯이 변하지 않는 val을 사용합니다. 이는 state를 읽을 때 현재 상태를 보장하기 위해서입니다.

따라서, UI에서 바로 UI state를 변경하면 안 됩니다. 이는 Single source of truth를 위반하여 multiple source of truth가 되며 불일치성 및 버그를 생성합니다.



예를 들어, NewsItemUiState를 Activity에서 변경한다면 data source의 값과 Activity에서 설정된 값이 충돌하게 됩니다. 따라서 immutability는 중요합니다.





• Naming conventions in this guide; 이 지침에서의 작명 규칙:

UI state class는 화면에서의 기능에 초점이 맞춰집니다.

functionality + UiState

예를 들어, news를 보여주는 state라면 NewsUiState라고 할 수 있습니다. 목록 안에 있는 news item을 보여주는 state라면 NewsItemUiState라고 짓습니다.





⊙ Manage state with Unidirectional Data Flow; 단방향 data 전달을 통한 state 관리:

State의 불변성에 대해서 말했지만, data는 특성상 자주 변화합니다. 즉, UI state 또한 자주 변해야 한다는 뜻입니다. 사용자의 상호작용 또는 다른 event들로 발생합니다.

이러한 event를 처리하기 위해 UI에 로직을 넣을 수도 있지만 UI 이름을 생각했을 때 이상한 상황입니다. data를 소유하고 생성하고 변형하는 행위를 하기 때문입니다. 또한, 테스트를 어렵게 만듭니다. 왜냐하면 결과 코드가 경계가 없이 강력하게 연결되어 있기 때문입니다.

이 항목에서는 Unidirectional Data Flow (UDF)를 사용하여 건강하게 각 역할을 나누는 법에 대해서 얘기합니다.



• State holders

UI state와 그에 필요한 로직을 가지고 있는 class(클래스)를 state holders라고 부릅니다. State holders의 크기는 그들이 관리하는 UI 요소에 따라 결정됩니다. bottom app bar에서 전체 화면 또는 navigation destination까지 다양합니다.



보통은 ViewModel을 사용합니다. 작은 앱의 경우 viewModel 하나만 있어도 충분합니다. 기사 앱에서는 NewsViewModel이 UI state를 생성하는 state holder입니다.



UI와 State holder가 상호 의존하는 방법은 여러 개가 있지만, 대략적으로 이해하자면, ViewModel에 event가 들어오고 state를 내보낸다고 생각하면 됩니다.






위의 패턴처럼 state 아래로 전달과 event 위로 전달하는 방식을 unidirectional data flow (UDF)라 합니다.

• ViewModel은 state를 보유하고 UI가 사용할 수 있도록 State를 보내줍니다. UI State는 ViewModel에서 앱 data로부터 변환됩니다.

• UI는 ViewModel에 사용자 event를 전달합니다.

• ViewModel은 사용자의 행위를 관리하고 state를 최신화합니다.

• 최신화된 state가 다시 UI로 전달되어 화면에 그려집니다.

• 위의 항목들은 state가 변경되는 event가 발생하면 반복합니다.



화면을 표시하기 위해 ViewModel은 repositories 또는 use case class를 사용하여 data를 가져와 UI state로 변환합니다.

기사 앱에서 북마크를 할 때 일어나는 일을 설명하는 그림입니다.







UDF를 사용하여 event를 처리하는지 알 수 있습니다.





• Types of logic; 타입의 로직:

기사 앱의 북마크 기능은 business 로직입니다. 왜냐하면 이것은 당신의 앱에 가치를 주기 때문입니다.

  - Business 로직은 앱 data를 제품에 적용하는 것입니다. 하나의 예는 북마크 기능입니다. business(비즈니스) logic(로직)은 보통 domain 또는 data layer에 존재해야 하고, UI에 있어서는 안됩니다.

  - UI behavior logic 또는 UI logic은 state의 변화를 어떻게 화면에 그릴까를 의미합니다. 예를 들면 안드로이드 Resource를 사용하여 올바른 글자를 화면에 표현하는 것, 사용자가 클릭 시 특정 화면으로 이동, toast나 snackbar 표출 등이 있습니다.



UI 로직은 보통 UI에 있어야 하는 Context 같은 UI 타입을 포함합니다. Context는 ViewModel에 있으면 안 됩니다. 만약 복잡한 UI 로직을 따로 처리하고 싶다면 간단한 class를 state holder로 만들면 됩니다. UI에서 만들어진 간단한 class는 Android SDK 의존성을 가져도 상관없습니다. 왜냐하면 UI의 생명주기를 따르기 때문입니다. ViewModel은 UI 보다 생명주기가 깁니다.



• Why use UDF?; 왜 UDf를 사용하는가?:

state를 한곳에서 관리할 수 있기 때문입니다. 이러한 분리로 UI가 정말 UI에만 집중할 수 있게 해줍니다. state 변화에 따라 UI를 최신화합니다.

추가적으로 UDF는 이러한 것들이 가능합니다.

  - Data 일관성. 하나의 소스에서만 UI에 영향을 줍니다.

  - 테스트가 쉬움. state가 UI와 독립적이기 때문입니다.

  - 유지 보수가 쉬움. state의 변화는 사용자 event나 data로부터 일어나기 때문입니다.






⊙ Expose UI state; UI state 전달:

UI state를 정의하고 어떻게 관리할지를 정하면 다음 할 일은 UI state를 전달하는 일입니다. 왜냐하면 UDF를 사용하기 때문입니다. 다르게 얘기하면, 여러 version의 state가 생성되기 때문입니다. UI state는 LiveData나 StateFlow로 추적됩니다. 따라서 ViewModel에 직접 확인하지 않고도 UI state를 최신화할 수 있습니다. 그리고 이러한 타입들은 항상 최신화된 값들만 남습니다. 따라서 설정이 변경된 뒤에 다시 UI state를 불러올 때 용이합니다.

Compose에서는 mutableStateOf나 snapshotFlow 같은 State APIs를 사용합니다.



이렇게 전달받은 UI state를 UI에 연결하면 됩니다.

UI state는 backing mutable stream으로 전달합니다.







위의 ViewModel 코드는 UI state를 내부적으로는 수정할 수 있지만, 전달하는 외부에서는 수정할 수 없게 만듭니다.

비동기 작업이 필요한 경우에는 viewModelScope를 사용하여 coroutine을 사용합니다.

try, catch를 사용하여 적절하게 UI state를 조정할 수 있습니다.

함수를 사용하여 UI state를 변경하는 것은 UDF의 유명한 적용 방식입니다.





• Additional considerations; 추가적인 고려 사항:

  - UI state는 관련된 것들을 다뤄야 한다. 이것은 불일치성을 줄여주고 코드의 이해를 쉽게 해줍니다. 예를 들어 기사 목록과 북마크의 개수를 서로 다른 UI state로 나눈다면 하나는 최신화되고, 하나는 되지 않는 상황에 놓일 것입니다. 하나의 UI state만 사용한다면 모두 최신 상태로 관리됩니다. 더욱이 business logic은 여러 소스의 병합이 필요할 수 있습니다. 예를 들어, 북마크 버튼을 프리미엄에 가입한 사람에게만 보여줘야 할 수도 있습니다. 그렇다면 아래처럼 정의할 수 있습니다.








북마크 버튼의 생성은 두 개의 다른 속성에서 비롯됩니다. business logic이 복잡해짐에 따라 하나의 UI state로 관리하는 것이 중요해집니다.



  - UI states: 하나의 UI state 또는 여러 개의 UI state? UI state를 하나만 사용할 것인지, 여러 개로 사용할 것인지는 위의 사례에서 설명하였습니다. 하나의 UI state가 가지는 가장 큰 이점은 편하고 data가 일관성 있다는 것입니다. 받는 쪽은 항상 최신의 정보를 가집니다. 그러나 ViewModel에 여러 개의 UI state가 적절할 때도 있습니다.

    = 연관되지 않은 data 타입: 어떤 UI는 독립적일 필요가 있습니다. 묶음 가격을 표시하는 UI state를 기사 UI state와 합친다면, 합쳤을 때 이점보다 단점이 더 큽니다. 기사 UI state가 묶음 가격 UI state보다 더 자주 업데이트되기 때문입니다.

    = UI state 차이점 발견: UI state에 있는 여러 값 중 하나만 변경되어도 UI state는 최신화가 일어납니다.  왜냐하면 view는 차이점 발견 기능이 없어 전달된 UI state가 변경된 최신의 상태인지 이전과 같은 상태인지 모릅니다. 모든 전달은 view를 최신화 시킵니다. 따라서 Flow의 distinctUntilChanged()를 사용하거나 라이브 데이터를 사용합니다. 






⊙ Consume UI state; UI state 수신

UI에서 연속적인 UI state를 수신하기 위해서 추적이 가능한 data 타입을 사용합니다. Livedata의 observe나 flow의 collect가 있습니다. 이러한 수신은 view의 lifecycle을 따라야 합니다. 왜냐하면 view가 보이지 않을 때에는 UI는 UI state를 추적하지 않아야 하기 때문입니다.

flow를 사용한다면, repeatOnLifecycle을 사용해야 합니다.

Stateflow는 이미 적용되어 있습니다.








• Show in-progress operations; 간단한 진행 중 작업:

불러오는 중을 표시하려면 boolean을 사용한 필드를 사용하면 됩니다.







• Show erros on the screen; 오류를 화면에 표시:

진행 중 표시와 비슷합니다. 왜냐하면 boolean으로 쉽게 표현할 수 있기 때문입니다. 그러나, 오류는 사용자에게 보여줄 오류 메시지를 포함하고 있습니다. 그러므로 진행 중과 같이 불러오는 중 또는 불러오는 중이 아님으로 구분되는 것이 아니라 오류 데이터를 포함한 data class로 만들어야 합니다.

예를 들어, 최신 기사를 불러오는 도중 오류가 발생한다면 하나 또는 여러 개의 메시지를 사용자에게 보여주어야 할 것입니다.






이러한 메시지는 snackbars 같은 UI 요소로 보여줍니다. 왜냐하면 UI event가 어떻게 생성되고 사용되는지와 관련 있기 때문입니다.





⊙ Threading and concurrency; 스레딩과 동시성:

ViewModel에서 일어나는 모든 작업은 Main-safe(메인 세이프) 여야 합니다. main-safe는 main thread에서 실행해도 안전하다는 뜻입니다. data, domain layer는 다른 thread에서 작동해야 하기 때문입니다.

만약 ViewModel에서 장기간 작업을 하게 된다면 data, domain layer처럼 background(백그라운드) thread를 사용해야 합니다.

Kotlin coroutines는 동시성을 위한 최상의 방법입니다. 그리고 Jetpack Architecture Components는 Coroutine을 사용할 수 있게 제작되었습니다.





⊙ Navigation; 화면 전환:

앱 화면 전환은 event 같은 수신으로 관리됩니다. 예를 들어 SignInViewModel에서 로그인이 진행되면 UI state는 isSignedIn이라는 field(필드)가 있고 true로 설정될 겁니다. 이러한 변경은 수신이 되어 화면전환에 사용됩니다.





⊙ Paging; 페이징:

Paging 라이브러리는 PagingData를 사용하여 UI를 그립니다. 왜냐하면 PagingData는 변화하는 내용을 보여주기 때문입니다. 다른 말로, 이것은 불변하는 타입이 아닙니다. 그래서 UI state에 이 불변하는 타입을 넣어서는 안됩니다. 대신에 ViewModel에 독립적인 UI state 전달 stream(스트림)을 만들어야 합니다.





⊙ Animations; 애니메이션;

부드러운 화면전환을 제공하기 위해 두 번째 화면에서 data가 불러와지기를 기다려야 합니다. View framework에서는 postponeEnterTransition()과 startPostponedEnterTransition()을 제공해 줍니다. 이러한 API는 두 번째 UI 요소가 다 그려질 때까지 기다려줍니다.



끝.




카테고리: Android




댓글

이 블로그의 인기 게시물

Python urllib.parse.quote()

Python OpenCV 빈 화면 만들기

tensorflow tf.random.uniform()

Android Notification with Full Screen

KiCad 시작하기 2 (PCB 만들기)

Android Minimum touch target size

Python bs4.SoupStrainer()

KiCad 시작하기 4 (기존 회로도 수정 및 추가)

음악 총보(Score), 파트보(Part)

tensorflow tf.expand_dims()