Compose 공식문서를 참고하여 작성하였습니다
UI 상태는 Activity나 프로세스 재생성으로 인해 손실될 수 있다
좋은 UX를 위해 UI 상태를 유지하는 것이 중요하다
(사용자가 타이핑을 하고 있는데 재생성으로 인해 타이핑하던 text가 없어지면..?!)
상태를 저장하고 복원하기 위해 다양한 API를 사용할 수 있고, 상태가 저장되는 위치와 로직에 따라 달라질 수 있다
상태가 UI에 위치한 경우
UI 상태가 Composable 함수나 Composition 범위에 지정된 PlainStateHolder에서 관리될 경우,
rememberSavable을 사용해 재생성 후 상태를 유지할 수 있다
rememberSavable 특징
UI 상태를 저장하고 복원
Activity, 프로세스 재생성 후에도 상태 유지
PrimitiveType은 기본적으로 저장 가능
PrimitiveType이 아닌 유형은 Parcelize, listSaver, mapSaver 등 사용
rememberSavable은 Bundle을 통해 savedInstanceState 메커니즘을 사용하여 상태 저장
@Composable
fun <T : Any> rememberSaveable(
vararg inputs: Any?,
saver: Saver<T, out Any> = autoSaver(),
key: String? = null,
init: () -> T
): T {
...
// SavedStateInstance 메커니즘을 사용해 상태를 저장하고 복원하는 역할
val registry = LocalSaveableStateRegistry.current
val holder = remember {
// value is restored using the registry or created via [init] lambda
val restored = registry?.consumeRestored(finalKey)?.let {
saver.restore(it)
}
val finalValue = restored ?: init()
// 상태를 저장하고 복원하는 데 필요한 정보를 담고 있는 클래스
SaveableHolder(saver, registry, finalKey, finalValue, inputs)
}
...
return value
}
예시) 채팅 버블 축소/확장을 저장하는 showDetails를 rememberSavable로 저장
@Composable
fun ChatBubble(
message: Message
) {
var showDetails by rememberSaveable { mutableStateOf(false) }
ClickableText(
text = AnnotatedString(message.content),
onClick = { showDetails =!showDetails }
)
if (showDetails) {
Text(message.timestamp)
}
}
예시) rememberLazyListState (LazyListState 저장)
@Composable
fun rememberLazyListState(
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
return rememberSaveable(saver = LazyListState.Saver) {
LazyListState(
initialFirstVisibleItemIndex,
initialFirstVisibleItemScrollOffset
)
}
}
rememberSavable을 사용할 때 주의할 점
Bundle은 사이즈 제한이 있어 사이즈가 큰 객체를 저장하면 런타임 에러가 발생할 수 있어, 사이즈가 큰 객체는 저장하지 않는 것이 좋다
ID나 key 같은 최소한의 상태만 저장하고 복잡한 UI 상태는 영구저장소를 사용하는 것이 좋다 (Room, SharedPreference ..)
상태가 비즈니스 로직에 위치한 경우
ViewModel은 Configuration Change에도 상태를 유지하지만 시스템에 의한 프로세스 종료 시에는 상태가 유지되지 않을 수 있다
이 경우, SavedStateHandle을 사용하거나 Local Storage를 사용해 상태를 유지할 수 있다
Bundle 메커니즘을 사용하는 SavedStateHandle에는 간단한 UI 요소 상태,
복잡하거나 큰 데이터를 저장할 때는 로컬 스토리지에 저장하는 것이 좋다
SavedStateHandle에는 UI 요소 상태를 저장하는 여러 API가 있다
saveable을 사용한 Compose State, getStateFlow를 사용한 StateFlow를 통해 효과적으로 상태를 관리할 수 있다
1. saveable API
UI 요소 상태를 MutableState로 읽고 쓰며 재생성 후에도 상태를 유지
saver를 사용해 기본 타입 외 복잡한 객체도 저장할 수 있다
@SavedStateHandleSaveableApi
fun <T> SavedStateHandle.saveable(
key: String,
stateSaver: Saver<T, out Any>,
init: () -> MutableState<T>
): MutableState<T> = saveable(
saver = mutableStateSaver(stateSaver),
key = key,
init = init
)
예시) MutableState로 관리되는 message
재생성 후에도 message 상태 유지
class ConversationViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(""))
}
private set
fun update(newMessage: TextFieldValue) {
message = newMessage
}
/*...*/
}
2. getStateFlow API
읽기 전용인 StateFlow 타입을 통해 상태 관리, key를 통해 새 값 방출
private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"
class ChannelViewModel(
channelsRepository: ChannelsRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
)
private val filteredChannels: Flow<List<Channel>> =
combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
filter(channels, type)
}.onStart { emit(emptyList()) }
fun setFiltering(requestType: ChannelsFilterType) {
savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
}
/*...*/
}
enum class ChannelsFilterType {
ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}
'Compose' 카테고리의 다른 글
[Compose] Compose 아키텍처 레이어링 (0) | 2025.02.23 |
---|---|
[Compose] Compose 아키텍처 (0) | 2025.02.23 |
[Compose] 상태 호이스팅(State Hoisting) (0) | 2025.02.21 |
[Compose] 상태 관리 (0) | 2025.02.10 |
[Compose] Compose Phases (0) | 2025.02.09 |