Compose에 UI 상태 저장

상태가 호이스팅된 위치와 필요한 로직에 따라 서로 다른 API를 사용하여 UI 상태를 저장하고 복원할 수 있습니다. 모든 앱은 이를 가장 효과적으로 달성하기 위한 API 조합을 사용합니다.

모든 Android 앱은 활동 또는 프로세스 재생성으로 인해 UI 상태가 손실될 수 있습니다. 이러한 상태 손실은 다음과 같은 이벤트로 인해 발생할 수 있습니다.

이러한 이벤트 후에 상태를 보존하는 것은 긍정적인 사용자 경험을 제공하는 데 있어 중요합니다. 어느 상태가 보존되도록 선택해야 하는지는 앱의 고유한 사용자 흐름에 따라 달라집니다. 권장사항은 적어도 사용자 입력 및 탐색 관련 상태는 유지하는 것입니다. 목록의 스크롤 위치, 사용자가 더 자세히 알고자 하는 항목의 ID, 진행 중인 사용자 환경설정 선택, 텍스트 필드의 입력 등을 예로 들 수 있습니다.

이 페이지에서는 상태가 호이스팅되는 대상 위치와 상태를 필요로 하는 로직에 따라 UI 상태를 저장하는 데 사용할 수 있는 API를 요약합니다.

UI 로직

상태가 UI에서 구성 가능한 함수나 컴포지션으로 범위가 지정된 일반 상태 홀더 클래스로 호이스팅되는 경우 rememberSaveable을 사용하여 여러 활동에서, 그리고 프로세스 재생성 후에도 상태를 유지할 수 있습니다.

다음 스니펫에서 rememberSaveable은 단일 불리언 UI 요소 상태를 저장하는 데 사용됩니다.

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

그림 1. 탭하면 펼쳐지고 접히는 채팅 메시지 대화창

showDetails은 채팅 풍선이 접혔는지 아니면 펼쳐졌는지를 저장하는 불리언 변수입니다.

rememberSaveable은 저장된 인스턴스 상태 메커니즘을 통해 UI 요소 상태Bundle에 저장합니다.

기본 유형을 자동으로 번들에 저장할 수 있습니다. 상태가 데이터 클래스와 같이 기본 유형이 아닌 유형에 저장되어 있다면 Parcelize 주석을 사용하거나, listSavermapSaver 등의 Compose API를 사용하거나, Compose 런타임 Saver 클래스를 확장하여 맞춤 Saver 클래스를 구현하는 등의 다른 저장 메커니즘을 사용할 수 있습니다. 이러한 메서드에 관한 자세한 내용은 상태 저장 방법 문서를 참고하세요.

다음 스니펫에서 rememberLazyListState Compose API는 rememberSaveable을 사용하여 LazyColumn 또는 LazyRow의 스크롤 상태로 구성되는 LazyListState를 저장합니다. 또한 스크롤 상태를 저장하고 복원할 수 있는 맞춤 Saver인 LazyListState.Saver를 사용합니다. 활동 또는 프로세스가 재생성된 후(예: 기기 방향 변경과 같은 구성 변경 후) 스크롤 상태가 보존됩니다.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

권장사항

rememberSaveableBundle을 사용하여 UI 상태를 저장합니다. Bundle은 활동에서 이루어지는 onSaveInstanceState() 호출과 같이 Bundle에 쓰기를 수행하기도 하는 다른 API와 공유됩니다. 단, 이 Bundle의 크기는 제한적이므로 여기에 큰 객체를 저장하면 런타임에서 TransactionTooLarge 예외가 발생할 수 있습니다. 특히 앱 전체에서 동일한 Bundle이 사용되는 단일 Activity 앱에서 문제가 될 수 있습니다.

이러한 유형의 비정상 종료를 방지하려면 번들에 크고 복잡한 객체나 객체 목록을 저장하지 않아야 합니다.

대신 ID나 키와 같이 필요한 최소 상태를 저장하고 더 복잡한 UI 상태의 복원을 영구 저장소와 같은 다른 메커니즘에 위임하는 데 이 데이터를 사용합니다.

이러한 디자인 옵션은 앱의 구체적인 사용 사례와 사용자가 앱에 기대하는 동작 방식에 따라 달라집니다.

상태 복원 확인

활동 또는 프로세스가 재생성되었을 때 rememberSaveable을 사용하여 Compose 요소에 저장된 상태가 올바르게 복원되는지 확인할 수 있습니다. 이를 위한 API가 있습니다(예: StateRestorationTester). 자세한 내용은 테스트 문서를 참고하세요.

비즈니스 로직

UI 요소 상태가 비즈니스 로직에서 필요하기 때문에 ViewModel로 호이스팅된 경우 ViewModel의 API를 사용할 수 있습니다.

