📌 androidx.compose.runtime.saveable 패키지란?
Jetpack Compose에서 구성 변경(Configuration Change) 시 UI 상태를 유지하려면 androidx.compose.runtime.saveable 패키지를 활용해야 합니다. 이 패키지는 화면 회전, 다크모드 변경, 프로세스 종료 후 복원 등에서 UI 상태가 유지될 수 있도록 도와줍니다.
✅ 주요 API
API | 설명 |
---|---|
rememberSaveable | 구성 변경 시에도 상태를 유지하도록 remember를 확장한 함수 |
Saver | 커스텀 객체를 저장할 수 있도록 변환하는 클래스 |
mapSaver | Saver의 간단한 버전으로, Map<String, Any>을 통해 객체를 저장 및 복원 |
listSaver | Saver의 간단한 버전으로, List를 사용하여 객체 저장 및 복원 |
1️⃣ rememberSaveable 기본 사용법
remember는 구성 변경 시 상태가 유지되지 않지만, rememberSaveable을 사용하면 자동으로 상태가 저장됩니다.
@Composable
fun RememberSaveableExample() {
var text by rememberSaveable { mutableStateOf("") }
Column {
TextField(value = text, onValueChange = { text = it })
Text(text = "입력한 값: $text")
}
}
📌 동작
- 일반적인 remember와 동일하게 동작하지만, 화면 회전 후에도 상태가 유지됨.
- 내부적으로 Bundle을 사용하여 상태를 저장하므로 기본 데이터 타입(Int, String, Boolean, List 등)만 저장 가능.
2️⃣ rememberSaveable과 MutableStateList 사용
SnapshotStateList 같은 Compose 전용 컬렉션(mutableStateListOf())도 저장 가능합니다.
@Composable
fun RememberSaveableListExample() {
val items = rememberSaveable { mutableStateListOf("Apple", "Banana", "Cherry") }
Column {
items.forEach { Text(it) }
Button(onClick = { items.add("Orange") }) {
Text("Add Orange")
}
}
}
📌 동작
- mutableStateListOf()를 rememberSaveable과 함께 사용하면 리스트 변경 사항도 저장됨.
3️⃣ Saver를 사용한 커스텀 객체 저장
rememberSaveable은 기본 데이터 타입만 저장할 수 있기 때문에, 사용자 정의 객체는 Saver를 이용하여 변환해야 합니다.
🚨 오류 발생 예시
data class User(val name: String, val age: Int)
@Composable
fun ErrorExample() {
var user by rememberSaveable { mutableStateOf(User("Alice", 25)) } // ❌ ERROR
}
📌 오류 이유
- User는 Bundle로 저장할 수 없는 객체이므로 rememberSaveable에서 사용할 수 없음.
✅ 해결 방법: Saver 사용
@Composable
fun RememberSaveableWithSaver() {
val userSaver = Saver<User, List<Any>>(
save = { listOf(it.name, it.age) }, // 저장할 때 리스트로 변환
restore = { User(it[0] as String, it[1] as Int) } // 복원할 때 객체로 변환
)
var user by rememberSaveable(stateSaver = userSaver) { mutableStateOf(User("Alice", 25)) }
Column {
Text("이름: ${user.name}, 나이: ${user.age}")
Button(onClick = { user = user.copy(age = user.age + 1) }) {
Text("나이 증가")
}
}
}
📌 동작
- User 객체를 List로 변환하여 저장하고, 다시 User로 복원.
- 화면 회전 후에도 유지됨.
4️⃣ mapSaver를 사용한 객체 저장
Saver를 더 간단하게 만들고 싶다면 mapSaver 를 사용할 수도 있습니다.
val userSaver = mapSaver(
save = { user -> mapOf("name" to user.name, "age" to user.age) },
restore = { map -> User(map["name"] as String, map["age"] as Int) }
)
@Composable
fun RememberSaveableWithMapSaver() {
var user by rememberSaveable(stateSaver = userSaver) { mutableStateOf(User("Bob", 30)) }
Column {
Text("이름: ${user.name}, 나이: ${user.age}")
Button(onClick = { user = user.copy(age = user.age + 1) }) {
Text("나이 증가")
}
}
}
📌 장점
- mapSaver를 사용하면 Saver보다 더 간단한 코드로 객체 저장 가능.
🎯 rememberSaveable은 화면 회전이나 프로세스 종료 후에도 UI 상태를 유지하는 핵심 API이므로, 상태 저장이 필요한 Compose UI에서는 적극적으로 활용해야 합니다! 🚀
📌 중첩된 커스텀 객체를 androidx.compose.runtime.saveable API로 저장하는 방법
androidx.compose.runtime.saveable 패키지를 이용하여 커스텀 객체가 다른 커스텀 객체를 멤버로 포함하고 있을 때도 저장할 수 있습니다.
이를 위해 Saver 또는 mapSaver를 활용하여 중첩된 객체를 개별 필드로 변환 및 복원해야 합니다.
1️⃣ Saver를 사용한 중첩 객체 저장
🚨 일반적인 rememberSaveable 사용 시 발생하는 문제
아래와 같은 중첩된 데이터 클래스를 rememberSaveable로 저장하면 에러가 발생합니다.
data class Address(val city: String, val zipCode: String)
data class User(val name: String, val age: Int, val address: Address)
@Composable
fun ErrorExample() {
var user by rememberSaveable { mutableStateOf(User("Alice", 25, Address("Seoul", "12345"))) } // ❌ ERROR
}
🚨 이유
- rememberSaveable은 Bundle에 저장 가능한 기본 데이터 타입만 지원 (Int, String, Boolean, List, etc.)
- User 내부의 Address 객체는 Bundle로 직접 저장할 수 없음.
- 따라서 객체를 저장 가능한 데이터로 변환해야 함.
✅ 해결 방법: Saver로 중첩 객체 저장
중첩된 커스텀 객체(Address)를 포함하는 User를 저장하려면 각 객체를 List로 변환해야 합니다.
val userSaver = Saver<User, List<Any>>(
save = { listOf(it.name, it.age, listOf(it.address.city, it.address.zipCode)) },
restore = {
val addressData = it[2] as List<String>
User(it[0] as String, it[1] as Int, Address(addressData[0], addressData[1]))
}
)
@Composable
fun RememberSaveableNestedObject() {
var user by rememberSaveable(stateSaver = userSaver) {
mutableStateOf(User("Alice", 25, Address("Seoul", "12345")))
}
Column {
Text("이름: ${user.name}, 나이: ${user.age}")
Text("주소: ${user.address.city}, ${user.address.zipCode}")
Button(onClick = {
user = user.copy(address = user.address.copy(city = "Busan")) // ✅ 새 객체 할당
}) {
Text("도시 변경")
}
}
}
📌 Saver로 해결한 원리
- addressSaver: Address 객체를 [city, zipCode] 리스트로 변환 후 저장.
- userSaver:
- User 객체를 [name, age, [city, zipCode]] 리스트로 변환 후 저장.
- 복원 시 리스트에서 name, age를 가져오고, addressData를 Address 객체로 변환.
- rememberSaveable(stateSaver = userSaver)를 사용하여 상태를 저장.
2️⃣ mapSaver를 사용한 중첩 객체 저장
mapSaver를 활용하면 Saver보다 더 간결한 코드로 중첩된 객체를 저장할 수 있습니다.
val userSaver = mapSaver(
save = { user ->
mapOf(
"name" to user.name,
"age" to user.age,
"address" to mapOf("city" to user.address.city, "zipCode" to user.address.zipCode)
)
},
restore = { map ->
val addressMap = map["address"] as Map<String, String>
User(map["name"] as String, map["age"] as Int, Address(addressMap["city"]!!, addressMap["zipCode"]!!))
}
)
@Composable
fun RememberSaveableWithMapSaver() {
var user by rememberSaveable(stateSaver = userSaver) {
mutableStateOf(User("Bob", 30, Address("Incheon", "54321")))
}
Column {
Text("이름: ${user.name}, 나이: ${user.age}")
Text("주소: ${user.address.city}, ${user.address.zipCode}")
Button(onClick = {
user = user.copy(address = user.address.copy(city = "Daegu")) // ✅ UI 업데이트 반영
}) {
Text("도시 변경")
}
}
}
🚀 mapSaver를 활용한 방식
- addressSaver는 Map<String, String> 형태로 저장.
- userSaver는 Map<String, Any>을 사용하여 중첩된 Address도 Map 형태로 저장.
- 복원 시 Map에서 데이터를 추출하여 User와 Address를 다시 생성.
androidx.compose.ui.text 패키지의 Saver.kt 를 참고하여 코드를 좀 더 가독성 있게 만들기
📌 Savers.kt에서 사용되는 save 확장 함수 이해하기
Jetpack Compose의 androidx.compose.ui.text.Savers.kt 파일을 보면, Saver<T, Saveable>을 좀 더 간결하게 다룰 수 있도록 확장 함수가 정의되어 있습니다.
/**
* Utility function to be able to save nullable values. It also enables not to use with() scope
* for readability/syntactic purposes.
*/
internal fun <T : Saver<Original, Saveable>, Original, Saveable> save(
value: Original?,
saver: T,
scope: SaverScope
): Any {
return value?.let { with(saver) { scope.save(value) } } ?: false
}
/**
* Utility function to restore nullable values. It also enables not to use with() scope
* for readability/syntactic purposes.
*/
internal inline fun <T : Saver<Original, Saveable>, Original, Saveable, reified Result> restore(
value: Saveable?,
saver: T
): Result? {
// Most of the types we save are nullable. However, value classes are usually not but instead
// have a special Unspecified value. In that case we delegate handling of the "false"
// value restoration to the corresponding saver that will restore "false" as an Unspecified
// of the corresponding type.
if (value == false && saver !is NonNullValueClassSaver<*, *>) return null
return value?.let { with(saver) { restore(value) } as Result }
}
/**
* Use for non-null value classes where the Unspecified value needs to be separately handled,
* for example as in the [OffsetSaver]
*/
private interface NonNullValueClassSaver<Original, Saveable : Any> : Saver<Original, Saveable>
private fun <Original, Saveable : Any> NonNullValueClassSaver(
save: SaverScope.(value: Original) -> Saveable?,
restore: (value: Saveable) -> Original?
): NonNullValueClassSaver<Original, Saveable> {
return object : NonNullValueClassSaver<Original, Saveable> {
override fun SaverScope.save(value: Original) = save.invoke(this, value)
override fun restore(value: Saveable) = restore.invoke(value)
}
}
// ...
save
확장함수 : 널 값을 갖는 상태도 저장할 수 있도록 구현됨. 널 값은 기본적으로 false 로 저장restore
확장함수 : 널 값을 갖는 상태도 복원할 수 있도록 구현됨. 널 값은 기본적으로 false 로 저장NonNullValueClassSaver
: 널 값을 멤버 프로퍼티로 갖지 않는 클래스를 복원할때 널 값으로 저장된 (false 로 저장된) 값을 복원하려는 경우, 널 값이 아닌 Unspecified 와 같은 값으로 복원해야 하는데 이러한 처리를 해주는 클래스
이와 비슷한 확장함수를 저희 코드에도 선언하여 상태 값의 저장과 복원을 좀 더 가독성 있는 코드로 개선할 수 있습니다.
🔽 save 확장 함수 적용 후 코드 간결화
val addressSaver = Saver<Address, List<String>>(
save = { listOf(it.city, it.zipCode) },
restore = { Address(it[0], it[1]) }
)
val userSaver = Saver<User, List<Any>>(
// 각 클래스 별로 상태 저장 로직을 분리하여 좀 더 가독성 있게 개선됨
save = { listOf(it.name, it.age, addressSaver.save(it.address)) },
restore = {
val addressData = it[2] as List<String>
User(it[0] as String, it[1] as Int, addressSaver.restore(addressData)!!)
}
)
'천복만복 프로그래밍 > 천복만복 안드로이드' 카테고리의 다른 글
[Compose] TooltipBox (0) | 2025.03.01 |
---|---|
[Compose] SnapshotStateList (0) | 2025.02.23 |
[Compose] Alignment line (0) | 2025.02.21 |
인텐트 필터 매칭 동작 정리 (0) | 2025.01.02 |
Edge to Edge (2) | 2024.10.02 |