개요
공식 문서 나와 있는 순서대로 보고 있는데, 두번째 섹션이 수명 주기 였는데 상태 관리를 보고 와야 할 것 같아서 일단 상태 관리 내용부터 먼저 공부를 해봤다.
컴포즈의 상태
앱의 상태라는 것은 시간이 지남에 따라 변할 수 있는 값이다.
상태라는 것에 대해 정확하게 이해하지 못했는데,
"앱의 현재 데이터 및 UI 상태" 라고 생각해보니 이해가 얼핏 되었다.
이름과 이메일 같은 사용자 정보도 상태고, 알림 설정이나, 블로그 댓글 등 앱 내 데이터도 상태고, 팝업이나 현재 선택된 메뉴 같은 것도 앱의 상태이다.
공식 문서에는 4가지 예시를 언급한다.
-> 네트워크 연결을 설정할 수 없을 때 표시되는 스낵바
-> 블로그 게시물 및 관련 댓글
-> 사용자가 클릭하면 버튼에서 재생되는 물결 애니메이션
-> 사용자가 이미지 위에 그릴 수 있는 스티커
이 글을 통해 '상태'와 '컴포저블' 간의 관계, 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") }
)
}
}
실제로 이 상태에서 키보드 입력을 해도 아무런 텍스트가 입력되지 않는다.
*핵심용어*
컴포지션 -> Compose가 컴포저블을 실행할 때 빌드한 UI에 관한 설명이다.
초기 컴포지션 -> 처음 컴포저블을 실행하여 생성된 컴포지션이다.
리컴포지션 -> 데이터가 변경될 때 컴포지션을 업데이트하기 위해 컴포저블을 다시 실행하는 것이다.
컴포저블의 상태
컴포저블 함수는 remember를 사용해 메모리에 객체를 저장할 수 있다.
remember에 의해 계산된 값은 초기 컴포지션 중에 컴포지션에 저장되고, 저장된 값은 리컴포지션 중에 반환된다.
remember는 변경 가능한 객체뿐만 아니라 변경할 수 없는 객체를 저장하는 데 사용할 수 있다.
< remember를 정리하자면, 재구성이 발생할 때 컴포넌트 안에 선언한 변수도 다시 초기화가 일어나는데, 최초에만 초기화를 진행하고 이전 상태값을 기억하기 위해 remember를 사용하는 것이다. >
* remember는 객체를 컴포지션에 저장하고, remember를 호출한 컴포저블이 컴포지션에서 삭제되면 이 객체를 삭제한다.
mutableStateOf는 관찰 가능한 MutableState<T>를 생성하는데, 이는 런타임 시 Compose에 통합되는 관찰 가능한 유형이다.
interface MutableState<T> : State<T> {
override var value: T
}
value가 변경되면 value를 읽는 컴포저블 함수의 리컴포지션이 예약된다.
컴포저블에서 MutableState 객체를 선언하는 데 3가지 방법이 있다.
val mutableState = remember {mutableStateOf(default)}
var value by remember {mutableStateOf(default)}
val (value, setValue) = remember { mutableStateOf(default)}
여기서 by 위임 구문을 쓰기 위해서는 다음과 같은 항목을 import해줘야 한다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
remember가 재구성 과정 전체에서 상태를 유지하는 데 도움은 되지만, configuration에서는 상태가 유지되지 않는다. 이 경우에는 rememberSaveable을 사용해야 한다.
rememberSaveable은 Bundle에 저장할 수 있는 모든 값을 자동으로 저장한다.
지원되는 기타 상태 유형
Compose는 MutableState<T> 가 아니더라도 다른 방식을 통해 상태를 유지할 수 있다.
기존 Android 앱에서 사용해왔던, LiveData, Flow 등 Compose에는 이들을 State<T>로 만들 수 있는 함수가 내장되어 있다고 한다.
Flow
collectAsStateWithLifecycle(), collectAsState()가 있는데
collectAsStateWithLifecycle()의 경우 수명주기를 인식하여, 앱이 백그라운드에 있는 상태에서는 flow 수집을 멈춘 뒤, 다시 포그라운드로 돌아왔을 때 수집을 진행한다.
그렇기에 플랫폼 제약이 없는 코드에서는 collectAsState를 사용하고, Android와 같이 수명주기에 중요성이 있는 것에서는 collectAsStateWithLifecycle를 추천한다고 한다.
LiveData
observeAsState()는 LiveData를 관찰하기 시작하고 State를 통해 값을 나타낸다.
핵심사항
컴포즈는 State 객체를 읽어오는 과정에서 자동으로 재구성된다. Compose에서 LiveData나 StateFlow와 같은 관찰 가능한 다른 유형을 사용할 경우 이를 읽기 위해서 먼저 State로 변환해야 한다.
스테이트풀(Stateful) & 스테이트리스(Stateless)
remember를 사용해 객체를 저장하는 컴포저블은 내부 상태를 생성해 컴포저블을 스테이트풀(Stateful)로 만든다.
스테이트풀의 경우 호출자가 상태를 제어할 필요가 없고, 상태를 직접 관리하지 않아도 상태를 사용할 수 있는 경우에 유용하다. 그러나 내부 상태를 갖는 컴포저블은 재사용 가능성이 적고 테스트하기 더 어려운 경향이 있다.
스테이트리스(Stateless) 컴포저블은 상태를 갖지 않는 컴포저블이다. 얘를 달성하는 방법 중에 상태 호이스팅을 사용하는 것이 있다.
재사용 가능한 컴포저블을 개발할 때는 동일한 컴포저블의 스테이트풀(Stateful) 버전과 스테이트리스(Stateless) 버전을 모두 노출해야 하는 경우가 있다.
스테이트풀 버전은 상태를 염두에 두지 않는 호출자에게 편리하며, 스테이트리스 버전은 상태를 제어하거나 끌어올려야 하는 호출자에게 필요하다.
상태 호이스팅
컴포즈에서 상태 호이스팅은 컴포저블을 스테이트리스(Stateless)로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴이다.
일반적인 패턴은 상태 변수를 두 개의 매개변수로 바꾸는 것이다.
value : T = 표시할 현재 값
onValueChange : (T) -> Unit = T가 제안된 새 값인 경우 값을 변경하도록 요청하는 이벤트
@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") })
}
}
위에 코드를 작성하고 HelloScreen() 컴포저블 함수를 Preview 해 보면, TextField에 입력할 때마다, 값이 변경되어 나타나게 된다.
이렇게 상태호이스팅을 사용한다면 5가지 장점이 있다.
1. 단일 정보 소스 : 상태를 복제하는 대신 옮겼기 때문에 정보 소스가 하나만 있다.
2. 캡슐화됨 : 스테이트풀 컴포저블만 상태를 수정할 수 있다. (여기서는 HelloScreen()이 스테이트풀 컴포저블)
3. 공유 가능함 : 호이스팅한 상태를 여러 컴포저블과 공유할 수 있다. 다른 컴포저블에서 name을 읽으려 할 때 호이스팅을 통해 가능 하다.
4. 가로채기 가능함 : 스테이트리스 컴포저블의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지를 결정할 수 있다.
5. 분리됨 : 스테이트리스 컴포저블의 상태는 어디에나 저장할 수 있다.
HelloContent는 상태의 저장 방식과 분리된다. 여기서 분리된다는 말은 HelloScreen을 수정하거나 교체할 경우 HelloContent의 구현 방식을 변경할 필요가 없다는 뜻이다.
상태가 내려가고 이벤트가 올라가는 패턴을 단방향 데이터 흐름이라고 한다. 이 경우 상태는 HelloScreen에서 HelloContent로 내려가고 이벤트는 HelloContent에서 HelloScreen으로 올라간다.
단방향 데이터 흐름을 따를 경우 UI에 상태를 표시하는 컴포저블과 상태를 저장하고 변경하는 앱 부분을 서로 분리할 수 있다.
Compose에서의 상태 복원
rememberSaveable는 리컴포지션 간에, Activity 또는 프로세스 재생성 전반에 걸쳐 상태를 유지하기 때문에 remember와 유사하게 동작한다.
-> remeberSaveable의 경우 사용자가 Activity를 완전히 닫으면 상태를 유지하지 않는다.
상태 저장 방법
Bundle에 추가하는 모든 데이터 유형은 자동으로 저장된다. Bundle에 추가할 수 없는 항목을 저장하려는 경우의 옵션은 다음과 같다.
Parcelize
@Parcelize 어노테이션 사용시 객체가 parceable이 되며 번들로 제공될 수 있다.
@Parcelize
data class City(val name: String, val country: String) : Parcelable
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
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
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"))
}
}
'Android > Android Compose' 카테고리의 다른 글
[Android] Compose - (3) 상태를 호이스팅할 대상 위치 (0) | 2024.08.19 |
---|---|
[Android] Compose UI - (3) 페이저(Pager) (1) | 2024.08.15 |
[Android] Compose UI - (2) 수정자(Modifier) (0) | 2024.08.14 |
[Android] Compose UI - (1) 앱 레이아웃 (0) | 2024.08.10 |
[Android] Compose - (1) 컴포즈 알아보기 (1) | 2024.08.09 |