[Compose] 상태관리
컴포저블 상태 관리에 대해 공부한다.
Composables 상태
1. Composables 상태란?
컴포즈에서 State(상태)는 UI가 가지고 있는 현재 데이터를 의미한다. 버튼이 눌렸을 경우 카운터가 증가하는 UI가 있다고 가정하면 카운터값이 상태가 된다. 상태는 변할 수 있는 데이터이고, UI는 이 상태를 기반으로 리컴포지션해 최신 데이터를 반영하게 된다.
2. 상태의 종류
호이스팅되지 않은 상태 (Local State)
컴포즈 내부에서 직접 상태를 관리하는 경우로 remeber, mutableStateOf를 활용해 간단하게 저장해 사용이 가능하다.
@Composable
fun Counter(){
var count by remember { mutableStateOf(0) }
Column(horizontalAlignment = Alignment.CenterHorizontally){
Text(text = "Count: $count")
Button(onClik = { count++ }){
Text("카운드 증가")
}
}
}
- remember : 상태를 저장한다.
- mutableStateOf() : count 값을 관리한다.
버튼에 클릭 이벤트가 발생하면 count 값이 증가하고 UI가 자동으로 갱신된다.
호이스팅된 상태 (State Hoisting)
컴포즈 내부가 아니라 부모에게 위임하여 상태를 관리하는 방식이다. UI와 상태 관리를 분리하여 재사용성을 높이고 테스트가 쉽다.
@Composable
fun Counter(){
var count by remember { mutableStateOf(0) }
Counter(count = count, onIncrement = { count++ })
}
@Composable
fun Counter(count: Int, onIcrement: () -> Unit){
Column(horizontalAlignment = Alignment.CenterHorizontally){
Text(text = "Count: $count")
Button(onClik = { count++ }){
Text("카운드 증가")
}
}
}
- Counter() : 상태를 관리하지 않고 UI만 구성하기 때문에 다른 화면에서도 재사용이 가능하다.
- CountScreen() : 상태를 관리하며 Counter()의 부모로 테이터 조작이 가능하다.
3. 상태 호이스팅
컴포즈를 Stateless(독립적이며 단순히 요청이 오면 변환만하고 상태는 전적으로 부모에게 위임하는 구조)로 만들기 위한 구조이다. 상태 변수를 두개의 매개변수로 바꾸는 것을 의미한다.
//상태
var value by rememberSaveble {mutableStateOf(defult)}
//상태를 람다로 변경해 사용
TestView(value = value, onTestChange = { value = it })
컴포저블 저장 장치 (remember)
1. remember란?
- 컴포즈에서 지원해주는 remember API는 메모리에 디버깅 할 수 있게 해준다.
- remember는 리컴포지션이 발생해도 기존 값을 유지할 수 있도록 도와주는 역할을 한다.
- 최초 실행시 remember블록이 초기화 되어 초기값을 반환한다.
- 리컴포지션 발생시 기존 값을 다시 반환한다. (재할당하지 않는다.)
- 컴포지션이 삭제될때 remember도 같이 제거된다.
- 단순히 값을 저장하고 리컴포지션이 되는 경우에도 값을 유지하는 역할만 한다. (상태 변경을 감지하지 않는다.)
2. rememberSaveable란?
- 내부적으로 SaveStateHandle / Bundle을 사용하기 때문에 Acitivty가 재생성되어도 값을 유지한다.
- 화면 회전, 다크 모드, 메모리 부족으로 백그라운드에서 강제종료되는 경우에 모두 상태를 유지한다. 단, 최근 앱에서 강제 종료하는 경우에는 상태를 잃는다.
- 다중 Frmagnet를 사용해 화면을 전환해도 유지한다. (트랜젝션으로 remove()하기 전까지 상태를 유지한다.)
- Jetpack Navigation을 사용할때도 상태를 유지한다.
3. rememberSaveable 상태를 저장하는 방법
Parcelize
객체에 @Parcelize 주석을 사용해 객체를 번들로 제공한다.
@Parcelize
data class City(val name: String, val country: String): Parcelable
@Composable
fun CityScreen(){
var selectedCity = rememberSaveable{
mutableStateOf(City("Madrid", "Spain")) //번들로 저장이 가능하다.
}
}
MapSaver
데이터 클래스에 지원되지 않는 타입, 객체에 동적인 데이터, 클래스가 직렬화가 불가능한 타입인 경우(sealed class, object)인 경우엔 @Parcelize 주석이 사용이 불가능하다. 이때 MapSaver를 사용해 번들로 저장이 가능하게 만든다.
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
List index를 키로 사용하기 때문에 맵 키를 정의할 필요가 없다.
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"))
}
}
컴포저블 상태 관찰 (mutableState)
1. 상태 관찰
컴포즈에서 상태 변화를 감지하는 경우는 mutableStateOf와 같은 상태 객체를 remeber로 기억할때 적용된다. 컴포즈에서 상태 관찰에 사용되는 유형은 다음과 같다.
- State<T>
- MutableState
- Flow
- LiveData
- ShateFlow
- RxJava
2. State<T>
State<T>는 일기 전용으로 UI를 단순 관찰만 하는 경우에 사용한다.
@Composable
fun CounterScreen(){
val count = remember = { mutableStateOf(0) } //mutalbeStateOf로 읽기, 쓰기가 가능하다.
Column{
CounterDisplay(count)
Button(onClick = { count.value++ }){
Text("카운트 증가")
}
}
}
@Composable
fun CounterDisplay(count: State<Int>){ //State<T> 타입으로 읽기만 가능하다.
Text("Coount: $count")
}
3. MutableState<T>
MutableState<T>는 읽고 쓰기가 모두 가능하다.
@Composable
fun CounterScreen(){
val count = remember { mutableStateOf(0) }
Column{
CounterDisplay(count)
Button(onClick = { count.value++ }){
Text("카운트 증가")
}
}
}
@Composable
fun CounterDisplay(count: MutableState<Int>){ //mutalbeStateOf로 읽기, 쓰기가 가능하다.
Text("Coount: $count")
}
4. mutableStateOf
mutableStateOf는 MutableStateOf<T>를 생성하고 mutalbeStateOf는 옵저빙이 가능한 MutableState를 생성되고 MutableState는 런타임시 Compose의 상태 관찰 시스템에 자동으로 등록된다.
var rememberCount = remember { 0 } //리컴포지션과 무관하다.
var mutableCount by mutableStateOf(0) //값이 변경되면 리컴포지션이 일어난다.
mutableCount는 컴포즈가 옵저빙하고 있기 때문에 값이 변경되면 컴포저블에 리컴포지션이 발생하고 자동으로 UI를 갱신한다.
5. mutableStateOf 객체를 생성하는 3가지 방법
가장 많이 사용되는 방식은 by로 객체 생성을 위임하는 것이지만 특정 다른 컴포저블에 MutableState를 넘겨줘야한다면 = 으로 객체를 생성해주어야한다.
val mutableState = remember { mutableStateOf(defult) }
@Composable
fun Counter(){
//.value로 값에 접근해야한다.
val countState = remember { mutableStateOf(0) }
Column{
Text("Count: ${countState.value}")
Button(onClick = { countState.value++ }){
Text("카운트 증가")
}
}
}
MutableState 객체를 직접 전달하여 다른 컴포저블에서도 사용할 수 있지만 .value를 계속 사용해야되기 때문에 코드가 길어지는 단점이 있다.
var value by remember { mutableStateOf(defult) }
@Composable
fun Counter(){
//mutableState를 by 위임하기 때문에 바로 접근이 가능하다.
val countState by remember { mutableStateOf(0) }
Column{
Text("Count: ${countState}")
Button(onClick = { countState++ }){
Text("카운트 증가")
}
}
}
가독성이 좋다는 장점이 있지만 다른 컴포저블에 MutableState 객체를 넘길 수 없다.
val (value, setValue) = remember { mutableStateOf(defult) }
@Composable
fun Counter(){
//코틀린 구조 분해를 사용해 value, setter를 분리
val (count, setSount) = remember { mutableStateOf(0) }
Column{
Text("Count: ${count}") //getter로 값 읽기
Button(onClick = { setCount(count + 1) }){ //setter로 값 변경
Text("카운트 증가")
}
}
}
구조 분해 할당을 사용하기 때문에 setValue의 이름을 변경할 수 없다.
참고
컴포즈에서 ArrayList<T>, MutableList<T>와 같이 변경이 가능한 객체를 사용해 상태 변화를 관찰할 경우 앱에 잘못 되거나 오래된 데이터가 표기 될 수 있다. 이런 문제를 해결하기 위해 State<List<T>>나 listOf()와 같이 변경이 불가능한 데이터형식을 사용하는 것이 좋다.
6. 호스팅된 상태에서 상태 관찰
부모 컴포즈에 위임된 상태에선 2가지 방식으로 상태 관찰이 가능하다. 대신 위임을 하기 위해선 = remember {} 로 생성하기 때문에 컴포즈에서 접근하기 위해 .value 를 사용해주어야한다.
@Composable
fun CounterScreen(){
val count = remember { mutableStateOf(0) }
Column{
CounterDisplay(count)
Button(onClick = { count.value++ }){
Text("카운트 증가")
}
}
}
//컴포즈 내부에서 값을 변경하는 경우엔 MutableState로 받아준다.
@Composable
fun MutableCounterDisplay(count: MutableState<Int>){
Text("Coount: ${count.vale}")
}
//컴포즈 내부에서 값을 변경할 필요가 없는 경우엔 State로 받아준다.
@Composable
fun StateCounterDisplay(count: State<Int>){
Text("Coount: ${count.value}")
}
7. mutableStateOf가 Compose에 통합 되는 과정
- mutableStateOf(value)가 호출되면 Compose 내부에서 관찰이 가능한 MutableState<T> 객체를 생성한다.
- 생성된 객체는 Compose 런타임에 자동으로 추적된다.
- UI가 mutableStateOf 값을 읽으면 Compose는 해당 Composeable이 값을 사용하고 있다는 사실을 기록한다.
- 값이 변경되면 Compose는 해당 객체를 사용하는 Composeble을 Recomposition하여 UI를 업데이트한다.
mutableStateOf는 단순히 값을 저장하는 저장소가 아니라 Compose와 연결되어 반응형 상태로 동작하게 된다.