ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] Compose State 관리 - 1
    안드로이드 2021. 8. 2. 14:31
    반응형

    State와 Composition

    Compose는 선언적이므로 Compose를 업데이트하는 유일한 방법은 새 파라메터로 동일한 Composable을 호출하는 것이다. 

    이 파라메터는 UI 상태를 나타난다. 상태가 업데이트될때마다 재구성이 실행된다. 따라서 TextField와 같은 항목은 명령형 XML 기반 뷰에서 처럼 자동으로 업데이트 되지 않는다. Composable이 새 상태에 따라 업데이트되면서 새 상태를 명시적으로 알려줘야 한다.

    @Composable
    fun HelloContent() {
       Column(modifier = Modifier.padding(16.dp)) {
           Text(
               text = "Hello!",
               modifier = Modifier.padding(bottom = 8.dp),
               style = MaterialTheme.typography.h5
           )
           OutlinedTextField(
               value = "",
               onValueChange = { },
               label = { Text("Name") }
           )
       }
    }

    이 코드를 실행하면 아무 일도 일어나지 않는다. 그 이유는 TextField가 자체적으로 업데이트 되지 않기 때문이다. value 파라메터가 업데이트 될 때 업데이트 된다. 이는 Compose의 Composition과 Recomposition(재구성) 때문이다.

     

    ※ 용어 설명

      * Composition : Jetpack Compose가 Composable을 실행할 때 빌드한 UI에 관한 설명

      * Initial Composition : 처음 Composable을 실행하여 Composition을 생성

      * Recomposition(재구성) : 데이터가 변경될 때 Composition을 업데이트 하기 위해 Composable을 다시 실행하는 것

     

    Initial Composition과 Recomposition(재구성)은 이전 포스팅을 참고하면 된다.

    2021.07.30 - [안드로이드] - [Android] Compose 이해하기

     

    [Android] Compose 이해하기

    Compose란 Jetpack Compose는 Android를 위한 선언형 UI 도구 Kit이다. 앱 UI를 선언형 API를 제공하여 더 쉽게 작성하고 유지관리할 수 있도록 지원한다. 선언형 프로그래밍 패러다임 지금까지 Android의 뷰

    growup-lee.tistory.com

     

    Composable의 State

    Composable 함수는 remember Composable을 사용하여 메모리에 단일 객체를 저장할 수 있다.  remember에 의해 계산된 값은 Initial Composition 중에 Composition에 저장되고 저장된 값은 Recomposition(재구성) 중에 반환된다. remember는 변경 가능한 객체뿐만 아니라 변경할 수 없는 객체를 저장하는데 사용할 수 있다.

     

    remember는 객체를 Composition에 저장하고, remember를 호출한 Composable이 Composition에서 삭제되면 그 객체를 잊는다.

     

    mutableStateOf는 Observable한 MutableState<T>를 생성하고, 런타임 시 Compose에 통합되는 Observable 유형이다.

    interface MutableState<T> : State<T> {
        override var value: T
    }

     

    value가 변경되면 value를 읽는 Composable 함수의 Recomposition(재구성)이 예약된다. 

    Composable에서 MutableState를 선언하는 방법은 3가지이다.

    • val mutableState = remember { mutableStateOf(default) }
    • var value by remember { mutableStateOf(default) }
    • val (value, setValue) = remember { mutableStateOf(default) }

    위 3개의 선언은 동일한 것이며 상황에 맞게 읽기 쉬운 코드를 선택하여 작성하면 된다.

     

    by 위임 구문을 사용하기 위해서는 아래 두 import 구문이 필요하다.

    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.setValue

     

    remember 된 value는 다른 Composable의 파라메터로 사용하거나 로직 구문에 사용하여 Composable이 보여지도록 변경할 수 있다.

    예를 들어 이름이 비어있을 경우 인사말을 보이지 않게 하고 싶다면 if문을 사용하면 된다.

    @Composable
    fun HelloContent() {
       Column(modifier = Modifier.padding(16.dp)) {
           var name by remember { mutableStateOf("") }
           if (name.isNotEmpty()) {
               Text(
                   text = "Hello, $name!",
                   modifier = Modifier.padding(bottom = 8.dp),
                   style = MaterialTheme.typography.h5
               )
           }
           OutlinedTextField(
               value = name,
               onValueChange = { name = it },
               label = { Text("Name") }
           )
       }
    }

    remember는 Recomposition(재구성) 과정 전체에 상태를 유지하는데 도움이 되지만, 상태는 구성 변경하는 전반에 걸쳐 유지되지 않는다. 이 경우에는 rememberSavable을 사용해야 한다. remeberSavable은 Bundle에 저장할 수 있는 값은 자동으로 저장한다. 다른 값들은 Custom Saver 객체에 전달할 수 있다.

     

    지원되는 기타 State 유형

    Jetpack Compose는 State를 유지하기 위해 MutableState<T>만 사용하도록 하지 않았다. Jetpack Compose는 다른 Observable 타입도 지원한다. Jetpack Compose에서 다른 Observable 타입을 읽기 위해서는 State<T>로 변경해야한다. 그래야 상태가 변할 때 Jetpack Compose에서 자동으로 Recomposition(재구성) 된다.

     

    Compose는 안드로이드 앱에서 사용되는 일반 Observable 타입은 State<T>를 생성하는 함수가 내장되어 있다. 아래 세 가지 타입은 안드로이드 앱에서 사용되는 Observable 타입이다.

    • LiveData
    • Flow
    • RxJava2

    위 세개는 State<T>를 return 하는 Jetpack Compose용 확장 함수를 이용하여 State<T>로 변환하고 이를 통해 Recomposition(재구성) 할 수 있다.

     

    Stateful 과 Stateless

    remember를 사용하여 객체를 저장하는 Composable은 내부 상태를 Stateful로 만든다.

    위 예제에서 HelloContent는 name 상태를 보존하고 수정하므로 Stateful의 한 예 이다. 호출자가 상태를 제어할 필요가 없고 상태를 직접 관리하지 않아도 상태를 사용할 수 있는 경우에 유용하다. 그러나 내부 상태를 갖는 Composable은 재사용 가능성이 낮고 테스트하기가 더 어려운 경향이 있다.

     

    Stateless Composable은 상태를 갖지 않는 Composable이다. Stateless를 하기 위한 한 가지 쉬운 방법은 State Hoisting을 사용하는 것이다.

     

    재사용 가능한 Composable을 만들기 위해서는 Stateful 버전과 Stateless 버전을 모두 노출해야하는 경우가 있다. Stateful 버전은 상태를 염두하지 않는 호출자에 편리하고, Stateless는 상태를 제어하거나 끌어올려야하는 호출자에 필요하다.

     

    State Hoisting

    Compose에서 State Hoisting은 Composable을 Stateless로 만들기 위해 상태를 Composable의 호출자로 옮기는 패턴이다.

    • value: T : 표시할 현재 값
    • onValueChange: (T) -> Unit : T가 새 값인 경우 값을 변경하도록 요청하는 이벤트

    onValueChange로만 제한되지 않고 Composable에 따라 달라질 수 있다.

     

    이러한 방식으로 끌어올려진 State는 몇가지 중요한 속성들을 가진다.

    • 하나의 진실의 근원(Single source of truth) : 상태를 복제하는 대신 옮김으로써, 진실의 근원은 한개만 있는 것을 보장한다. 이것은 버그를 피하는데 도움이 된다.
    • 캡슐화됨(Encapsulated) : Stateful Composable만 상태 변경이 가능하다. 이것은 완전한 내부 속성이다.
    • 공유가능(Shareable) : 끌어올려진 상태는 여러 Composable에 공유가 가능하다. 다른 Comopsable에서 name을 사용하려고 하면 Hoisting을 통해 공유할 수 있다.
    • 가로채기 가능함(Interceptable) : Stateless Composable의 호출자는 상태가 변경되기 전에 event를 무시할지 변경할지 결정할 수 있다.
    • 분리됨(Decoupled) : Stateless Composable의 State는 어느 곳에서든 저장될 수 있다. 예를 들어, name을 ViewModel에 옮겨 저장할 수 있다.

    아래 예제는 HelloContent에서 name과 onValueChange라는 속성을 HelloContent의 밖으로 추출하여 트리 상단인 HelloScreen으로 옮겼다. 그리고 HelloScreen에서 HelloContent를 호출하고 있다.

    @Composable
    fun HelloScreen() {
        var name by rememberSaveable { mutableStateOf("") }
    
        HelloContent(name = name, onNameChange = { name = it })
    }
    
    @Composable
    fun HelloContent(name: String, onNameChange: (String) -> Unit) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "Hello, $name",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.h5
            )
            OutlinedTextField(
                value = name,
                onValueChange = onNameChange,
                label = { Text("Name") }
            )
        }
    }

    HelloContent에서 상태를 끌려올림으로써, Composable에 대해 추론, 다른 상황에서 재사용, 테스트에 더 용이해진다.

    HelloContent는 상태가 저장되는 방식과 분리된다. 분리됨(Decoupling)의 의미는 HelloScreen을 수정하거나 대체해도 구현된 HelloContent는 변경하지 않아도 된다는 의미이다.

    위 그림처럼 State가 아래로 내려가고 Event가 올라가는 패턴을 단방향 데이터 플로우라고 한다. 이 경우 state가 HelloScreen에서 HelloContent로 내려가고, event가 HelloContent에서 HelloScreen으로 올라간다. 단방향 데이터 플로우에 따르면 UI에 상태를 화면에 표시하는 Composable과 상태를 저장하고 변경하는 앱 부분을 서로 분리할 수 있다.

     

    State를 끌어올릴 때 상태의 위치를 쉽게 파악하는데 도움이 되는 세가지 규칙이 있다.

    1. State는 최소한 State를 사용하는 Composable의 가장 낮은 공통 부모에 끌어올려야한다.(읽기)
    2. State는 최소한 가장 높은 Level에서 변경되는 위치에 끌어올려야한다.(쓰기)
    3. 한 event 호출에 의해 두가지 State가 변경된다면 둘 다 끌어올려야한다.

    이 세가지 규칙보다 더 높이 끌어올릴수도 있지만 State를 끌어내리면 단뱡향 데이터 플로우를 따르는데 어려움이 있거나 불가능해질 수 있다.

     

    마치며

    Compose의 State 관리에 대해 알아보았다. 다음에는 State 관리에 대해 더 알아보기로 하자.

     

    반응형

    댓글

Designed by Tistory.