앱의 상태는 시간이 지남에 따라 변할 수 있는 값입니다. 이는 매우 광범위한 정의로서 Room 데이터베이스부터 클래스 변수까지 모든 항목이 포함됩니다.
모든 Android 앱에서는 사용자에게 상태가 표시됩니다. 다음은 Android 앱 상태의 몇 가지 예입니다.
- 네트워크 연결을 설정할 수 없을 때 표시되는 스낵바
- 블로그 게시물 및 관련 댓글
- 사용자가 클릭하면 버튼에서 재생되는 물결 애니메이션
- 사용자가 이미지 위에 그릴 수 있는 스티커
Jetpack Compose를 사용하면 Android 앱에서 상태를 저장하고 사용하는 위치와 방법을 명시적으로 나타낼 수 있습니다. 이 가이드에서는 상태와 컴포저블 간의 관계, 그리고 보다 손쉬운 상태 처리를 위해 Jetpack Compose에서 제공되는 API에 관해 집중적으로 설명합니다.
상태 및 구성
Compose는 선언적이므로 Compose를 업데이트하는 유일한 방법은 새 인수로 동일한 컴포저블을 호출하는 것입니다. 이러한 인수는 UI 상태를 표현합니다. 상태가 업데이트될 때마다 재구성이 실행됩니다. 따라서 TextField
와 같은 항목은 명령형 XML 기반 뷰에서처럼 자동으로 업데이트되지 않습니다. 컴포저블이 새 상태에 따라 업데이트되려면 새 상태를 명시적으로 알려야 합니다.
@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") } ) } }
이 코드를 실행하고 텍스트를 입력하려고 하면 아무 일도 일어나지 않습니다. TextField
가 자체적으로 업데이트되지 않기 때문입니다. value
매개변수가 변경될 때 업데이트됩니다. 이는 Compose에서의 컴포지션 및 리컴포지션 작동 방식 때문입니다.
초기 컴포지션 및 리컴포지션에 관한 자세한 내용은 Compose 이해를 참고하세요.
컴포저블의 상태
구성 가능한 함수는 remember
API를 사용하여 메모리에 객체를 저장할 수 있습니다. remember
에 의해 계산된 값은 초기 컴포지션 중에 컴포지션에 저장되고 저장된 값은 리컴포지션 중에 반환됩니다.
remember
는 변경 가능한 객체뿐만 아니라 변경할 수 없는 객체를 저장하는 데 사용할 수 있습니다.
mutableStateOf
는 관찰 가능한 MutableState<T>
를 생성하는데, 이는 런타임 시 Compose에 통합되는 관찰 가능한 유형입니다.
interface MutableState<T> : State<T> {
override var value: T
}
value
가 변경되면 value
를 읽는 구성 가능한 함수의 리컴포지션이 예약됩니다.
컴포저블에서 MutableState
객체를 선언하는 데는 세 가지 방법이 있습니다.
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
이러한 선언은 동일한 것이며 서로 다른 용도의 상태를 사용하기 위한 구문 슈가로 제공됩니다. 작성 중인 컴포저블에서 가장 읽기 쉬운 코드를 생성하는 선언을 선택해야 합니다.
by
위임 구문에는 다음 가져오기가 필요합니다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
기억된 값을 다른 컴포저블의 매개변수로 사용하거나 문의 로직으로 사용하여 표시할 컴포저블을 변경할 수 있습니다. 예를 들어 이름이 비어 있는 경우 인사말을 표시하지 않으려면 if
문에 상태를 사용합니다.
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
remember
가 재구성 과정 전체에서 상태를 유지하는 데 도움은 되지만 구성 변경 전반에서는 상태가 유지되지 않습니다. 이 경우에는 rememberSaveable
을 사용해야 합니다. rememberSaveable
은 Bundle
에 저장할 수 있는 모든 값을 자동으로 저장합니다. 다른 값의 경우에는 맞춤 Saver 객체를 전달할 수 있습니다.
지원되는 기타 상태 유형
Compose에서는 상태를 보존하기 위해 MutableState<T>
를 사용할 필요가 없습니다. Compose는 다른 관찰 가능한 유형을 지원합니다. Compose에서 관찰 가능한 다른 유형을 읽으려면 상태가 변할 때 컴포저블이 자동으로 재구성될 수 있도록 State<T>
로 변환해야 합니다.
Compose에는 Android 앱에 사용되는 관찰 가능한 일반 유형에서 State<T>
를 만들 수 있는 함수가 내장되어 있습니다. 이러한 통합을 사용하기 전에 아래에 설명된 대로 적절한 아티팩트를 추가하세요.
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
은 수명 주기를 인식하는 방식으로Flow
의 값을 수집하므로 앱에서 앱 리소스를 보존할 수 있습니다. 이는 ComposeState
에서 마지막으로 내보낸 값을 나타냅니다. Android 앱에서 흐름을 수집하는 데 이 API를 사용하는 것이 좋습니다.build.gradle
파일(2.6.0-beta01 이상이어야 함)에는 다음 종속 항목이 필요합니다.
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
-
collectAsState
도Flow
에서 값을 수집하여 ComposeState
로 변환한다는 점에서collectAsStateWithLifecycle
과 유사합니다.플랫폼 제약이 없는 코드에는 Android 전용인
collectAsStateWithLifecycle
대신collectAsState
를 사용하세요.collectAsState
의 경우compose-runtime
에서 사용할 수 있으므로 추가 종속 항목이 필요하지 않습니다. -
observeAsState()
는 이LiveData
를 관찰하기 시작하고State
를 통해 값을 나타냅니다.build.gradle
파일에는 다음 종속 항목이 필요합니다.
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}
-
subscribeAsState()
는 RxJava2의 반응형 스트림(예:Single
,Observable
,Completable
)을 ComposeState
로 변환하는 확장 함수입니다.build.gradle
파일에는 다음 종속 항목이 필요합니다.
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}
-
subscribeAsState()
는 RxJava3의 반응형 스트림(예:Single
,Observable
,Completable
)을 ComposeState
로 변환하는 확장 함수입니다.build.gradle
파일에는 다음 종속 항목이 필요합니다.
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.5"
}
스테이트풀(Stateful)과 스테이트리스(Stateless)
remember
를 사용하여 객체를 저장하는 컴포저블은 내부 상태를 생성하여 컴포저블을 스테이트풀(Stateful)로 만듭니다. HelloContent
는 내부적으로 name
상태를 보존하고 수정하므로 스테이트풀(Stateful) 컴포저블의 한 예가 됩니다. 이는 호출자가 상태를 제어할 필요가 없고 상태를 직접 관리하지 않아도 상태를 사용할 수 있는 경우에 유용합니다. 그러나 내부 상태를 갖는 컴포저블은 재사용 가능성이 적고 테스트하기가 더 어려운 경향이 있습니다.
스테이트리스(Stateless) 컴포저블은 상태를 갖지 않는 컴포저블입니다. 스테이트리스(Stateless)를 달성하는 한 가지 쉬운 방법은 상태 호이스팅을 사용하는 것입니다.
재사용 가능한 컴포저블을 개발할 때는 동일한 컴포저블의 스테이트풀(Stateful) 버전과 스테이트리스(Stateless) 버전을 모두 노출해야 하는 경우가 있습니다. 스테이트풀(Stateful) 버전은 상태를 염두에 두지 않는 호출자에게 편리하며, 스테이트리스(Stateless) 버전은 상태를 제어하거나 끌어올려야 하는 호출자에게 필요합니다.
상태 호이스팅
Compose에서 상태 호이스팅은 컴포저블을 스테이트리스(Stateless)로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴입니다. Jetpack Compose에서 상태 호이스팅을 하는 일반적 패턴은 상태 변수를 다음 두 개의 매개변수로 바꾸는 것입니다.
value: T
: 표시할 현재 값onValueChange: (T) -> Unit
:T
가 제안된 새 값인 경우 값을 변경하도록 요청하는 이벤트
하지만 onValueChange
로만 제한되지 않습니다. 컴포저블에 더 구체적인 이벤트가 어울리는 경우 람다를 사용하여 그 이벤트를 정의해야 합니다.
이러한 방식으로 끌어올린 상태에는 중요한 속성이 몇 가지 있습니다.
- 단일 정보 소스: 상태를 복제하는 대신 옮겼기 때문에 정보 소스가 하나만 있습니다. 버그 방지에 도움이 됩니다.
- 캡슐화됨: 스테이트풀(Stateful) 컴포저블만 상태를 수정할 수 있습니다. 철저히 내부적 속성입니다.
- 공유 가능함: 호이스팅한 상태를 여러 컴포저블과 공유할 수 있습니다. 다른 컴포저블에서
name
을 읽으려는 경우 호이스팅을 통해 그렇게 할 수 있습니다. - 가로채기 가능함: 스테이트리스(Stateless) 컴포저블의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정할 수 있습니다.
- 분리됨: 스테이트리스(Stateless) 컴포저블의 상태는 어디에나 저장할 수 있습니다. 예를 들어 이제는
name
을ViewModel
로 옮길 수 있습니다.
이 예에서는 HelloContent
에서 name
과 onValueChange
를 추출한 다음, 이러한 항목을 트리 상단을 거쳐 HelloContent
를 호출하는 HelloScreen
컴포저블로 옮깁니다.
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
HelloContent
에서 상태를 끌어올리면 더 쉽게 컴포저블을 추론하고 여러 상황에서 재사용하며 테스트할 수 있습니다. HelloContent
는 상태의 저장 방식과 분리됩니다. 분리된다는 것은 HelloScreen
을 수정하거나 교체할 경우 HelloContent
의 구현 방식을 변경할 필요가 없다는 의미입니다.
상태가 내려가고 이벤트가 올라가는 패턴을 단방향 데이터 흐름이라고 합니다. 이 경우 상태는 HelloScreen
에서 HelloContent
로 내려가고 이벤트는 HelloContent
에서 HelloScreen
으로 올라갑니다. 단방향 데이터 흐름을 따르면 UI에 상태를 표시하는 컴포저블과 상태를 저장하고 변경하는 앱 부분을 서로 분리할 수 있습니다.
자세한 내용은 상태를 호이스팅할 대상 위치 페이지를 참고하세요.
Compose에서 상태 복원
rememberSaveable
API는 저장된 인스턴스 상태 메커니즘을 사용하여 리컴포지션 전반에서, 그리고 활동 또는 프로세스 재생성 전반에서 상태를 유지하므로 remember
와 유사하게 동작합니다. 예를 들어 화면이 회전하면 이러한 문제가 발생합니다.
상태를 저장하는 방법
Bundle
에 추가되는 모든 데이터 유형은 자동으로 저장됩니다. Bundle
에 추가할 수 없는 항목을 저장하려는 경우 몇 가지 옵션이 있습니다.
Parcelize
가장 간단한 해결 방법은 객체에 @Parcelize
주석을 추가하는 것입니다. 그러면 객체가 parcelable이 되며 번들로 제공될 수 있습니다. 예를 들어 다음 코드는 parcelable City
데이터 유형을 만들어 상태에 저장합니다.
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
MapSaver
어떤 이유로 @Parcelize
가 적합하지 않을 경우 mapSaver
를 사용하여 시스템이 Bundle
에 저장할 수 있는 값 집합으로 객체를 변환하는 고유한 규칙을 정의할 수 있습니다.
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
listSaver
를 사용하고 색인을 키로 사용하면 맵의 키를 정의할 필요가 없습니다.
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 상태 페이지를 참고하세요.
키가 변경될 경우 계산 기억 다시 트리거
remember
API는 MutableState
와 함께 자주 사용됩니다.
var name by remember { mutableStateOf("") }
여기서 remember
함수를 사용하면 리컴포지션 후에도 MutableState
값이 유지됩니다.
일반적으로 remember
는 calculation
람다 매개변수를 취합니다. remember
가 처음 실행되면 calculation
람다를 호출하고 그 결과를 저장합니다. 리컴포지션 중에 remember
는 마지막으로 저장된 값을 반환합니다.
캐싱 상태 외에도 remember
를 사용하여 초기화하거나 계산하는 데 비용이 많이 드는 객체 또는 작업의 결과를 컴포지션중에 저장할 수도 있습니다. 매 리컴포지션마다 이 계산을 반복하지 않는 것이 좋습니다.
한 가지 예는 비용이 많이 드는 작업인 이 ShaderBrush
객체를 만드는 경우입니다.
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember
는 컴포지션을 종료할 때까지 값을 저장합니다. 하지만 캐시된 값을 무효화하는 방법이 있습니다. remember
API는 key
또는 keys
매개변수도 취합니다. 이러한 키 중 하나라도 변경될 경우 다음번에 함수가 재구성될 때 remember
는 캐시를 무효화하고 계산 람다 블록을 다시 실행합니다. 이 메커니즘을 통해 컴포지션 내 객체의 전체 기간을 제어할 수 있습니다. 계산은 기억된 값이 컴포지션을 종료할 때까지가 아니라 입력이 변경될 때까지 유효합니다.
다음 예는 이 메커니즘의 작동 방식을 보여줍니다.
이 스니펫에서 ShaderBrush
가 생성되고 Box
컴포저블의 배경 페인트로 사용됩니다. remember
는 앞에서 설명한 대로 ShaderBrush
인스턴스를 저장합니다. 이 인스턴스를 다시 만드는 데 비용이 많이 들기 때문입니다. remember
는 avatarRes
를 선택된 배경 이미지인 key1
매개변수로 사용합니다. avatarRes
가 변경되면 브러시는 새 이미지로 재구성되고 Box
에 다시 적용됩니다. 이는 사용자가 선택 도구에서 배경으로 할 다른 이미지를 선택할 때 발생할 수 있습니다.
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
다음 스니펫에서는 상태가 일반 상태 홀더 클래스
MyAppState
로 호이스팅됩니다. 이 클래스는 rememberMyAppState
함수를 노출하여 remember
로 클래스의 인스턴스를 초기화합니다. 이러한 함수를 노출하여 리컴포지션에도 유지되는 인스턴스를 만드는 것은 Compose의 일반적인 패턴입니다. rememberMyAppState
함수는 remember
의 key
매개변수 역할을 하는 windowSizeClass
를 받습니다. 이 매개변수가 변경되면 앱은 최신 값으로 일반 상태 홀더 클래스를 다시 만들어야 합니다. 예를 들어 사용자가 기기를 회전하는 경우 이러한 상황이 발생할 수 있습니다.
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
Compose는 클래스의 같음 구현을 사용하여 키가 변경되었는지 확인하고 저장된 값을 무효화합니다.
리컴포지션 외에 키와 함께 상태 저장
rememberSaveable
API는 Bundle
에 데이터를 저장할 수 있는 remember
코드의 래퍼입니다. 이 API를 사용하면 재구성뿐만 아니라 활동 재생성 및 시스템에서 시작된 프로세스 종료 시에도 상태를 유지할 수 있습니다.
rememberSaveable
는 remember
가 keys
를 받는 것과 같은 목적으로 input
매개변수를 받습니다. 입력이 변경되면 캐시는 무효화됩니다. 다음에 함수가 재구성될 경우 rememberSaveable
는 계산 람다 블록을 다시 실행합니다.
다음 예에서 rememberSaveable
는 typedQuery
가 변경될 때까지 userTypedQuery
를 저장합니다.
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
자세히 알아보기
상태 및 Jetpack Compose에 관한 자세한 내용은 다음 추가 리소스를 참고하세요.
샘플
Codelab
동영상
블로그
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- Compose UI 설계
- Compose에 UI 상태 저장
- Compose의 부작용