Jetpack Compose에서 구성 가능한 함수는 remember 함수를 사용하여 상태를 보유하는 경우가 많습니다. 기억된 값은
상태 및 Jetpack Compose에 설명된 대로 리컴포지션 전반에서 재사용할 수 있습니다.
remember는 리컴포지션 전반에서 값을 유지하는 도구 역할을 하지만 상태는 컴포지션의 수명보다 오래 지속되어야 하는 경우가 많습니다. 이 페이지에서는
remember, retain, rememberSaveable,
및 rememberSerializable API의 차이점, API 선택 시기, Compose에서 기억되고 유지되는 값을 관리하는
권장사항을 설명합니다.
올바른 수명 선택
Compose에는 컴포지션 전반에서 상태를 유지하는 데 사용할 수 있는 여러 함수(remember, retain, rememberSaveable, rememberSerializable)가 있습니다. 이러한 함수는 수명과 의미가 다르며 각각 특정 종류의 상태를 저장하는 데 적합합니다. 차이점은 다음 표에 설명되어 있습니다.
|
|
|
|
|---|---|---|---|
값이 리컴포지션 후에도 유지되나요? |
✅ |
✅ |
✅ |
값이 활동 재생성 후에도 유지되나요? |
❌ |
✅ 동일한 ( |
✅ 동등한 ( |
값이 프로세스 종료 후에도 유지되나요? |
❌ |
❌ |
✅ |
지원되는 데이터 유형 |
전체 |
활동이 소멸될 때 누수되는 객체를 참조해서는 안 됩니다. |
직렬화 가능해야 합니다. |
사용 사례 |
|
|
|
remember
remember는 Compose에서 상태를 저장하는 가장 일반적인 방법입니다. remember가
처음 호출되면 지정된 계산이 실행되고
기억됩니다. 즉, 컴포저블에서 향후 재사용할 수 있도록 Compose에서 저장됩니다. 컴포저블이 리컴포지션되면 코드를 다시 실행하지만 remember에 대한 호출은 계산을 다시 실행하는 대신 이전 컴포지션에서 값을 반환합니다.
구성 가능한 함수의 각 인스턴스에는 위치별 메모이제이션 이라고 하는 자체 기억된 값 집합이 있습니다. 기억된 값이 리컴포지션 전반에서 사용하기 위해 메모이제이션되면 컴포지션 계층 구조의 위치에 연결됩니다. 구성 가능한 함수가 여러 위치에서 사용되는 경우 컴포지션 계층 구조의 각 인스턴스에는 자체 기억된 값 집합이 있습니다.
기억된 값이 더 이상 사용되지 않으면 삭제 되고 레코드가 삭제됩니다. 기억된 값은 컴포지션 계층 구조에서 삭제되거나 (key 컴포저블 또는 MovableContent를 사용하지 않고 다른 위치로 이동하기 위해 값이 삭제되고 다시 추가되는 경우 포함) 다른 key 매개변수로 호출될 때 삭제됩니다.
사용 가능한 선택사항 중에서 remember는 수명이 가장 짧고 이 페이지에 설명된 4가지 메모이제이션 함수 중에서 가장 먼저 값을 삭제합니다.
따라서 다음 작업에 가장 적합합니다.
- 스크롤 위치 또는 애니메이션 상태와 같은 내부 상태 객체 만들기
- 각 리컴포지션에서 비용이 많이 드는 객체 재생성 방지
하지만 다음은 피해야 합니다.
- 기억된 객체는 활동 구성 변경 및 시스템에서 시작된 프로세스 종료 전반에서 삭제되므로
remember를 사용하여 사용자 입력을 저장하지 마세요.
rememberSaveable 및 rememberSerializable
rememberSaveable 및 rememberSerializable은 remember를 기반으로 빌드됩니다. 이 가이드에서 설명된 메모이제이션 함수 중에서 수명이 가장 깁니다.
리컴포지션 전반에서 객체를 위치별로 메모이제이션하는 것 외에도 구성 변경 및 프로세스 종료 (시스템이 백그라운드에 있는 동안 앱의 프로세스를 종료하는 경우, 일반적으로 포그라운드 앱의 메모리를 확보하거나 사용자가 실행 중인 앱에서 권한을 취소하는 경우)를 포함하여 활동 재생성 전반에서 복원할 수 있도록 값을 저장 할 수도 있습니다.
rememberSerializable 은 rememberSaveable과 동일한 방식으로 작동하지만 kotlinx.serialization 라이브러리로 직렬화할 수 있는 복잡한 유형의 유지를 자동으로 지원합니다. 유형이 @Serializable로 표시되거나 표시될 수 있는 경우 rememberSerializable을 선택하고 다른 모든 경우에는 rememberSaveable을 선택합니다.
따라서 rememberSaveable 및 rememberSerializable은 모두 텍스트 필드 항목, 스크롤 위치, 전환 상태 등을 포함하여 사용자 입력과 연결된 상태를 저장하는 데 적합합니다. 사용자가 자신의 위치를 잃지 않도록 이 상태를 저장해야 합니다. 일반적으로 rememberSaveable 또는 rememberSerializable을 사용하여 앱에서 데이터베이스와 같은 다른 영구 데이터 소스에서 가져올 수 없는 상태를 메모이제이션해야 합니다.
rememberSaveable 및 rememberSerializable은 메모이제이션된 값을 Bundle로 직렬화하여 저장합니다. 여기에는 두 가지 결과가 있습니다.
- 메모이제이션하는 값은 기본형 (
Int,Long,Float,Double포함),String또는 이러한 유형의 배열 중 하나 이상으로 나타낼 수 있어야 합니다. - 저장된 값이 복원되면 컴포지션에서 이전에 사용한 것과 동일한 참조(
===)가 아니라 동등한 (==) 새 인스턴스가 됩니다.
kotlinx.serialization을 사용하지 않고 더 복잡한 데이터 유형을 저장하려면 맞춤 Saver를 구현하여 객체를 지원되는 데이터 유형으로 직렬화하고 역직렬화할 수 있습니다. Compose는 State, List, Map, Set 등과 같은 일반적인 데이터 유형을 기본적으로 이해하고 이러한 유형을 자동으로 지원되는 유형으로 변환합니다. 다음은 Size 클래스의 Saver 예입니다. listSaver를 사용하여 Size의 모든 속성을 목록에 패킹하여 구현됩니다.
data class Size(val x: Int, val y: Int) { object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver( save = { listOf(it.x, it.y) }, restore = { Size(it[0], it[1]) } ) } @Composable fun rememberSize(x: Int, y: Int) { rememberSaveable(x, y, saver = Size.Saver) { Size(x, y) } }
retain
retain API는 값을 메모이제이션하는 기간 측면에서 remember와
rememberSaveable/rememberSerializable 사이에 있습니다. 유지된 값은 기억된 값과 다른 수명 주기를 경험하므로 이름이 다릅니다.
값이 유지되면 위치별로 메모이제이션되고 앱의 수명 주기에 연결된 별도의 수명 주기가 있는
보조 데이터 구조에 저장됩니다. 유지된 값은 직렬화되지 않고 구성 변경 후에도 유지될 수 있지만 프로세스 종료 후에는 유지될 수 없습니다. 컴포지션 계층 구조가 다시 생성된 후 값이 사용되지 않으면 유지된 값이 폐기 됩니다 (이는 retain의 삭제와 동일함).
rememberSaveable보다 수명이 짧은 대신 retain은 람다 표현식, 흐름, 비트맵과 같은 큰 객체 등 직렬화할 수 없는 값을 유지할 수 있습니다. 예를 들어 retain을 사용하여 미디어 플레이어 (예: ExoPlayer)를 관리하여 구성 변경 중에 미디어 재생이 중단되지 않도록 할 수 있습니다.
@Composable fun MediaPlayer() { // Use the application context to avoid a memory leak val applicationContext = LocalContext.current.applicationContext val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() } // ... }
retain과 ViewModel 비교
핵심적으로 retain과 ViewModel은 모두 구성 변경 전반에서 객체 인스턴스를 유지하는 가장 일반적으로 사용되는 기능에서 유사한 기능을 제공합니다. retain 또는 ViewModel을 선택하는 것은 유지하는 값의 유형, 범위 지정 방법, 추가 기능이 필요한지 여부에 달려 있습니다.
ViewModel은 일반적으로 앱의 UI와 데이터 레이어 간의 통신을 캡슐화하는 객체입니다. 이를 통해 구성 가능한 함수에서 로직을 이동하여 테스트 가능성을 개선할 수 있습니다. ViewModel는 ViewModelStore 내에서 싱글톤으로 관리되며 유지된 값과 수명이 다릅니다. ViewModel은 ViewModelStore가
소멸될 때까지 활성 상태를 유지하지만 유지된 값은 콘텐츠가 컴포지션에서 영구적으로 삭제될 때 폐기됩니다 (예를 들어 구성 변경의 경우 UI 계층 구조가 다시 생성되고 컴포지션이 다시 생성된 후 유지된 값이 사용되지 않으면 유지된 값이 폐기됨).
ViewModel 에는 Dagger 및 Hilt를 사용한 종속 항목 삽입, SavedState와의 통합, 백그라운드 작업을 실행하기 위한 기본 코루틴 지원을 위한 기본 통합도 포함되어 있습니다. 따라서 ViewModel은 백그라운드 태스크 및 네트워크 요청을 실행하고, 프로젝트의 다른 데이터 소스와 상호작용하며, 선택적으로 ViewModel의 구성 변경 전반에서 유지되고 프로세스 종료 후에도 효력을 유지해야 하는 미션 크리티컬 UI 상태를 캡처하고 유지하는 데 적합합니다.
retain 은 특정 구성 가능한 인스턴스 범위에 있고 형제 구성 가능한 함수 간에 재사용하거나 공유할 필요가 없는 객체에 가장 적합합니다. ViewModel이 UI 상태를 저장하고 백그라운드 작업을 실행하는 데 적합한 위치 역할을 하는 반면 retain은 캐시, 노출수 추적 및 분석, AndroidView 종속 항목, Android OS와 상호작용하거나 결제 처리기 또는 광고와 같은 서드 파티 라이브러리를 관리하는 기타 객체와 같은 UI 배관 객체를 저장하는 데 적합합니다.
최신 Android 앱 아키텍처 권장사항 외부에서 맞춤 앱 아키텍처 패턴을 설계하는 고급 사용자의 경우 retain을 사용하여 사내의 'ViewModel-like' API를 빌드할 수도 있습니다. 코루틴 및 저장된 상태 지원은 기본적으로 제공되지 않지만 retain은 이러한 기능이 빌드된 ViewModel-look-alike의 수명 주기를 위한 빌딩 블록 역할을 할 수 있습니다. 이러한 구성요소를 설계하는 방법에 관한 세부정보는 이 가이드의 범위를 벗어납니다.
|
|
|
|---|---|---|
범위 지정 |
공유된 값이 없습니다. 각 값은 컴포지션 계층 구조의 특정 지점에서 유지되고 연결됩니다. 다른 위치에서 동일한 유형을 유지하면 항상 새 인스턴스에서 작동합니다. |
|
소멸 |
컴포지션 계층 구조를 영구적으로 벗어날 때 |
|
추가 기능 |
객체가 컴포지션 계층 구조에 있든 없든 콜백을 수신할 수 있습니다. |
기본 |
소유자 |
|
|
사용 사례 |
|
|
retain과 rememberSaveable 또는 rememberSerializable 결합
객체에 retained와 rememberSaveable 또는 rememberSerializable의 하이브리드 수명이 있어야 하는 경우가 있습니다. 이는 객체가 저장된 상태 모듈 가이드에 설명된 대로 저장된 상태를 지원할 수 있는 ViewModel이어야 함을 나타낼 수 있습니다.
retain과 rememberSaveable 또는 rememberSerializable을 동시에 사용할 수 있습니다. 두 수명 주기를 올바르게 결합하면 복잡성이 크게 증가합니다.
이 패턴은 더 고급 맞춤 아키텍처 패턴의 일부로, 다음이 모두 참인 경우에만 사용하는 것이 좋습니다.
- 유지되거나 저장되어야 하는 값의 혼합으로 구성된 객체 (예: 사용자 입력을 추적하는 객체 및 디스크에 쓸 수 없는 인메모리 캐시)를 정의합니다.
- 상태가 구성 가능한 함수 범위에 있으며
ViewModel의 싱글톤 범위 지정 또는 수명에 적합하지 않습니다.
이러한 모든 경우에 클래스를 저장된 데이터, 유지된 데이터, 자체 상태가 없고 유지된 객체와 저장된 객체에 위임하여 상태를 적절하게 업데이트하는 '중재자' 객체의 세 부분으로 분할하는 것이 좋습니다. 이 패턴은 다음과 같은 형태를 취합니다.
@Composable fun rememberAndRetain(): CombinedRememberRetained { val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) { ExtractedSaveData() } val retainData = retain { ExtractedRetainData() } return remember(saveData, retainData) { CombinedRememberRetained(saveData, retainData) } } @Serializable data class ExtractedSaveData( // All values that should persist process death should be managed by this class. var savedData: AnotherSerializableType = defaultValue() ) class ExtractedRetainData { // All values that should be retained should appear in this class. // It's possible to manage a CoroutineScope using RetainObserver. // See the full sample for details. var retainedData = Any() } class CombinedRememberRetained( private val saveData: ExtractedSaveData, private val retainData: ExtractedRetainData, ) { fun doAction() { // Manipulate the retained and saved state as needed. } }
수명별로 상태를 분리하면 책임과 저장소의 분리가 매우 명시적이 됩니다. 저장된 데이터는 savedInstanceState 번들이 이미 캡처되어 업데이트할 수 없는 경우 저장된 데이터 업데이트가 시도되는 시나리오를 방지하므로 유지된 데이터로 조작할 수 없습니다. 또한 Compose를 호출하거나 활동 재생성을 시뮬레이션하지 않고 생성자를 테스트하여 재생성 시나리오를 테스트할 수 있습니다.
이 패턴을 구현하는 방법의 전체 예는 전체 샘플 (RetainAndSaveSample.kt)을 참고하세요.
위치별 메모이제이션 및 적응형 레이아웃
Android 애플리케이션은 휴대전화, 폴더블, 태블릿, 데스크톱을 비롯한 여러 폼 팩터를 지원할 수 있습니다. 애플리케이션은 적응형 레이아웃을 사용하여 이러한 폼 팩터 간에 전환해야 하는 경우가 많습니다. 예를 들어 태블릿에서 실행되는 앱은 2열 목록 세부정보 뷰를 표시할 수 있지만 더 작은 휴대전화 화면에 표시될 때는 목록과 세부정보 페이지 간에 이동할 수 있습니다.
기억된 값과 유지된 값은 위치별로 메모이제이션되므로 컴포지션 계층 구조의 동일한 지점에 표시되는 경우에만 재사용됩니다. 레이아웃이 다양한 폼 팩터에 맞게 조정되면 컴포지션 계층 구조의 구조가 변경되어 값이 삭제될 수 있습니다.
Jetpack Navigation 3의 ListDetailPaneScaffold 및 NavDisplay와 같은 기본 제공 구성요소의 경우 이는 문제가 되지 않으며 레이아웃 변경 전반에서 상태가 유지됩니다. 폼 팩터에 맞게 조정되는 맞춤 구성요소의 경우 다음 중 하나를 실행하여 레이아웃 변경의 영향을 받지 않도록 상태를 확인합니다.
- 상태 저장 컴포저블이 항상 컴포지션 계층 구조의 동일한 위치에서 호출되도록 합니다. 컴포지션 계층 구조에서 객체를 재배치하는 대신 레이아웃 로직을 변경하여 적응형 레이아웃을 구현합니다.
MovableContent를 사용하여 상태 저장 컴포저블을 정상적으로 재배치합니다.MovableContent인스턴스는 기억된 값과 유지된 값을 이전 위치에서 새 위치로 이동할 수 있습니다.
팩터리 함수 기억
Compose UI는 구성 가능한 함수로 구성되지만 많은 객체가 컴포지션의 생성 및 구성에 사용됩니다. 가장 일반적인 예는 자체 상태를 정의하는 복잡한 구성 가능한 객체입니다.
LazyList와 같이
LazyListState를 허용합니다.
Compose 중심 객체를 정의할 때는 수명과 키 입력을 모두 포함하여 의도된 기억 동작을 정의하는 remember 함수를 만드는 것이 좋습니다. 이렇게 하면 상태 소비자가 컴포지션 계층 구조에서 예상대로 유지되고 무효화되는 인스턴스를 확실하게 만들 수 있습니다. 구성 가능한 팩터리 함수를 정의할 때는 다음 가이드라인을 따르세요.
- 함수 이름에
remember를 접두사로 붙입니다. 선택적으로 함수 구현이 객체가retained되는지에 따라 달라지고 API가remember의 다른 변형에 의존하도록 발전하지 않는 경우 대신retain접두사 를 사용합니다. - 상태 지속성이 선택되고 올바른
Saver구현을 작성할 수 있는 경우rememberSaveable또는rememberSerializable을 사용합니다. - 사용과 관련이 없을 수 있는
CompositionLocal을 기반으로 부작용을 방지하거나 값을 초기화합니다. 상태가 생성되는 위치가 사용되는 위치가 아닐 수 있습니다.
@Composable fun rememberImageState( imageUri: String, initialZoom: Float = 1f, initialPanX: Int = 0, initialPanY: Int = 0 ): ImageState { return rememberSaveable(imageUri, saver = ImageState.Saver) { ImageState( imageUri, initialZoom, initialPanX, initialPanY ) } } data class ImageState( val imageUri: String, val zoom: Float, val panX: Int, val panY: Int ) { object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver( save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) }, restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) } ) }