관리 메뉴

오늘도 더 나은 코드를 작성하였습니까?

State Holder 및 UI State 본문

카테고리 없음

State Holder 및 UI State

hik14 2022. 12. 8. 19:22

UDF 관리를 State Holder라고 하는 특수 클래스에 위임할 때의 이점도 강조합니다.

State Holder는 ViewModel 또는 일반 클래스를 통해 구현할 수 있습니다

 

UI 레이어에서 애플리케이션 상태를 관리하는 방법, 즉 UI 상태 생성 파이프라인을 이해할 수 있습니다. 다음 사항을 이해하고 파악할 수 있습니다.

  • UI 레이어에 있는 UI state type 이해
  • UI 레이어의 이러한 UI state에서 작동하는 로직 유형 이해
  • State Holder의 적절한 구현(예: ViewModel 또는 간단한 클래스)을 선택하는 방법 파악

UI 상태 생성 파이프라인의 요소

UI State와 이를 생성하는 로직이 UI 레이어를 정의

UI 상태

UI 상태는 UI를 설명하는 속성입니다. UI 상태에는 두 가지 유형이 있습니다.

  • 화면 UI 상태: 화면에 표시해야 하는 항목(데이터)입니다. 예를 들어 NewsUiState 클래스에는 UI를 렌더링하는 데 필요한 뉴스 기사와 기타 정보가 포함될 수 있습니다. 화면 ui 상태는 앱 데이터를 포함하므로 대개 계층 구조에서 다른 레이어(repository..etc)에 연결됩니다.
  • UI 요소 상태: 렌더링 방식에 영향을 주는 UI 요소에 고유한 속성을 나타냅니다. UI 요소는 표시하거나 숨길 수 있으며 특정 글꼴이나 글꼴 크기, 글꼴 색상을 적용할 수 있습니다. Android 뷰에서 뷰는 기본적으로 스테이트풀(Stateful)이므로 이 상태 자체를 관리하여 상태를 수정하거나 쿼리하는 메서드를 노출합니다. 텍스트에 관한 TextView 클래스의 get  set 메서드를 예로 들 수 있습니다. Jetpack Compose에서 상태는 컴포저블의 외부에 있으며 컴포저블 아주 가까이에서 호출 구성 가능한 함수나 상태 홀더로 끌어올릴 수도 있습니다. Scaffold 컴포저블의 ScaffoldState를 예로 들 수 있습니다.

로직

UI 상태는 정적 속성이 아닙니다. 시간이 지남에 따라 애플리케이션 데이터와 사용자 이벤트로 인해 UI 상태가 변경되기 때문입니다.

로직은 변경된 UI 상태 부분, 변경 이유, 변경해야 하는 시점 등 구체적인 변경사항을 결정합니다.

로직은 비즈니스 로직 또는 UI 로직일 수 있습니다.

  • 비즈니스 로직은 앱 데이터에 대한 제품 요구사항의 구현입니다. 예를 들어 사용자가 버튼을 탭할 때 뉴스 리더 앱에서 기사를 북마크에 추가합니다. 북마크를 파일이나 데이터베이스에 저장하는 이 로직은 일반적으로 도메인 또는 데이터 레이어에 배치됩니다. state holder는 일반적으로 노출되는 메서드를 호출하여 이 로직을 이러한 레이어에 위임합니다.
  • UI 로직은 화면에 UI 상태를 표시하는 방법과 관련이 있습니다. 사용자가 카테고리를 선택했을 때 올바른 검색창 힌트를 가져오는 것, 목록의 특정 항목으로 스크롤하는 것, 또는 사용자가 버튼을 클릭할 때 특정 화면으로의 navi 로직을 예로 들 수 있습니다.

Android 수명 주기와 UI 상태 및 로직 유형

UI 레이어는 두 부분으로 구성됩니다.

하나는 UI 수명 주기에 종속되고 다른 하나는 UI 수명 주기와 무관합니다.

