Android App architecture: UI State production

안드로이드 App architecture: UI State production을 알아보겠습니다.



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


현대의 UIs(유아이)는 static(정적) 하지 않습니다. UI의 state(상태)는 user(사용자)와 상호작용하며 변화하거나 app(앱)이 새로운 data(데이터)를 불러올 때 변화합니다.



이 문서에서는 UI state를 생성과 관리하는 지침을 알려드립니다. 문서의 마지막에서는 당신은 이러한 것들을 알게 됩니다.

• UI state를 생성할 때 어떤 APIs를 사용해야 하는지를 알게 됩니다. state holder에서 state가 어떤 source(출처)의 성질로 변화하는지에 따라 다릅니다. Unidirectional(단방향) data flow(흐름) 따릅니다.

• UI state의 생성이 시스템 자원의 어디에서 관리되어야 하는지 알게 됩니다.

• 소비하는 UI에 어떻게 UI state를 노출해야 하는지 알게 됩니다.



기본적으로 state 생성은 변경 사항을 UI state에 점진적으로 적용하는 것입니다. State는 항상 존재하고, events(이벤트)의 결과로 변합니다. 각기 다른 events와 state는 아래의 표로 요약됩니다.





간단히 위의 요약을 외울 수 있는 방법이 있습니다. staet는 존재하고, events 발생한다입니다. 아래의 도식은 state가 events에 의해 변하는 것을 시간 순서대로 시각화 한 것입니다. 각 event는 적절한 state holder에서 처리되어야 하고, 결과는 state를 변경시켜야 합니다.





Events는 이렇게 발생합니다.

• Users: 앱의 UI와 상호작용

• Other sources of state change: state change의 다른 source; UI, domain(도메인) 또는 data layers(레이어) 앱 data를 표현하는 APIs. 예를 들면, snackbar(짧은 메시지 표시) 시간 초과 events, use cases(유즈 케이스) 또는 repositories(저장소) 각각에서 발생하는 event 등이 있습니다.





⊙ The UI state production pipeline: UI state 생성 과정;

안드로이드 앱의 state 생성은 처리 과정 요소로 볼 수 있습니다.



• Inputs: 입력; state 변경의 sources. 아마도 아래의 것들일 겁니다.

  - 내부에서 UI layer로. "할 일" 앱에서 일정의 제목을 추가하는 것과 같은 user event입니다. 또는 UI state에서 UI logic(논리)에 접근하기 위해 제공되는 APIs 사용하는 것입니다. 예를 들면, Jetpack Compose(컴포즈)에서 DrawerState의 open 함수를 부르는 것입니다.

  - 외부에서 UI layer로. Domain과 data layers에서 UI state의 변경을 일으키는 경우입니다. 예를 들면, NewsRepository에서 뉴스 불러오기를 완료한 경우 또는 다른 events로 발생하는 경우가 있습니다.

  - 위의 두 가지 경우가 섞인 상태.



• State holders: Business logic과 UI logic이 state 변경을 일으키고, user의 event를 처리하여 UI state를 생성합니다.



• 출력: 앱이 화면에 표시할 수 있는 정보를 가진 UI state는 사용자에게 필요한 정보를 제공합니다.





⊙ State production APIs: State 생성 APIs;

state를 생성하는 2 개의 주요한 APIs가 있습니다. 생성 과정 중 당신이 어디에 있는지에 따라 다릅니다.


• Input: 입력; asynchronouse(비동기) APIs를 사용해야 합니다. UI thread(스레드)에서 벗어나 UI 버벅임을 없애야 합니다. 예를 들면, Kotlin의 Coroutines 또는 Flow. Java의 RxJava 또는 Callback(콜백)이 있습니다.



• Output.: 출력; 추적 가능한 data holder APIs를 사용하여 state가 변경될 때, UI를 없애고 다시 화면에 표시할 수 있어야 합니다. 예를 들면, StateFlow, Compose State 또는 LiveData가 있습니다. 추적 가능한 data holders는 UI가 항상 UI state를 보유하고 화면에 표시된다는 것을 보장합니다.




위의 둘 중에 output을 위한 추적 가능한 API를 사용하는 것보다 input을 위한 asynchronouse API를 선택하는 것이 state 생성에 큰 영향을 줍니다. input은 생성과정에 어떠한 처리를 적용할 것인지 말하고 있기 때문입니다.







⊙ State production pipeline assembly: State 생성 과정 모음;

다음 영역에서는 state 생성 기술 중 다양한 inputs에 최고인 것을 다루고 여기에 맞는 output APIs도 다룹니다. 각각의 state 생성 과정은 inputs와 outputs를 합치는 것이고 다음을 충족해야 합니다.


• Lifecycle aware: 생명주기 포착; UI가 더 이상 보이지 않거나 활동하지 않을 때, state 생성 과정은 소비되는 곳이 없어야 합니다. 명확하게 필요한 상황이 아니라면 말이죠.



