Compose에서의 상태
이 섹션에서는 화면에 약간의 상호작용을 추가합니다. 지금까지는 정적 레이아웃을 만들었지만,
이제는 사용자 변경사항에 반응하여 화면과 상호작용할 수 있게 합니다.
버튼을 클릭할 수 있게 만드는 방법과 항목의 크기를 조절하는 방법을 알아보기 전에 각 항목이 펼쳐진 상태인지를 가리키는 값을 어딘가에 저장해야 합니다. 이 값을 항목의 상태라고 합니다. 인사말마다 이러한 값 중 하나가 필요하므로 이 값의 논리적 위치는 Greeting 컴포저블에 있습니다. 이 불리언 값 expanded와 이 값이 코드에서 사용되는 방식을 살펴보세요.
// Don't copy over
@Composable
private fun Greeting(name: String) {
var expanded = false // Don't do this!
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
onClick 작업과 동적 버튼 텍스트도 추가했습니다. 이에 관한 설명은 나중에 자세히 다룹니다.
하지만, 이 작업은 예상대로 작동하지 않습니다. expanded 변수에 다른 값을 설정해도 Compose에서 이 값을 상태 변경으로 감지하지 않으므로 아무 일도 일어나지 않습니다.
Compose 앱은 구성 가능한 함수를 호출하여 데이터를 UI로 변환합니다. 데이터가 변경되면 Compose는 새 데이터로 이러한 함수를 다시 실행하여 업데이트된 UI를 만듭니다. 이를 리컴포지션이라고 합니다. 또한, Compose는 데이터가 변경된 구성요소만 다시 구성하고 영향을 받지 않는 구성요소는 다시 구성하지 않고 건너뛰도록 개별 컴포저블에서 필요한 데이터를 확인합니다.
다음은 Compose 이해에 언급된 내용입니다.
구성 가능한 함수는 자주 실행될 수 있고 순서와 관계없이 실행될 수 있으므로 코드가 실행되는 순서 또는 이 함수가 다시 구성되는 횟수에 의존해서는 안 됩니다.
이 변수를 변경했을 때 리컴포지션을 트리거하지 않는 이유는 이 변수를 Compose에서 추적하고 있지 않기 때문입니다. 또한, Greeting이 호출될 때마다 변수가 거짓으로 재설정됩니다.
컴포저블에 내부 상태를 추가하려면 mutableStateOf 함수를 사용하면 됩니다. 이 함수를 사용하면 Compose가 이 State를 읽는 함수를 재구성합니다.
State 및 MutableState는 어떤 값을 보유하고 그 값이 변경될 때마다 UI 업데이트(리컴포지션)를 트리거하는 인터페이스입니다.
import androidx.compose.runtime.mutableStateOf
...
// Don't copy over
@Composable
fun Greeting() {
val expanded = mutableStateOf(false) // Don't do this!
}
하지만, 컴포저블 내의 변수에 mutableStateOf를 할당하기만 할 수는 없습니다. 앞에서 설명한 것처럼 false 값을 가진 변경 가능한 새 상태로 상태를 재설정하여 컴포저블을 다시 호출하는 때는 언제든지 리컴포지션이 일어날 수 있습니다.
여러 리컴포지션 간에 상태를 유지하려면 remember를 사용하여 변경 가능한 상태를 기억해야 합니다.
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
...
@Composable
fun Greeting() {
val expanded = remember { mutableStateOf(false) }
...
}
remember는 리컴포지션을 방지하는 데 사용되므로 상태가 재설정되지 않습니다.
화면의 서로 다른 부분에서 동일한 컴포저블을 호출하는 경우 자체 상태 버전을 가진 UI 요소를 만듭니다. 내부 상태는 클래스의 비공개 변수로 보면 됩니다.
구성 가능한 함수는 상태를 자동으로 '구독'합니다. 상태가 변경되면 이러한 필드를 읽는 컴포저블이 재구성되어 업데이트를 표시합니다.
상태 변경 및 상태 변경사항에 반응
상태를 변경하기 위해 Button이 onClick이라는 매개변수를 사용한다고 알고 있을 수도 있지만, 값을 사용하지 않고 함수를 사용합니다.
이런 방식으로 함수를 사용하는 것이 익숙하지 않을 수도 있지만 이는 Compose에서 광범위하게 사용되는 매우 강력한 Kotlin 기능입니다. 함수는 Kotlin의 일급 시티즌이므로 변수에 할당되거나 다른 함수로 전달될 수 있으며 그 함수에서 반환될 수도 있습니다. Compose가 Kotlin 기능을 사용하는 방법은 여기에서 확인할 수 있습니다.
함수가 정의되는 방법과 이를 인스턴스화하는 방법을 자세히 알아보려면 함수 유형 문서를 참고하세요.
작업에 람다 표현식을 할당하여 클릭 시 실행할 작업을 정의할 수 있습니다. 예를 들어, 펼침 상태의 값을 전환하고 값에 따라 다른 텍스트를 표시해 보겠습니다.
ElevatedButton(
onClick = { expanded.value = !expanded.value },
) {
Text(if (expanded.value) "Show less" else "Show more")
}
에뮬레이터에서 앱을 실행하는 경우 버튼을 클릭하면 expanded가 전환되어 버튼 내부의 텍스트 리컴포지션이 트리거되는 것을 확인할 수 있습니다. 각 Greeting은 서로 다른 UI 요소에 속하기 때문에 자체적으로 펼쳐진 상태를 유지합니다.
이 지점까지의 코드는 다음과 같습니다.
@Composable
private fun Greeting(name: String) {
val expanded = remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
항목 펼치기
이제 실제로 요청을 받은 경우 항목을 펼쳐 보겠습니다. 상태에 따라 달라지는 추가 변수를 추가합니다.
@Composable
private fun Greeting(name: String) {
val expanded = remember { mutableStateOf(false) }
val extraPadding = if (expanded.value) 48.dp else 0.dp
...
extraPadding은 간단한 계산을 실행하므로 리컴포지션에 대비하여 이 값을 기억할 필요가 없습니다.
따라서, 이제 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.colorScheme.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)
}
ElevatedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
에뮬레이터에서 실행하는 경우 각 항목을 독립적으로 펼칠 수 있어야 합니다.