이렇게 분리하면 각 부분에 사용할 수 있는 데이터 소스가 결정되므로 다른 유형의 UI 상태와 로직이 필요합니다.

 

UI 수명 주기와 무관: UI 레이어의  앱의 데이터 생성 레이어(데이터 또는 도메인 레이어)를 처리하고 비즈니스 로직으로 정의됩니다. UI의 수명 주기, 구성 변경, Activity 재생성은 UI 상태 생성 파이프라인의 활성화 여부에 영향을 줄 수 있지만 생성된 데이터의 유효성에는 영향을 미치지 않습니다.

 

UI 수명 주기에 종속: UI 레이어의 이 부분은 UI 로직을 처리하며 수명 주기나 구성 변경사항의 직접적인 영향을 받습니다. 이러한 변경사항은 내부에서 읽은 데이터 소스의 유효성에 직접 영향을 미치므로 상태는 수명 주기가 활성 상태일 때만 변경될 수 있습니다. 런타임 권한과 구성 종속 리소스(예: 현지화된 문자열) 가져오기를 예로 들 수 있습니다.

위 내용을 아래 표와 같이 요약할 수 있습니다.

UI 상태 생성 파이프라인

UI 상태 생성 파이프라인은 UI 상태를 생성하기 위해 실행하는 단계를 나타냅니다. 이러한 단계는 이전에 정의된 로직 유형을 적용하는 것으로 구성되며 UI의 요구사항에 완전히 종속됩니다. 일부 UI는 파이프라인의 UI 수명 주기와 무관한 부분과 UI 수명 주기에 종속된 부분 모두에서 또는 둘 중 하나에서 이점을 얻을 수 있고 아무런 이득을 얻지 못할 수도 있습니다.

 

즉, UI 레이어 파이프라인의 다음과 같은 순열은 유효합니다.

 

UI 자체에서 생성 및 관리하는 UI 상태. 예를 들어 간단하고 재사용 가능한 기본 카운터는 다음과 같습니다.

@Composable
fun Counter() {
    // The UI state is managed by the UI itself
    var count by remember { mutableStateOf(0) }
    Row {
        Button(onClick = { ++count }) {
            Text(text = "Increment")
        }
        Button(onClick = { --count }) {
            Text(text = "Decrement")
        }
    }
}

UI 로직(3번째 아이템이 화면의 최상단에 있다면) → UI. 예를 들어 사용자가 목록 상단으로 이동할 수 있는 버튼을 표시하거나 숨깁니다

@Composable
fun ContactsList(contacts: List<Contact>) {
    val listState = rememberLazyListState()
    val isAtTopOfList by remember {
        derivedStateOf {
            listState.firstVisibleItemIndex < 3
        }
    }

    // Create the LazyColumn with the lazyListState
    ...

    // Show or hide the button (UI logic) based on the list scroll position
    AnimatedVisibility(visible = !isAtTopOfList) {
        ScrollToTopButton()
    }
}

비즈니스 로직 → UI 로직 → UI. 특정 UI 상태에 관해 화면에 올바른 정보를 표시하기 위해 스크롤하는 UI 요소입니다.

@Composable
fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {
    // Read screen UI state from the business logic state holder
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    val contacts = uiState.contacts
    val deepLinkedContact = uiState.deepLinkedContact

    val listState = rememberLazyListState()

    // Create the LazyColumn with the lazyListState
    ...

    // Perform UI logic that depends on information from business logic
    if (deepLinkedContact != null && contacts.isNotEmpty()) {
        LaunchedEffect(listState, deepLinkedContact, contacts) {
            val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)
            if (deepLinkedContactIndex >= 0) {
              // Scroll to deep linked item
              listState.animateScrollToItem(deepLinkedContactIndex)
            }
        }
    }
}

두 가지 종류의 로직이 모두 UI 상태 생성 파이프라인에 적용되는 경우 비즈니스 로직이 항상 UI 로직보다 먼저 적용되어야 합니다.