• Easy to consume: 사용하기 간편; UI는 UI state를 쉽게 화면에 구현할 수 있어야 합니다. state 생성 과정의 output을 고려하는 것은 다양한 View(뷰) APIs를 고려한다는 뜻입니다. View system이나 Jetpack Compose가 해당됩니다.







• 메모:

다음에 오는 영역은 Kotlin과 jetpack Compose를 활용한 코드입니다. 그러나 이 지침은 Kolin과 Java의 비슷한  다른 APIs에도 적용 가능합니다.







⊙ Inputs in state production pipelines: State 생성 과정의 inputs;

State 생성 과정의 inputs는 state를 변경하는 source를 제공합니다.



• 한 번만 실행하는 작업의 경우 synchronous(동기) 또는 asynchronouse를 사용합니다. 예를 들면 suspend 함수가 있습니다.

• Stream(흐름) APIs, 예를 들면 Flows.

• 위의 둘 다.



다음에 오는 영역은 위의 inputs를 어떻게 state 생성 과정으로 모으는지 설명합니다.



• One-shot APIs as sources of state change: One-shot APIs로 state를 변경하기:

MutableStateFlow API는 추적 가능하고, 변경 가능한, state를 저장하는 기능을 가집니다. Jetpack Compose에서는 Compose text API를 사용할 때, mutableStateOf 사용을 고려합니다. 두 APIs는 안전한 atomic(원자) 최신화가 가능합니다. 최신화가 synchrounous든 asynchronouse든 무조건 최신화를 진행합니다.



예를 들면, 주사위를 굴리는 간단한 앱에서 state 최신화를 생각해 봅시다. 매번 굴릴 때마다 user는 Random.nextInt() 함수를 synchronouse 하게 실행합니다. 그리고 결과는 UI state에 기록됩니다.



StateFlow









Compose State






  - Mutating the UI state from asynchronous calls: 비동기에서 UI state 변경;

Asynchronous 결과로 state 변경이 필요한 경우, 적절한 CoroutineScope를 사용하여 Coroutine을 실행해야 합니다. 이것은 CoroutineScope가 취소되었을 때, 작업도 같이 취소할 수 있게 합니다. State holder에서 suspend(연기된) 함수의 결과를 observable(추적 가능한) API를 사용해서 UI state를 노출합니다.



예를 들면, Architecture sample 앱에서 AddEditTaskViewModel를 볼 수 있습니다. suspending 함수인 saveTask() 함수가 일을 asynchronously 하게 저장할 때, MutableStateFlow에 있는 update 함수가 UI state에게 변화를 전파합니다.





StateFlow






Compose State






  - 메모:

AAC ViewModel의 viewModelScope로 실행된 Coroutines의 경우 완료, 예외 또는 다른 것들로 실행됩니다. 이것은 Coroutines가 명확하게 취소되거나 ViewModel이 없어지지 않는다면, UI가 보이느냐 아니냐에 따라 발생이 결정됩니다. 이것은 보통 짧게 실행되는 것에 좋습니다. 당신은 viewModelScope에서 5초 또는 그 이상되는 것을 실행하지 말아야 합니다. 대신, deferred(연기) 시켜 enqueue(대기열에 넣다) 하거나 긴 작업을 하는 WorkManager를 써야 합니다.





  - Mutating the UI state from background threads: 백그라운드에서 UI state 변경;

UI state 생성은 Coroutines의 main dispatcher로 실행하는 것이 선호됩니다. 이것은 아래 코드 예시에서 withContext 밖 부분을 가리킵니다. 그러나 당신이 UI state 최신화를 다른 백그라운드 context(컨텍스트)에서 해야 할 필요가 있다면, 당신은 아래의 APIs를 사용할 수 있습니다.

    = withContext 함수 사용하여 다른 concurrent(동시) context에서 실행.

    = MutableStateFlow의 update 함수 사용.

    = Compose State에서 Snapshot.withMutableSnapshot을 사용하여 atomic 최신화를 보장하여 concurrent context에서 State 변경.



예를 들면, 아래의 DiceRollViewModel에서 SlowRandom.nextInt()는 계산이 많이 필요한 suspend 함수로 CPU bound Coroutine에서 부를 필요가 있습니다.



StateFlow








Compose State






  - 메모:

만약 모든 coroutines 실행되는 것이 다른 context에서 필요하다면, 당신은 viewModelScope.launch(defaultDispatcher()) {}을 직접적으로 부를 수 있습니다.



  ! 경고:

Compose state를 UI thread가 아닌 곳에서 Snapshot.withMutableSnapshot {} 없이 최신화  할 시, state 생성과정에서 오동작이 있을 수 있습니다.







• Stream APIs as sources of state change: 연속 APIs로 state 변경;

Streams(흐름)으로 state가 여러 값으로 인해 계속해서 변할 때, 모든 sources를 하나로 합쳐 output을 만들어 직접적으로 state 생성합니다.



Kotlin Flows를 사용할 때, 당신은 combine 함수로 이를 구현할 수 있습니다. Now in Android에서 이 예시를 볼 수 있습니다. InterestsViewModel을 보시죠.







  - 메모:

당신은 stateIn을 사용하여 Flow를 StateFlow로 변경할 수 있습니다. StateFlow는 UI state를 위한 observable API입니다.





stateIn은 StateFlows를 만들고, StateFlows는 UI가 activity의 state 생성 과정을 잘 관리하게 해줍니다. UI가 보일 때에만 실행할 수 있습니다.



    = SharingStarted.WhileSubscribed() 사용은 생성 과정이 lifecycle-aware 동작을 알아채게 만들고 UI가 보일 때만 실행되게 만듭니다.

    = SharingStarted.Lazily 사용은 생성 과정이 user가 UI에 다시 돌아올 수 있는 한, 계속 실행됩니다. UI가 backstack에 있거나 다른 화면에 보이지 않는 상황을 말합니다.





아직 적용되지 않은 stream을 합치는 경우, Kotlin의 Flows 같은 stream APIs는 merging, flattening 같은 고급 형태 변환 도구들을 제공합니다. 그리고 이 도구들은 streams를 UI state로 변경시킵니다.





  - 핵심 부분:

대부분의 경우, combine은 stream APIs에서 state 생성에 가장 현명한 방법입니다.





  - One-shot and stream APIs as sources of state change: 한 번 실행과 stream으로 state 변경;

one-shot(한 번만 실행)과 stream으로 state가 변경되는 경우, streams는 제약 조건을 정의할 수 있습니다. 그러므로 one-shot 함수를 streams APIs로 변경하거나 one-shot 결과를 streams에 넣어서 위의 stream 영역에서 설명한 데로 처리를 계속 진행합니다.



Flows를 사용한다는 뜻은 하나 또는 여러 개의 private backing MutableStateFlow instances를 만들어서 state change를 한다는 뜻입니다. 당신은 Compose state를 사용한다면 snapshot flows를 사용할 수 있습니다.



아래의 architecture-samples에 있는 TaskDetailViewModel을 보시죠.





StateFlow








Compose State





  - 메모:

Compose State는 snapshotFlow {} API를 사용해서 변환시킵니다. 다른 예제인 Now In Android의 ForYouViewModel을 보세요.





⊙ Output types in state production pipelines: State 생성 과정의 Output types



UI state를 위한 output API 선택은 UI가 구현되는데, 어떠한 API를 사용하는지에 따라 결정됩니다. 안드로이드 앱에서 Views를 사용할지, Jetpack Compose를 사용할지 결정할 수 있습니다. 결정에 영향을 주는 것에는 다음이 있습니다.

• State를 lifecycle과 결합하여 읽기.

• State holder에서 여러 곳으로 노출하기.



아래의 표는 state 생성 과정에서 어떤 API를 사용해야 하는지에 대한 요약이 있습니다. input과 consumer를 잘 보고 선택하세요.






⊙ State production pipeline initialization: State 생성 과정 초기화;

State 생성 과정 초기화에는 생성 과정의 초기 설정이 포함됩니다. 생성 과정에 필수적인 초깃값을 제공하는 것도 포함됩니다. 예를 들면, 뉴스 기사 상세 페이지를 보기 위한 id 값 또는 asynchronous로 불러오기 등이 있습니다.



당신은 state 생성 과정을 lazily(게으르게) 하게 불러올 필요가 있습니다.  이는 시스템 자원을 적절하게 보존할 수 있게 해줍니다. 이 말뜻은 consumer가 output이 발생하기 전까지 기다린다는 뜻입니다. Flow APIs는 이것을 stateIn의 started 전달 인자를 통해 구현 가능합니다. 만약 이러한 것이 불가능하다면, 아래의 코드처럼 state 생성 과정을 명확하게 실행하는 idempotent(멱등) 한 initialize() 함수를 사용합니다.







! 경고:

init 블록 또는 ViewModel constructor(생성 블록)에 asynchronous 한 작업을 실행하는 것을 피해야 합니다. Asynchronous 한 작업은 object(오브젝트)를 생성하는 동안 side effect가 발생해서는 안 됩니다. 왜냐하면 asynchronous 코드는 object가 완전히 초기화되기 전에 읽거나 쓰기 작업이 일어날 수 있기 때문입니다. 또한 이것은 object의 유출을 일으키며, 오류를 진단하고 찾기 어렵게 만듭니다. 이것은 특히 Compose State를 사용할 때 더 중요합니다. ViewModel이 Compose State를 가지고 있을 때, Compose State를 init 블록에서 Coroutines를 사용해서 변경해서는 안 됩니다. 그렇지 않다면, IllegalStateException이 발생할 수 있습니다.





끝.



카테고리: Android



댓글

이 블로그의 인기 게시물

Python urllib.parse.quote()

Python bytes.fromhex()

Python OpenCV 빈 화면 만들기

Android Notification with Full Screen

Android Minimum touch target size

Android Compose Instrumentation test to unit test

KiCad 시작하기 7 (FreeRoute 사용하기 2)

tensorflow tf.expand_dims()

딩기 요트 명칭

Android AVD Ram size change