Android App architecture: State holders and UI state
안드로이드 App architecture: State holders and UI state를 알아보겠습니다.
출처:
https://developer.android.com/topic/architecture/ui-layer/stateholders
UI(유아이) layer(레이어) 지침에서 논의했던 unidirectional data flow (UDF, 단방향 data 흐름)은 UI State(상태)를 생산하고 관리하는 것을 의미합니다.
또한 UDF를 관리하는 특별한 class를 state holder(홀더)라고 합니다. 당신은 state holder를 ViewModel 또는 일반 class에 적용할 수 있습니다. 이 문서에서는 좀 더 가까이에서 state holders 공부하고 UI layer에서의 역할을 다룹니다.
이 문서의 마지막에서 당신은 앱의 state를 어떻게 UI layer에서 다루는지 이해하게 됩니다. 이것은 UI state의 생성 pipeline(과정)을 다룹니다. 당신은 아래의 사항들을 이해하고 알게 됩니다.
• UI layer에 존재하는 UI state의 type(타입) 이해
• UI layer에 있는 UI state의 logic(논리) type 이해
• ViewModel이나 일반 class 같은 state holder를 적정하게 선택하여 사용하는 방법 습득
⊙ Elements of the UI state production pipeline: UI state 생성 과정의 요소;
UI state와 logic이 UI layer를 정의합니다.
• UI state: UI 상태;
UI state는 UI를 설명하는 property(소유물)입니다. 여기에는 두 가지 종류의 UI state type이 있습니다.
- Screen UI state(화면 유아이 상태)는 화면에 표시를 위해 필요한 것입니다. 예를 들면, NewsUiState class(클래스)에는 뉴스 기사와 UI를 표현하기 위한 다른 정보들이 있을 겁니다. 이 state는 보통 다른 layer(레이어)와 연결되어 있습니다. 앱의 data를 포함하기 때문이죠.
- UI element state(유아이 요소 상태)는 UI 요소가 어떻게 화면에 표현될지에 대한 property의 본질입니다. UI element는 보이거나 숨겨지거나 특정한 글꼴, 글자 크기, 글자 색깔 등을 포함합니다. 안드로이드의 View(뷰)에서는 이 state를 원래부터 stateful 하게 관리합니다. methods(함수)를 노출하여 변경하거나 state에 query를 사용하기도 합니다. 예를 들면, TextView의 글자를 위한 get과 set methods가 있습니다. Jetpack Compose(젯팩 컴포즈)에서는 composable 밖에 존재합니다. 그리고 당신은 hoist(호이스트)를 통해 composable을 벗어나 composable을 부르는 함수나 state holder로 옮길 수 있습니다. 이것의 예시로는 Scaffold composable을 위한 ScaffoldState가 있습니다.
• Logic: 논리;
UI state는 정적인 property가 아닙니다. 앱의 data와 user events는 UI state를 항상 변화시킵니다. Logic은 변화를 결정합니다. 예를 들면, 어떤 부분의 UI state가 변화되었는지, 왜 변화된 건지, 언제 변화가 되어야 하는지 등입니다.
Logic은 business logic과 UI logic이 있습니다.
- Business logic: 앱 data를 위해 제품에 필요한 사항입니다. 예를 들면, 뉴스 앱에서 뉴스 보기 항목 안의 기사를 책갈피 설정하기 위해 버튼을 누르는 경우가 있습니다. 이 logic은 책갈피를 file 또는 database(데이터베이스)에 저장하는 것이고, 보통 domain(도메인)과 data layers에 존재합니다. state holder는 단지 domain과 data layers에서 노출하는 함수를 호출할 뿐입니다.
- UI logic: UI state를 어떻게 화면에 표시할지에 관한 사항입니다. 예를 들면, 카테고리를 선택한 user에게 적절한 검색 힌트를 제공해 주는 것, 목록에서 특정한 항목으로 스크롤 하는 것 또는 user가 버튼을 눌렀을 때, 화면 전환을 하는 것 등이 있습니다.
⊙ Android lifecycle and the types of UI state and logic: 안드로이드 생명주기와 UI state의 type과 logic;
UI layer는 2 개의 부분으로 나눠집니다. 하나는 UI lifecycle(생명주기)에 종속적이고, 다른 하나는 아닙니다. 이러한 구분은 각 부분에서 data sources가 사용 가능하게 만들기 때문에 다른 type의 UI state와 logic이 필요합니다.
• UI lifecycle independent: UI 생명주기와 독립적인 관계;
이 부분의 UI layer는 data 생성 layers(data 또는 domain layers)와 상호작용하며, business logic에 의해 결정됩니다. Lifecycle, 설정 변경 그리고 Activity 재생성은 UI state가 활성화되어 있을 때에는 UI에 영향을 줍니다. 그러나 생성된 data에는 영향을 주지 않습니다.
• UI lifecycle dependent: UI 생명주기와 종속적인 관계;
이 부분의 UI layer는 UI logic과 lifecycle의 영향 또는 설정 변경과 상호작용합니다. 이러한 변경은 UI layer에서 data 읽기에 영향을 줍니다. 그리고 그러한 결과로 생긴 state는 오직 해당 lifecycle이 활성화되었을 때에만 적용됩니다. 예를 들면, runtime(실행 중) permissions(권한 요청)과 설정과 연관된 현지화된 글자 resources 등이 있습니다.
위의 글은 아래의 표로 정리 요약됩니다.
• The UI state production pipeline: UI state의 생성 과정;
UI state 생성 과정은 UI state를 만드는 것에서부터 시작합니다. 이러한 과정은 이전에 정의된 logic types가 포함됩니다. 그리고 UI에서 필요한 것에 완전히 종속됩니다. 몇몇 UIs는 UI Lifecycle independent와 UI Lifecycle dependent 부분 모두에서 이점을 얻을 수 있거나 둘 중 하나에서 얻거나 하나도 얻지 못할 수 있습니다.
아래의 UI layer 생성 과정의 예시를 보시죠.
- Ui state를 UI 안에서 생성하고 관리합니다. 예를 들면, 간단하고 재사용 가능한 계수기.
- UI logic -> UI. 예를 들면, 맨 위로 버튼을 숨기고 보여주는 기능.
- Business logic -> UI. User의 사진을 화면에 보여주는 UI 요소.
- Business logic -> UI logic -> UI. 주어진 UI state를 이용하여 스크롤 한 화면에 적절한 정보 표시하기.
UI state 생성 과정에서 두 가지 logic이 적용된 사례에서는 business logic이 반드시 UI logic 전에 적용되어야 합니다. business logic을 UI logic 이후에 적용하게 된다면, business logic이 UI logic에 종속됩니다. 이어지는 영역에서는 logic types와 각자의 state holders의 심도 있는 관점에서 왜 이것이 문제가 되는지 알아봅니다.
⊙ State holders and their responsibilities: State holders와 그들의 역할;
State holder의 역할은 앱이 읽을 수 있게 state를 저장하는 것입니다. logic이 필요한 경우에는 logic이 있는 곳에 중간 연결자가 되어 data sources에 접근할 수 있게 해줍니다. 이러한 방법은 state holder가 적절한 data source를 logic과 연결시켜줍니다.
이러한 방식은 아래의 이점이 있습니다.
• Simple UIs: 간단한 UIs; UI는 단지 자신의 state에 연결될 뿐입니다.
• Maintainability: 유지 보수가 용이하다; state holder에 정의된 logic은 UI 변경 없이 반복이 가능합니다.
• Testability: 테스트하기 쉽다; UI와 state 생성 logic은 독립적으로 테스트할 수 있습니다.
• Readability: 읽기 쉽다; Code를 읽을 때에는 UI 표현 code와 UI state 생성 code와 구별하기 쉽습니다.
크기와 scope(영역) 상관없이 모든 UI element는 자신의 state holder와 1:1 관계를 가집니다.
더 나아가, state holder는 UI state를 변경하거나 변경하게 될 user의 행위를 받아들이고 처리하는 역할을 합니다.
• 메모:
State holder는 필수적으로 필요한 것이 아닙니다. 간단한 UIs의 경우 presentation(표현) code에 logic이 포함될 수 있습니다.
• Types of state holders: state holders types:
UI state와 logic처럼 state holders에도 두 가지 type이 있습니다. UI lifecycle 과의 관계에 따라 결정됩니다.
- The business logic state holder: 비즈니스 logic state holder;
- The UI logic state holder: UI logic state holder;
이어지는 영역에서는 state holders의 types를 자세히 다룹니다. 먼저, business logic state holder를 알아보겠습니다.
• 메모:
만약 UI logic state holder가 data와 domain layers의 정보와 연관되어 있다면, 이것은 business logic state holder로 이전해야 합니다. 왜냐하면 business logic state holder는 UI logic state holder보다 더 길게 살아남고 UI lifecycle과 독립적입니다.
⊙ Business logic and its state holder: 비즈니스 logic과 state holder;
Business logic state holders는 user envet와 data 또는 domain layers의 data를 화면의 UI state로 변환하는 역할을 합니다. 좋은 user 경험을 주기 위해서 Android lifecycle과 앱 설정 변경을 고려하는 business logic을 사용하는 state holders는 아래의 속성을 가집니다.
• Produces UI State: UI State 생성; Business logic state holders는 UIs를 그리기 위한 UI state를 생성합니다. 이 UI state는 자주 user events를 처리하거나 domain이나 data layers에서 data를 읽어오는 역할을 합니다.
• Retained through activity recreation: activity 재생성 시에도 유지; Business logic state holders는 Activity 재생성에도 state와 state 생성 과정이 user의 끊김 없는 경험을 위해 유지되어야 합니다. 프로세스가 종료되는 상황같이 유지가 안되고 재생성되는 경우, state holder는 손쉽게 마지막 state를 재생성할 수 있게 만들어 user 경험을 동일하게 만들어야 합니다.
• Possess long lived state: 오래 살아남는 state를 가짐; Business logic state holders는 보통 navigation(화면 전환) destination(목적지)을 위한 state를 관리합니다. 따라서 navigation graph(그래프)에서 제거되기 전까지 화면 전환 변경에도 state를 유지합니다.
• Is unique to its UI and is not reusable: UI에 유일하게 존재하고, 재사용이 불가능; Business logic state holders는 보통 특정 앱 기능에 사용할 state를 제공합니다. 예를 들면, TaskEditViewModel 또는 TaskListViewModel이 있습니다. 이렇게 앱 기능과 관련되어 있습니다. 같은 state holder는 여러 기기에서도 같은 동작을 지원합니다. 예를 들어, 모바일, TV 그리고 태블릿의 앱들은 같은 business logic state holder를 공유합니다.
메모:
Business logic state holder는 보통 ViewModel instance(인스턴스)를 사용합니다. 왜냐하면, ViewModel instances는 위에 설명한 것들을 지원해 줍니다. 특히, Activity 재생성에도 살아남습니다.
예를 들어, Now in Android에서 작성자 navigation destination 화면을 봅시다.
Business logic state holder로 AuthorViewModel이 사용되고, UI state를 생성합니다.
위에서 언급한 Property를 한 번 봅시다.
• Produces AuthorScreenUiState
AuthorViewModel는 AuthorsRepository와 NewsRepository에서 data를 읽고, AuthorScreenUiState를 생성합니다. 또한, Author를 follow 하거나 unfollow 하는 business logic을 AuthorsRepository로 보내는 역할도 하고 있습니다.
• Has access to the data layer
AuthorsRepository와 NewsRepository의 instance는 constructor(생성자)에 추가되어 Author를 follow 하는 business logic이 추가됩니다.
• Survives Activity recreation
ViewModel이 사용되었기 때문에, 빠른 Activity 재생성에도 살아남습니다. 만약 프로세스가 종료되는 상황이라면 SavedStateHandle object에 data layer에서 다시 불러오기 위한 최소한의 정보를 저장할 수 있습니다.
• Possesses long lived state
ViewModel은 navigation graph에 포함됩니다. 그래서 author destination이 nav graph에서 사라지더라도 uiState StateFlow는 memory(메모리)에 남아 있습니다. StateFlow의 추가적인 장점으로는 앱의 business logic이 lazy(게으른) 하게 state를 생성합니다. 왜냐하면 UI state에서 수집을 해야지만 생성되기 때문입니다.
• Is unique to its UI
AuthorViewModel은 author navigation destination에만 적용 가능하고, 다른 곳에서는 재활용이 불가합니다. navigation destination을 뛰어넘는 business logic이 필요하다면 해당 logic은 data 또는 domain에 캡슐화되어 존재해야 합니다.
• 메모:
ViewModel은 destination-level UIs에만 사용해야 합니다. 당신은 재사용 가능한 당신의 UI에 이것을 사용해선 안됩니다. 예를 들면, 검색바나 Chip groups가 있습니다. 이러한 경우에는 일반 classes를 사용하는 게 더 적합합니다.
! 경고:
ViewModel을 하위 composable(컴포저블) 함수로 전달하지 마세요. ViewModel type과 결합하게 됩니다. 이것은 테스트, 재사용 그리고 미리 보기가 어렵게 됩니다. 그리고 ViewModel instance에 대한 Single Source Of Truth (SSOT)가 무시됩니다. 아래로 전달한 ViewModel은 state가 변할 때마다 모든 composables가 불러와집니다. 이것은 버그를 만들고 debug(디버그)를 어렵게 만듭니다. 대신, UDF를 따르는 것이 좋습니다. 그리고 필요한 state만 아래로 전달하는 것이 좋습니다. 추가적으로, ViewModel의 composable SSOT가 있는 곳으로 event를 상위로 보내야 합니다. 이것이 ViewModel의 event와 함수를 일관되게 관리하고 SSOT를 따르는 것입니다.
• The ViewModel as a business logic state holder: Business logic state holder로써 ViewModel;
ViewModels의 장점은 Android 개발자가 적절하게 business logic에 접근할 수 있고, 화면에 앱 data가 잘 표현될 수 있게 해주는 것입니다. 이러한 이점들은 아래의 것들을 포함합니다.
- ViewModels에서 실행된 기능은 configuration(설정) 변경에도 계속 진행
- Navigation 과의 통합
= Navigation은 화면이 back stack(뒤로 가기 목록)에 있을 때 ViewModels를 caches(캐시) 합니다. 이것은 이전 화면으로 돌아갔을 때, 불러와진 data가 보존됨을 의미합니다. 이것은 composable 화면의 lifecycle을 따르는 state holder를 사용하면 더 어렵습니다.
= ViewModel은 back stack에서 사라질 때, 정리됩니다. 이것은 당신의 state가 자동으로 정리된다는 뜻입니다. 이것은 새로운 화면으로 가거나 configuration 변경이나 다른 이유들로 composable 화면이 버려질 때와는 다른 상황입니다.
- Hilt 같은 다른 Jetpack libraries와의 통합
• 메모:
만약 ViewModel의 이점이 당신의 경우에 맞지 않는다면, 다른 방법으로 ViewModel의 역할을 일반 state holder class로 이전할 수 있습니다.
⊙ UI logic and its state holder: UI logic과 state holder;
UI logic은 UI가 생성한 data를 처리하는 logic을 포함합니다. 이것은 UI 요소의 state 또는 permissions API와 Resources 같은 UI data source 일 수 있습니다. UI logic을 다루는 state holders는 아래의 속성을 가집니다.
• UI state를 생성하고 UI 요소 state를 관리
• Activity 재생성 시에 보존되지 않음; UI logic을 가진 state holders는 보통 UI에서 만든 data source에 종속적입니다. Configuration 변경에도 이러한 정보를 지키려고 노력하게 되면, memory leak(누출)이 발생하게 됩니다. 만약 state holders가 data를 configuration 변경에도 보존하려고 한다면, Activity 재생성에도 유지되는 다른 component(요소)로 전달해 줘야 합니다. Jetpack Compose를 예로 들면, UI 요소 states는 remembered 함수를 사용하여 생성합니다. 그리고 Activity 재생성에도 보존해야 하는 것들은 rememberSaveable을 사용하게 됩니다. 이러한 예시는 rememberScaffoldState()와 rememberLazyListState()에도 포함되어 있습니다.
• UI에 scoped된 sources of data를 참조; lifecycle APIs와 Resources 같은 Sources of data는 동일한 UI lifecycle에서 안전하게 UI logic에서 참조하여 읽을 수 있습니다.
• 다양한 UIs에서 재사용 가능; 다양한 UI logic state holder의 instance를 앱의 여러 부분에서 재사용 가능합니다. 예를 들면, chip group의 user 입력 events를 다루는 state holder는 검색 화면에서 필터 chips에 동일하게 사용될 수 있습니다. 그리고 또한, 이메일의 "to"(누구에게) 항목에도 사용될 수 있습니다.
UI logic state holder는 보통 일반 class를 사용합니다. 왜냐하면 UI 자신이 UI logic state holder의 생성을 책임지고 UI 자신과 UI logic state holder의 lifecycle을 동일하게 할 수 있기 때문입니다. Jetpack Compose 예를 들면, state holder는 Composition의 일부분이며, Composition의 lifecycle을 따르게 됩니다.
• 메모:
일반 class state holders는 UI logic이 너무 복잡하여 UI에서 분리해야 할 때 사용합니다. 그렇지 않다면, UI logic은 UI 안에서 정의될 수 있습니다.
아래의 Now in Android sample은 위의 설명을 잘 이해할 수 있게 돕습니다.
Now in Android sample(샘플)은 화면 크기에 따라 bottom app bar와 navigation rail을 보여줍니다. 작은 화면은 bottom app bar을 사용하고, 큰 화면은 navigation rail을 보여줍니다.
NiaApp composable 함수에서 적절한 navigation UI를 결정하는 logic은 business logic 과는 상관없습니다. 따라서 일반 class state holder인 NiaAppState에서 관리합니다.
위의 NiaAppState 예시 코드에서 주목할 만한 것들이 있습니다.
• Activity 재생성 시, 보존되지 않음; NiaAppState는 remembered를 사용하여 불러옵니다. 불러오는 함수 이름은 작명 규칙에 따라 rememberNiaAppState로 지었습니다. Activity가 재생성되면 이전에 있던 instance들은 모두 사라지고 새로운 Activity의 configuration으로 다시 생성됩니다. state holder에 사용된 dependencies는 새로운 것이거나 이전에 만들어졌던 것일 수 있습니다. 예를 들어, rememberNiaAppState 안에 있는 rememberNavController()가 rememberSaveable을 사용한다면, Activity 재생성에도 살아남습니다.
• UI에 scoped된 sources of data를 참조; navigationController, Resources 그리고 다른 비슷한 lifecycle scoped types는 NiaAppState에 안전하게 사용됩니다. 왜냐하면 그들은 같은 lifecycle을 가지기 때문입니다.
• 메모:
Plain state holder classes는 재사용 되는 UI에 추천됩니다. 예를 들면, search bars 또는 chip groups가 있습니다. 이런 경우에는 ViewModel은 사용해선 안됩니다. 왜냐하면 화면 전환과 business logic을 다루는 데에 좋기 때문입니다.
⊙ Choose between a ViewModel and plain class for a state holder: state holder로 사용할 ViewModel과 pain class 중 하나 선택;
위의 영역에서 설명하듯이 ViewModel과 plain class state holder은 UI state에 사용되는 logic과 logic이 다루는 data에 따라 결정됩니다.
• 메모:
대부분의 앱은 가능하면 UI 내부에 UI logic을 넣습니다. 아니라면, plain class state holders로 뺍니다. 이것은 간단한 앱에는 괜찮습니다. 하지만, 다른 상황에서는 logic을 UI 외부의 plain class state holder로 빼는 것이 가독성에 좋습니다.
요약하자면, 아래의 도식은 UI State 생성 과정에서 state holders의 위치를 보여줍니다.
궁극적으로, state holders를 사용하는 UI state는 이것이 소비되는 곳과 가까이에 위치해야 합니다. 공식적이진 않지만, 당신은 소유권을 가지면서도 state를 낮게 유지하려고 노력해야 합니다. 만약 당신이 business logic에 접근할 필요가 있고, UI state가 화면 전환이나 Activity 재생성에도 유지되기를 원한다면, ViewModel이 가장 좋은 선택지이고 business logic state holder로 사용하기 적합합니다. 짧게 사용되는 UI state와 UI logic은 사용되는 UI의 lifecycle에 종속적인 plain class를 사용합니다.
⊙ State holders are compoundable: State holders는 혼합할 수 있다;
State holders는 다른 state holder에 종속될 수 있습니다. 이렇게 하기 위해서는 합치려는 state holder의 생존 기간이 동일하거나 짧아야만 가능합니다. 예시가 있습니다.
• UI logic state holder는 다른 UI logic state holder에 종속될 수 있다.
• 화면 level(수준)의 state holder는 UI logic state holder에 종속될 수 있다.
아래의 코드는 Compose의 DrawerState가 다른 내부 state holder인 swipeableState에 종속된 것을 보여줍니다. 그리고 앱의 UI logic state holder가 DrawerState에 어떻게 종속하는지도 보여줍니다.
! 주의:
주어진 화면 level state holders는 화면 또는 부분적인 복잡한 business logic을 다룹니다. 이 뜻은 화면 level의 state holder가 다른 화면 level의 state holder에 종속되는 것이 이치에 맞지 않다는 뜻입니다. 만약 당신이 이러한 상황에 있다면, 당신에게 필요한 화면과 state holders를 고려해 보시길 바랍니다.
state holder의 잘못된 생명주기를 적용한 사례는 UI logic state holder가 화면 level state holder에 종속되는 것입니다. 이것은 짧은 생명 주기의 state holder의 재사용성을 헤치고 필요보다 많은 logic과 state에 접근하게 만듭니다.
만약 짧은 생명 주기의 state holder가 긴 생명 주기의 state holder의 정보가 필요하다면, 전체 state holder가 아닌, 필요한 정보만 parameter로 전달해 줘야 합니다. 아래의 코드로 예를 들면, UI logic state holder class가 ViewModel을 전달받는 것이 아닌, ViewModel에서 필요한 것만 parameters로 받습니다.
카테고리: Android
댓글
댓글 쓰기
궁금한 점은 댓글 달아주세요.
Comment if you have any questions.