UI 로직 다음에 비즈니스 로직을 적용하려고 하면 비즈니스 로직이 UI 로직에 종속된다는 것을 의미합니다.

다음 섹션에서는 다양한 로직 유형과 상태 홀더를 자세히 살펴보며 이것이 문제가 되는 이유를 설명합니다.

 

State Holder 및 책임

State Holder의 책임은 앱이 읽을 수 있도록 상태를 저장하는 것입니다.

로직이 필요한 경우 State Holder는 중개자 역할을 하며 필요한 로직을 호스팅하는 데이터 소스에 대한 액세스 권한을 제공합니다.

이러한 방식으로 State Holder는 로직을 적절한 데이터 소스에 위임합니다.

여기에는 다음과 같은 이점이 있습니다.

  • 간단한 UI: UI가 state를 바인딩합니다.
  • 유지관리: State Holder에 정의된 로직을 UI 자체를 변경하지 않고도 반복할 수 있습니다.
  • 테스트 가능성: UI 및 상태 생성 로직을 독립적으로 테스트할 수 있습니다.
  • 가독성: 코드 리더가 UI 표시 코드와 UI 상태 생성 코드 간의 차이점을 명확하게 알아볼 수 있습니다.

크기나 범위와 관계없이 모든 UI 요소는 상응하는 State Holder와 1:1 관계를 갖습니다.

State Holder는 UI 상태 변경을 야기할 수 있는 모든 사용자 작업을 수락하고 처리할 수 있어야 하고 후속 상태 변경을 생성해야 합니다.

 

참고: State Holder가 반드시 필요한 것은 아닙니다. 간단한 UI는 표시 코드와 함께 로직을 인라인으로 호스팅할 수 있습니다.

State Holder 유형

UI 상태 및 로직의 종류와 마찬가지로 UI 레이어에는 UI 수명 주기와의 관계에 따라 정의되는 두 가지 유형의 상태 홀더가 있습니다.

  • 비즈니스 로직 State Holder
  • UI 로직 State Holder

 

먼저 비즈니스 로직 State Holder를 살펴봅니다.

 

참고: UI 로직 State Holder가 데이터 또는 도메인 레이어의 정보에 종속되는 경우 비즈니스 로직 상태 홀더에서 UI 로직 상태 홀더로 이 정보를 전달해야 합니다. 이는 비즈니스 로직State Holder가 UI 수명 주기와 무관하므로 UI 로직 State Holder보다 수명이 길기 때문입니다.

비즈니스 로직 및 State Holder

비즈니스 로직 State Holder는 사용자 이벤트를 처리하고 데이터 또는 도메인 레이어에서 화면 UI 상태로 데이터를 변환합니다.

Android 수명 주기와 앱 구성 변경사항을 고려할 때 최적의 사용자 환경을 제공하려면 비즈니스 로직을 활용하는 상태 홀더에 다음 속성이 있어야 합니다.

 

UI 상태 생성 비즈니스 로직 state holder는 UI의 UI 상태를 생성해야 합니다.
이 UI 상태는 종종 사용자 이벤트를 처리하거나,  도메인 및 데이터 레이어에서 데이터를 읽은 결과입니다.
Acitvity 재생성을 통해 유지됨 비즈니스 로직 state holder는 Activity 재생성 전반에 걸쳐 상태 및 상태 처리 파이프라인을 유지하여 원활한 사용자 환경을 제공할 수 있도록 합니다.
state holder를 유지할 수 없어 다시 만드는 경우(일반적으로 프로세스 중단 후) state holder는 일관된 사용자 환경을 보장하기 위해 마지막 상태를 쉽게 재생성할 수 있어야 합니다.
장기 지속 상태 보유 비즈니스 로직 state holder는 종종 navi destination의 상태를 관리하는 데 사용됩니다.
따라서 navi 그래프에서 삭제될 때까지 탐색 변경 후에도 상태를 유지하는 경우가 많습니다.
UI에 고유하며 재사용할 수 없음 비즈니스 로직 state holder는 일반적으로 특정 앱 기능(예: TaskEditViewModel 또는 TaskListViewModel)의 상태를 생성하므로 이 앱 기능에만 적용됩니다. 동일한 상태 홀더가 다양한 폼 팩터에서 이러한 앱 기능을 지원할 수 있습니다.
예를 들어 모바일, TV, 태블릿 버전의 앱은 동일한 비즈니스 로직 상태 홀더를 재사용할 수 있습니다.

