Compose의 상태 

지금까지는 정적인 레이아웃을 만들었지만 이제 사용자 액션에 반응하도록 만들어 보자.

어떻게 버튼을 클릭가능하게 만들고 아이템의 사이즈를 조절할 수 있는지 알기 전에, 각각의 아이템이 확장되거나 축소되는 상태값을 어딘가에 저장해야만 한다. 앞에서 만든 Greeting 함수별로 이러한 값을 가질 필요가 있기 때문에, 논리적으로 생각해보면 이러한 값은 Greeting 내에 있어야 한다. 하지만 다음과 같은 형태로 있으면 절대 안된다!

// 이 코드는 사용금지
@Composable
private fun Greeting(name: String) {
    var expanded = false // 절대로 이렇게 하면 안돼요!

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

위의 gif 움짤을 보면 “show more/show less” 문구를 동적으로 변경되는 것을 확인할 수 있는데, 이는 나중에 살펴보자. 아무튼 위의 코드는 동작하지 않는 엉터리 코드다. 

선언형 UI 툴킷인 Compose는 composable 함수를 호출하여 데이터를 UI로 변환한다. 데이터가 변경되면 Compose는 이러한 함수들을 새로운 데이터와 함께 다시 실행하여 업데이트된 UI를 생성한다. 이를 재구성(recomposition)이라고 한다. 또한 Compose는 데이터가 변경된 composable 요소만 재구성하고, 영향을 받지 않은 composable의 재구성을 건너뛰도록 개별적으로 composable 함수에 필요한 데이터를 확인한다. Compose 이해 편에서 언급했듯이 Composable 함수는 자주 실행될 수 있고, 코드가 실행되는 순서나, 재구성되는 횟수에 의존해서는 안된다.

Greeting 함수내의 expanded가 변경되도 재구성이 트리거되지 않는 이유는 Compose에서 이를 추적하지 않기 때문이다. 또한 Greeting이 호출될 때마다 변수는 false로 초기화 된다.

mutableStateOf 함수를 사용하면 composable 함수에 내부적으로 상태(state)를 추가 할 수 있다. 이를 통해 Compose가 State를 읽고 재구성 할 수 있도록 한다.

StateMutableState는 어떤 값을 가지고 있고, 이 값이 변경 될 때마다 UI 갱신을 트리거 할 수 있도록 하는 인터페이스다.

@Composable
fun Greeting() {
    val expanded = mutableStateOf(false) // 이렇게 사용하면 안됩니다.
}

하지만 composable 함수내에서 단순히 mutableStateOf함수를 사용할 수는 없다. 앞에서 설명했듯이, 재구성(recomposition)은 언제든지 발생할 수 있다. 이는 상태(state)를 초기화하고 새로운 mutable state생성과 함께 false값을 갖게 될 것이다.

재구성이 일어날 때 값을 보존하기 위해 remember라는 함수를 사용한다.

@Composable
fun Greeting() {
    val expanded = remember { mutableStateOf(false) }
}

remember는 재구성으로부터 해당 값의 변경을 보호하여 초기화 되지 않도록 한다.

화면의 각각 다른 부분에서 동일한 composable 함수를 호출하여 재사용한다면, 각각 고유한 버전의 상태가 있는 다른 UI 요소를 생성하게 되는 것이다. 즉, composable 함수에 선언된 state는 마치 클래스에 선언된 private 변수라고 생각할 수 있다.

Composable 함수는 자동으로 state에 “subscribed” 되어진다. 상태가 변경되면, composable 함수는 이러한 필드를 불러와 재구성하고 화면을 갱신한다.

Stateless vs Stateful

  • Stateless는 State를 갖지 않는 컴포저블을 말한다. Caller 쪽에서 State를제어한다.
  • Stateful은 remember를 사용해서 내부적으로 State를 생성하는 것을 말한다. 그러므로 Caller가 State를 관리하지 않는다.
    이는 재사용성 및 테스트 용이성을 떨어뜨린다.

따라서 Stateful보다는 Stateless로 만드는 것이 좋다. 이를 위해 State Hoisting을 시도 할 수 있다.

상태를 변경하고 상태변화에 반응하기

상태를 변경하기 위해 Button에 onClick이라는 매개변수를 사용하자. 이 매개변수는 값을 사용하지 않고 함수를 사용한다.

람다 표현식을 통해 onClick에서 어떤 액션을 취할지 정의 할 수 있다. expanded 상태를 토글하여, 상태에 따라 다른 텍스트를 나타낼 수 있도록 해보자.

OutlinedButton(
    onClick = { expanded.value = !expanded.value },
) {
    Text(if (expanded.value) "Show less" else "Show more")
}

앱을 실행해보면 버튼을 클릭할 때 expanded 상태가 변경되면서 버튼내에 있는 텍스트가 재구성 되는 것을 확인할 수 있다. 각 Greeting 함수는 자신의 expanded 상태를 각각 유지하기 때문에, 화면에서 각각의 버튼이 서로 다른 상태로 나타나는 것을 확인할 수 있다.

지금까지 작성한 코드를 한번 정리해보면 다음과 같다.

@Composable
private fun Greeting(name: String) {
    var expanded = remember { mutableStateOf(false) }

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

대화형 모드를 사용하면 에뮬레이터를 실행하지 않고 안드로이드 스튜디오 내에서 직접 실행하여 버튼 클릭과 같은 상호작용을 테스트해 볼 수 있다.

다음과 같은 일부 기능이 제한된다.

– 네트워크 액세스
– 파일 엑세스
– 일부 Context관련 API 사용 불가

아이템 확장하기

이제 요청이 있을 때 실제로 항목을 확장해보자. 상태에 따라 달라지게 할 변수를 추가하자.

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp
...

extraPadding을 재구성시 Compose 내에 기억하도록 할 필요는 없기 때문에, 단순히 함수가 호출 될 때 기존 상태를 보고 계산하도록 하자.

그렇다면 새로운 padding 값을 다음과 같이 Column에 적용할 수 있게 된다.

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

각각의 아이템이 독립적으로 확장되는 것을 확인할 수 있다.


후원하기

카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.