Compose 공식문서를 참고하여 작성하였습니다
State와 Composition
Compose에서는 State(상태)가 업데이트 될 때마다 Recomposition 실행
예시) 상태 변화가 없는 코드
@Composable
private fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("Name") }
)
}
}
예시) 상태를 업데이트 해 Recomposition이 발생하는 코드
@Composable
private fun HelloContent() {
var name by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = if (name.isEmpty()) "Hello!" else "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
그럼 Composable에서 상태는 어떻게 사용하는가?
Composable의 상태
1. mutableStateOf 사용
value 값을 가지는 holder를 리턴하는 함수
Compose가 read/write 관찰
값이 변경되면 값을 읽는 Composable의 Recomposition이 예약
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
@Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
MutableState 타입의 값을 정의하는 3가지 방법
val value1 = remember { mutableStateOf("") }
val value2 by remember { mutableStateOf("") }
val (value3, setValue) = remember { mutableStateOf("") }
2. remember나 rememberSavable을 사용해 상태 유지
초기 Composition에 값을 계산하고 저장
Recomposition 발생 시 저장된 값 재사용
key를 사용한다면, key 변경 시 값을 다시 계산하고 저장
remember API
remember API를 사용해 메모리에 객체를 저장
Configuration 변경 시 데이터 손실
처음 실행되면 calculation 람다를 호출하고 결과 저장
Recomposition 중 마지막 저장된 값을 반환
Composition을 종료할 때까지 값 캐시
key를 사용해 캐시된 값을 없애고 calculation 람다 블록 다시 실행
// Remember the value produced by calculation.
// calculation will only be evaluated during the composition
// Recomposition will always return the value produced by composition.
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
@Composable
inline fun <T> remember(
key1: Any?,
crossinline calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}
rememberSavable API
Bundle에 객체를 저장
Configuration 변경 시 데이터 유지
처음 실행되면 calculation 람다를 호출하고 결과 저장
inputs이 remember의 key와 동일
inputs 중 하나라도 변경되면 캐시값 없애고 calculation 람다 블록 다시 실행
@Composable
fun <T : Any> rememberSaveable(
vararg inputs: Any?,
saver: Saver<T, out Any> = autoSaver(),
key: String? = null,
init: () -> T
): T { ... }
3. 기타 상태 Type
상태를 유지하기 위해 MutableState를 사용하지 않고 다른 Type을 사용할 수 있다
Compose는 State 객체를 읽어올 때 자동으로 Recompose되기 때문에 State로 변환해야 한다
Compose에서 제공되는 함수로 State<T>로 변환하면 사용할 수 있다
Flow Type 사용 시
1. collectAsState 함수 사용
Flow에서 값을 수집해 State로 변환해 유지
Lifecycle을 인식하지 못함
@Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
2. collectAsStateWithLifecycle 함수 사용
Lifecycle을 인식하는 방식으로 Flow 값 수집해서 유지
@Composable
fun <T> Flow<T>.collectAsStateWithLifecycle(
initialValue: T,
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
): State<T> {
return produceState(initialValue, this, lifecycle, minActiveState, context) {
lifecycle.repeatOnLifecycle(minActiveState) {
if (context == EmptyCoroutineContext) {
this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
} else withContext(context) {
this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
}
}
}
}
LiveData 사용 시
observeAsState 함수 사용
@Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
val lifecycleOwner = LocalLifecycleOwner.current
val state = remember {
@Suppress("UNCHECKED_CAST") /* Initialized values of a LiveData<T> must be a T */
mutableStateOf(if (isInitialized) value as T else initial)
}
DisposableEffect(this, lifecycleOwner) {
val observer = Observer<T> { state.value = it }
observe(lifecycleOwner, observer)
onDispose { removeObserver(observer) }
}
return state
}
RxJava 사용 시
subscribeAsState 함수 사용
Single, Observable, Completable 타입을 State로 변환
그 외 다른 Observable한 Type
확장 함수를 빌드해서 사용
produceState API 사용
@Composable
fun <T> produceState(
initialValue: T,
vararg keys: Any?,
producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
LaunchedEffect(keys = keys) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
Stateful과 Stateless
Stateful
내부에서 상태가 생성되고 관리되는 Composable 함수
해당 함수를 호출하는 곳에서 상태를 관리하지 않아도 되지만 재사용성과 테스트하기 어렵다
@Composable
fun Stateful() {
// 상태를 직접 가지고 있음
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
Stateless
외부에서 상태가 생성되어 상태를 갖지 않는 Composable 함수
State Hoisting(상태 호이스팅)을 사용해 Stateless Composable 함수를 만든다
@Composable
fun Stateless(
count: Int,
onIncrement: () -> Unit
) {
// 상태를 외부에서 받아옴
Column {
Text("Count: $count")
Button(onClick = onIncrement) {
Text("Increment")
}
}
}
상태 호이스팅 (State Hoisting)
Stateless한 Composable을 만들기 위해 상태(State)를 Composable의 호출자로 올리는 패턴
일반적인 Hoisting 패턴
상태를 두 개의 매개변수로 변경하는 것
(값) value: T -----> 표시할 현재 값
(람다) onValueChange: (T) -> Unit ------> 새로운 T 값일 경우 콜백 이벤트
Hoisting된 상태의 특성
단일 정보 소스(Single source of Truth)
여러 곳에서 동일한 데이터를 관리하지 않고 상태가 하나의 출처에서 관리되어 데이터 일관성 유지
예시)
동일한 name을 각각 관리하는 경우
@Composable
fun Name1(name: String) {
var name by remember { mutableStateOf("Park") }
Text("이름: $name")
}
@Composable
fun Name2(name: String, onNameChange: (String) -> Unit) {
var name by remember { mutableStateOf("Park") }
Text(value = name, onValueChange = { name = it })
}
Main이라는 단일 소스에서 관리해 데이터 일관성 유지
@Composable
fun Main() {
var name by remember { mutableStateOf("Park") }
Name1(name)
Name2(name) { newName -> name = newName }
}
@Composable
fun Name1(name: String) {
Text("이름: $name")
}
@Composable
fun Name2(name: String, onNameChange: (String) -> Unit) {
Text(value = name, onValueChange = onNameChange)
}
캡슐화 (Encapsulated)
상태 변경 로직을 캡슐화
예시)
State Hoisting 전
(외부에서 상태 직접 변경 가능)
@Composable
fun MyCounter() {
var count by remember { mutableStateOf(0) } // 상태가 컴포넌트 내부
Text("Count: $count")
Button(onClick = { count++ }) { Text("Increment") } // 외부에서 상태 직접 변경 가능
}
State Hoisting 후
(외부에서 콜백을 통해 상태 전달)
@Composable
fun MyScreen() {
var count by remember { mutableStateOf(0) } // 상태 정의
MyCounter(count, onIncrement = { count++ }) // 상태 변경 로직을 전달해 MyCounter 내부로부터 캡슐화
}
@Composable
fun MyCounter(count: Int, onIncrement: () -> Unit) {
Text("Count: $count")
Button(onClick = onIncrement) { Text("Increment") } // 외부에서 onIncrement 콜백을 통해 상태 변경 요청
}
공유 가능 (Shareable)
해당 상태를 여러 하위 컴포넌트에서 공유할 수 있다
Intercept 가능 (Interceptable)
Stateless Composable 함수의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정할 수 있다
예시)
Stateless한 Name2 함수에서 콜백으로 값을 받아 조건에 따라 상태 변경
@Composable
fun Main() {
var name by remember { mutableStateOf("Park") }
Name1(name)
Name2(name) { newName ->
if (newName.isNotEmpty()) {
name = newName
}
}
}
분리 (Decoupled)
Stateless Composable의 상태는 어디에나 저장할 수 있고 유연하게 관리할 수 있다
하위 Composable은 상태를 받아서 사용만 하고 이벤트를 상위 컴포넌트에 전달하는 역할만 수행
상태 호이스팅을 할 경우 상태 위치를 정하는 3가지 방법
1. 상태를 사용하는 모든 Composable의 가장 낮은 공통 상위 요소(최소 공통적인 Parent)로 끌어올려야 한다
Composable A, B, C가 하나의 상태를 사용할 경우 A와 B의 공통 부모인 AB와, AB와 C의 공통 부모인 ABC가 있을 때
상태는 ABC까지 끌어올려야 한다
2. 상태가 변경될 수 있는 가장 높은 수준으로 끌어올려야 한다
Composable A에서 name 상태를 변경할 수 있고, A의 부모인 B에서도 name 상태를 변경할 수 있다면,
name 상태는 B까지 끌어올려야 한다
3. 동일한 이벤트에 대한 응답으로 두 상태가 변경되는 경우 함께 끌어올려야 한다
하나의 네트워크 API요청으로 인해 동시에 2개의 상태가 변경되야 한다면, 각 상태를 모두 끌어올려야 동시에 업데이트 할 수 있다
Compose에서의 상태 복원
rememberSavable API 사용
remember는 Composition 내 상태를 유지하지만 Configuration Change 같은 상황에서 초기화된다
rememberSavable을 사용하면 상태를 유지할 수 있다
원리
SavedInstanceState(Bundle)를 사용해 Composable의 상태를 저장하고 재생성될 때 상태 복원
remember vs rememberSavable
remember | rememberSavable | |
Composition 내 유지 | O | O |
재생성 | 초기화 | 유지 |
원리 | 메모리에 저장 | SavedInstanceState(Bundle)에 저장 |
상태를 저장하는 방법
기본적으로 Bundle에 추가되는 데이터 유형은 자동으로 저장
Bundle로 추가할 수 없는 데이터의 경우 추가할 수 있는 3가지 옵션
Parcelize 사용
Parcelable 인터페이스를 자동으로 구현해주는 Kotlin 플러그인
어노테이션을 붙이면 객체가 Parcelable이 되어 Bundle로 제공될 수 있다
@Parcelize
data class City(val name: String, val country: String) : Parcelable
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
Saver 사용
interface Saver<Original, Saveable : Any> {
/**
* Convert the value into a saveable one. If null is returned the value will not be saved.
*/
fun SaverScope.save(value: Original): Saveable?
/**
* Convert the restored value back to the original Class. If null is returned the value will
* not be restored and would be initialized again instead.
*/
fun restore(value: Saveable): Original?
}
MapSaver 사용
Parcelize를 사용하지 못할 경우, Key-Value 쌍으로 데이터 저장하는 Map 객체를 Bundle에 저장하고 복원
// 내부적으로 listSaver 활용
fun <T> mapSaver(
save: SaverScope.(value: T) -> Map<String, Any?>,
restore: (Map<String, Any?>) -> T?
) = listSaver<T, Any?>(...)
예시)
data class City(val name: String, val country: String)
val CitySaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name, countryKey to it.country) },
restore = { City(it[nameKey] as String, it[countryKey] as String) }
)
}
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
ListSaver 사용
List 객체를 Bundle에 저장하고 복원
fun <Original, Saveable> listSaver(
save: SaverScope.(value: Original) -> List<Saveable>,
restore: (list: List<Saveable>) -> Original?
): Saver<Original, Any> = @Suppress("UNCHECKED_CAST") Saver(...)
예시)
data class City(val name: String, val country: String)
val CitySaver = listSaver<City, Any>(
save = { listOf(it.name, it.country) },
restore = { City(it[0] as String, it[1] as String) }
)
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
'Compose' 카테고리의 다른 글
[Compose] UI 상태 저장 및 복원 (0) | 2025.02.22 |
---|---|
[Compose] 상태 호이스팅(State Hoisting) (0) | 2025.02.21 |
[Compose] Compose Phases (0) | 2025.02.09 |
[Compose] Composable 수명 주기 (0) | 2025.02.09 |
[Compose] Compose 이해하기 (0) | 2025.02.08 |