참고: 비즈니스 로직 상태 홀더는 일반적으로 ViewModel 인스턴스로 구현됩니다. 

ViewModel 인스턴스가 위에서 설명한 여러 기능, 특히 Activity 재생성 시 유지 기능을 지원하기 때문입니다.

 

앱의 작성자 탐색 대상을 살펴보겠습니다.

 

@HiltViewModel
class AuthorViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val authorsRepository: AuthorsRepository,
    newsRepository: NewsRepository
) : ViewModel() {

    val uiState: StateFlow<AuthorScreenUiState> = …

    // Business logic
    fun followAuthor(followed: Boolean) {
      …
    }
}

 

속성세부정보

AuthorScreenUiState 생성 AuthorViewModel은 AuthorsRepository  NewsRepository에서 데이터를 읽고 AuthorScreenUiState를 생성하는 데 이 데이터를 사용합니다. 또한 사용자가 AuthorsRepository에 위임하여 Author를 팔로우하거나 팔로우 해제하려고 할 때 비즈니스 로직을 적용합니다.
데이터 영역에 대한 액세스 권한 보유 AuthorsRepository  NewsRepository 인스턴스가 생성자에서 전달되므로 Author를 팔로우하는 비즈니스 로직을 구현할 수 있습니다.
Activity 재생성 시 유지 ViewModel로 구현되므로 빠른 Activity 재생성에도 유지됩니다. 프로세스 중단의 경우 SavedStateHandle 객체를 읽어 데이터 레이어에서 UI 상태를 복원하는 데 필요한 최소한의 정보를 제공할 수 있습니다.
장기 지속 상태 보유 ViewModel의 범위가 navi graph로 지정되므로 작성자 대상이 탐색 그래프에서 삭제되지 않는 한 uiState StateFlow의 UI 상태는 메모리에 유지됩니다. StateFlow를 사용하면 UI 상태의 수집기가 있는 경우에만 상태가 생성되므로 상태를 생성하는 비즈니스 로직의 적용을 지연시킬 수 있다는 이점도 추가됩니다.
UI에 고유함 AuthorViewModel은 작성자 탐색 대상에만 적용되며 다른 곳에서는 재사용할 수 없습니다. 탐색 대상에서 재사용되는 비즈니스 로직이 있는 경우 해당 비즈니스 로직은 데이터 또는 도메인 레이어 범위 구성요소에 캡슐화되어야 합니다.

참고: ViewModel은 destination 수준 UI에서만 사용해야 합니다.

검색창이나 칩 그룹과 같이 UI의 재사용 가능한 부분에서 사용하면 안 됩니다. 이러한 경우에는 일반 클래스가 더 적합합니다.

 

경고: ViewModel 인스턴스를 다른 composable function로 전달하지 마세요.

전달하면 composable function이 ViewModel 유형과 결합되어 재사용성이 떨어지고 테스트와 미리보기가 더 어려워집니다. 또한 ViewModel 인스턴스를 관리하는 명확한 단일 소스 저장소(SSOT)도 없습니다. ViewModel을 전달하면 여러 컴포저블이 ViewModel 함수를 호출하고 상태를 수정할 수 있으므로 버그를 디버그하기가 더 어려워집니다. 대신 UDF 권장사항을 따라 필요한 상태만 전달합니다.

 

