상태는 Compose의 선언적 UI 패러다임에서 UI를 어떻게 그리고 유지할지를 결정하는 데이터이다. 상태는 UI를 동적으로 업데이트하고 사용자와의 상호작용에 반응한다.
Compose는 Composable 함수를 호출하여 상태를 UI로 변환한다. 상태(데이터)가 변경되면 Compose는 동일한 Composable함수를 다시 호출하여 업데이트된 UI를 만든다.
이를 리컴포지션(recomposition)이라 하고 데이터가 변경된 Composable만 리컴포지션하고 그렇지 않은 것은 건너뛴다.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded = false
val expandedPadding = if (expanded) 48.dp else 0.dp
Surface {
Column(
modifier = Modifier.padding(bottom = expandedPadding)
) {
Text(text = "Hello, $name")
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
버튼을 클릭하면 항목이 펼쳐지고 접히는 것을 만들고자 한다. 따라서, 항목이 현재 펼쳐진 상태인지를 저장해야한다. 그렇다면 여기서는 expanded가 상태가 될 것이다.
우리는 버튼을 누르면 expanded값이 반전이 되고 true일 경우 확장되는 것을 기대한다. 실제로 실행시켜보면 예상대로(?) 아무일도 일어나지 않는다.
expanded 변수에 다른 값을 할당해도 Compose에서 이 값을 상태 변경으로 감지하지 않기 때문이다. 다르게 말하면, Compose에서 이 변수를 추적하고 있지 않기 때문이다.
Composable에 내부 상태를 추가하기 위해서는 mutableStateOf 함수를 사용하면 된다.
mutableStateOf는 상태 관리를 위해 제공되는 API중 하나로, Compose가 상태 변경을 감지할 수 있도록 값을 래핑(wrapping)하는 역할을 한다.
@Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
value가 변경되면 value를 읽는 Composable 함수의 리컴포지션이 예약된다.
component1, component2는 구조분해 선언을 지원하기 위한 것이다.
val (value, setValue) = remember { mutableStateOf(0) }의 경우,
value의 타입은 Int, setValue의 타입은 (Int) -> Unit이다.
(remember는 바로 다음에 알아보겠다.)
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded = mutableStateOf(false)
val expandedPadding = if (expanded) 48.dp else 0.dp
Surface {
Column(
modifier = Modifier.padding(bottom = expandedPadding)
) {
Text(text = "Hello, $name")
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
그렇다면 위 코드는 정상적으로 동작할까? 그렇지 않다.
애초에 컴파일 에러로 실행도 되지 않는다.
한번 상태 변화의 흐름을 생각해보자.
var expanded = mutableStateOf(false)가 실행되기에, expanded는 true를 유지하는 것이 아닌, false가 되어버린다.즉, 상태 변화는 감지하지만 상태를 유지하지 못하는 상황인 것이다.
리컴포지션 간에 상태를 유지하려면 remember를 사용하여 상태를 기억해야 한다.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
val expandedPadding = if (expanded) 48.dp else 0.dp
Surface {
Column(
modifier = Modifier.padding(bottom = expandedPadding)
) {
Text(text = "Hello, $name")
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
아직 한 가지 문제가 남았다.
화면 회전과 같은 구성변경이나 프로세스 종료 시 상태가 손실된다. 이는
remember를 사용하는 대신 rememberSavable을 사용하면 된다. 이 함수는 상태를 Bundle에 저장하고 복원하므로, 구성 변경과 프로세스 중단에도 상태를 유지한다.
앞서 말했다시피, rememberSavable은 Bundle에 값을 저장하므로 저장되는 데이터는 기본적으로 Primitive Type(Int, String, Boolean..etc)이어야 한다.
만약, expanded가 아래와 같아지면 어떻게 해야 할까?
// 런타임에 크래시가 발생하는 코드
data class Expanded(val value: Boolean) {
operator fun not(): Expanded {
return Expanded(value.not())
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(Expanded(false)) }
val expandedPadding = if (expanded.value) 48.dp else 0.dp
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier
.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(bottom = expandedPadding)
) {
Text(text = "Hello, $name")
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
Bundle에 추가할 수 없는 항목을 저장하려는 경우에는 세 가지 방법이 있다.
가장 간단한 방법으로, @Parcelize주석을 추가하여 객체를 parcelable로 만들어 Bundle에 제공하는 것이다.
@Parcelize
data class Expanded(val value: Boolean) : Parcelable {
operator fun not(): Expanded {
return Expanded(value.not())
}
}
어떤 이유로 @Parcelize가 적합하지 않을 경우
mapSaver를 사용하여 시스템이 Bundle에 저장할 수 있는 값 집합으로 객체를 변환하는 규칙을 정의할 수 있다.
data class Expanded(val value: Boolean) {
operator fun not(): Expanded {
return Expanded(value.not())
}
companion object {
val MapSaver = run {
val valueKey = "Value"
mapSaver(
save = { mapOf(valueKey to it.value) },
restore = { Expanded(it[valueKey] as Boolean) }
)
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable(stateSaver = Expanded.MapSaver) { mutableStateOf(Expanded(false)) }
...
}
listSaver를 사용하고 index를 키로 사용하면 mapSaver처럼 별도의 키를 정의할 필요가 없다.
data class Expanded(val value: Boolean) {
operator fun not(): Expanded {
return Expanded(value.not())
}
companion object {
val ListSaver = listSaver<Expanded, Any>(
save = { listOf(it.value) },
restore = { Expanded(it[0] as Boolean) }
)
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable(stateSaver = Expanded.ListSaver) { mutableStateOf(Expanded(false)) }
...
}