Android 애플리케이션에서 ViewModel을 사용하는 것의 주요 이점 중 하나는 구성 변경을 무료로 처리한다는 것입니다. 구성 변경이 발생하여 활동이 소멸되었다가 재생성된 경우 ViewModel로 호이스팅된 UI 상태는 메모리에 유지됩니다. 재생성 후에는 기존 ViewModel 인스턴스가 새 활동 인스턴스에 연결됩니다.

그러나 ViewModel 인스턴스는 시스템에서 시작된 프로세스 종료가 발생한 경우에는 유지되지 않습니다. UI 상태가 유지되도록 하려면 SavedStateHandle API를 포함하는 ViewModel의 저장된 상태 모듈을 사용하세요.

권장사항

SavedStateHandle은 UI 상태를 저장하기 위해 Bundle 메커니즘도 사용하므로 간단한 UI 요소 상태를 저장하는 데만 사용해야 합니다.

비즈니스 규칙을 적용하고 UI 이외의 애플리케이션 레이어에 액세스함으로써 생성되는 화면 UI 상태는 그 복잡도와 크기 때문에 SavedStateHandle에 저장해서는 안 됩니다. 복잡하거나 큰 데이터를 저장할 때는 로컬 영구 스토리지를 비롯한 여러 메커니즘을 사용할 수 있습니다. 프로세스가 재생성된 후에는 SavedStateHandle에 저장되었던 복원된 임시 상태(있는 경우)를 사용하여 화면이 재생성되고 화면 UI 상태가 데이터 영역으로부터 다시 생성됩니다.

SavedStateHandle API

SavedStateHandle에는 UI 요소 상태를 저장하는 여러 API가 있습니다. 그중에서도 다음 API가 중요합니다.

Compose State saveable()
StateFlow getStateFlow()

Compose State

SavedStateHandlesaveable API를 사용하여 UI 요소 상태를 MutableState로 읽고 씁니다. 그러면 여러 활동에서, 그리고 프로세스 재생성 후에도 최소한의 코드 설정으로 UI 요소 상태가 유지됩니다.

saveable API는 추가 설정 없이 기본 유형을 지원하며, rememberSaveable()처럼 맞춤 Saver를 사용하기 위해 stateSaver 매개변수를 받습니다.

다음 스니펫에서 message는 사용자 입력 유형을 TextField에 저장합니다.

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

saveable API 사용에 관한 자세한 내용은 SavedStateHandle 문서를 참고하세요.

StateFlow

getStateFlow()를 사용하여 UI 요소 상태를 저장하고 SavedStateHandle에서의 흐름으로 사용합니다. StateFlow는 읽기 전용이며, 이 API에서는 개발자가 키를 지정해야 흐름을 대체하여 새 값을 내보낼 수 있습니다. 키를 구성했으면 StateFlow를 검색하여 최신 값을 수집할 수 있습니다.

다음 스니펫에서 savedFilterType은 채팅 앱의 채팅 채널 목록에 적용된 필터 유형을 저장하는 StateFlow 변수입니다.

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
}

사용자가 새 필터 유형을 선택할 때마다 setFiltering이 호출됩니다. 이렇게 하면 SavedStateHandle에 키 _CHANNEL_FILTER_SAVED_STATE_KEY_와 함께 새 값이 저장됩니다. savedFilterType은 키에 저장된 최신 값을 내보내는 흐름입니다. filteredChannels는 채널 필터링을 수행하기 위해 흐름을 수신합니다.

getStateFlow() API에 관한 자세한 내용은 SavedStateHandle 문서를 참고하세요.

요약

다음 표에는 이 섹션에서 다룬 API와 각 API를 사용하여 UI 상태를 저장해야 하는 경우가 요약되어 있습니다.

이벤트 UI 로직 ViewModel의 비즈니스 로직
구성 변경 rememberSaveable 자동
시스템에서 시작된 프로세스 종료 rememberSaveable SavedStateHandle

사용할 API는 상태가 저장된 위치와 상태를 필요로 하는 로직에 따라 달라집니다. UI 로직에서 사용되는 상태의 경우 rememberSaveable을 사용합니다. 비즈니스 로직에서 사용되는 상태의 경우 ViewModel에 저장한다면 SavedStateHandle을 사용하여 저장합니다.

번들 API(rememberSaveableSavedStateHandle)는 적은 양의 UI 상태를 저장하는 데 사용해야 합니다. 이 데이터는 다른 저장 메커니즘과 함께 UI를 이전 상태로 복원하는 데 필요한 최소한의 데이터입니다. 예를 들어 사용자가 번들에서 보고 있는 프로필의 ID를 저장하면 프로필 세부정보와 같은 대용량 데이터는 데이터 영역에서 가져올 수 있습니다.

UI 상태를 저장하는 다양한 방법에 관한 자세한 내용은 아키텍처 가이드의 UI 상태 저장 문서데이터 영역 페이지를 참고하세요.