마찬가지로 ViewModel의 컴포저블 SSOT에 도달할 때까지 전파 이벤트를 위로 전달합니다. 이벤트를 처리하고 상응하는 ViewModel 메서드를 호출하는 SSOT입니다.

 

비즈니스 로직 State Holder로서의 ViewModel

Android 개발에서 ViewModel이 가진 이점 덕분에, 비즈니스 로직에 대한 액세스 권한을 제공하고 화면에 표시하기 위한 애플리케이션 데이터를 준비하는 데 ViewModel이 적합합니다. 이점은 다음과 같습니다.

  • ViewModel에 의해 트리거된 작업이 구성 변경에도 그대로 유지됩니다.
  • Navigation과의 통합:
    • 화면이 백 스택에 있는 동안 Navigation이 ViewModel을 캐시합니다. 이는 개발자가 destination으로 돌아갈 때 이전에 로드한 데이터를 즉시 사용할 수 있도록 하는 데 중요합니다. 이 작업은 컴포저블 화면의 수명 주기를 따르는 상태 홀더를 사용할 경우 더 어려워집니다.
    • 또한 destination이 백 스택에서 사라질 때 ViewModel도 삭제되기 때문에, State가 자동으로 정리됩니다. 이는 구성 변경으로 인한 새 화면으로의 이동 등 여러 이유로 발생할 수 있는 컴포저블 폐기에 관한 수신 대기와는 다릅니다.
  • Hilt와 같은 다른 Jetpack 라이브러리와의 통합

참고: ViewModel 이점이 사용 사례에 적용되지 않거나 개발자가 다른 방식으로 작업을 실행하는 경우 ViewModel의 책임을 일반 상태 홀더 클래스로 옮길 수 있습니다.

UI 로직 및 State Holder 

UI 로직은 UI 자체에서 제공하는 데이터에 작동하는 로직입니다.

UI 요소의 상태 또는 UI 데이터 소스(예: Permission API나 Resources)에 있을 수 있습니다.

UI 로직을 활용하는 State holder에는 일반적으로 다음 속성이 있습니다.

 

UI 상태 생성 및 UI 요소 상태 관리

 

Activity 재생성 시 유지되지 않음: UI 로직에서 호스팅되는 State Holder는 종종 UI 자체의 데이터 소스에 종속되므로 구성 변경 시 이 정보를 유지하려고 하면 메모리 누수를 일으키는 경우가 많습니다. State Holder가 구성 변경 시 데이터를 유지하려는 경우 Activity 재생성 시 유지되기에 더 적합한 다른 구성요소에 위임해야 합니다.

 

예를 들어 Jetpack Compose에서 remembered 함수로 만든 컴포저블 UI 요소 상태는 Activity 재생성 전반에 걸쳐 상태를 유지하기 위해 rememberSaveable에 위임되는 경우가 많습니다. 이러한 함수의 예로는 rememberScaffoldState()와 rememberLazyListState()가 있습니다.

 

UI 범위 데이터 소스 참조가 있음: UI 로직 State Holder가 UI와 동일한 수명 주기를 가지므로 수명 주기 API 및 리소스와 같은 데이터 소스를 안전하게 참조하고 읽을 수 있습니다.

 

여러 UI에서 재사용 가능: 동일한 UI 로직 State Holder의 다양한 인스턴스가 앱의 여러 부분에서 재사용될 수 있습니다. 예를 들어 칩 그룹의 사용자 입력 이벤트를 관리하는 State Holder를 필터 칩의 검색 페이지와 이메일 수신자의 'to' 필드에 사용할 수 있습니다.

일반적으로 UI 로직 State Holder는 일반 클래스로 구현됩니다.

 

 UI 자체가 UI 로직 State Holder 생성을 담당하고 UI 로직 State Holder의 수명 주기가 UI 자체의 수명 주기와 동일하기 때문입니다.

예를 들어 Jetpack Compose에서 상태 홀더는 컴포지션의 일부이며 컴포지션의 수명 주기를 따릅니다.

 

참고: 일반 클래스 State Holder는 UI 로직이 너무 복잡해서 UI 밖으로 이동할 때 사용됩니다. 그 외의 경우 UI 로직은 UI에서 인라인으로 구현할 수 있습니다.

 

Now in Android 샘플은 기기의 화면 크기에 따라 탐색 시 하단 앱 바 또는 탐색 레일을 표시합니다.

작은 화면에서는 하단 앱 바를, 큰 화면에서는 탐색 레일을 사용합니다.

 

Composable NiaApp 함수에서 사용되는 적절한 탐색 UI 요소를 결정하는 로직은 비즈니스 로직에 종속되지 않으므로 NiaAppState라는 일반 클래스 상태 홀더로 관리할 수 있습니다.

 

@Stable
class NiaAppState(
    val navController: NavHostController,
    val windowSizeClass: WindowSizeClass
) {

    // UI logic
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    // UI logic
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar

   // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

     /* ... */
}

위 예에서 NiaAppState에 관한 다음 세부정보가 중요합니다.

  • Activity 재생성 시 유지되지 않음: NiaAppState는 Compose 이름 지정 규칙을 따르는 composable rememberNiaAppState를 사용하여 만들어 컴포지션에서 remembered됩니다. Activity가 재생성되면 이전 인스턴스가 손실되고, 재생성된 Activity의 새 구성에 적합한 모든 종속 항목이 전달된 상태로 새 인스턴스가 생성됩니다. 이러한 종속 항목은 새로운 것이거나 이전 구성에서 복원된 것일 수 있습니다. 예를 들어 rememberNavController()는 NiaAppState 생성자에서 사용되며, rememberSaveable에 위임하여 Activity 재생성 전반에 걸쳐 상태를 유지합니다.
  • UI 범위 데이터 소스 참조가 있음: navigationController, Resources, 기타 유사한 수명 주기 범위 유형에 관한 참조가 NiaAppState에 안전하게 보관될 수 있습니다. 동일한 수명 주기 범위를 공유하기 때문입니다.

참고: 일반 State Holder 클래스는 검색창이나 칩 그룹과 같은 재사용 가능한 UI 부분에 사용하는 것이 좋습니다.

이 경우 ViewModel을 사용하면 안 됩니다. 탐색 대상의 상태를 관리하고 비즈니스 로직에 액세스하는 데 사용하기에 가장 적합하기 때문입니다.

 

StateHolder의 ViewModel과 일반 클래스 중에서 선택

ViewModel과 일반 클래스 State Holder  중 하나를 선택하는 일은 UI 상태에 적용되는 로직과 로직이 작동하는 데이터 소스에 따라 결정됩니다.

 

참고: 대부분의 애플리케이션은 다른 경우라면 일반 클래스 State Holder에 배치될 수 있는 UI 자체에서 UI 로직을 인라인으로 실행하도록 선택합니다. 이는 간단한 사례에서는 괜찮지만 다른 상황에서는 로직을 일반 클래스 State Holder로 가져와 가독성을 개선할 수 있습니다.

결과적으로 소비되는 위치와 가장 가까운 State Holder를 사용하여 UI 상태를 생성해야 합니다.

좀 더 쉽게 말하면 적절한 소유권을 유지하면서 상태를 가능한 한 낮게 유지해야 합니다.

 

비즈니스 로직에 액세스해야 하며 화면이 이동될 수 있는 한(Activity 재생성 시에도) UI 상태를 유지해야 하는 경우 비즈니스 로직 상태 홀더 구현에 ViewModel을 사용하는 것이 좋습니다.

 

단기 지속 UI 상태 및 UI 로직의 경우 수명 주기가 UI에만 종속되는 일반 클래스로 충분합니다.