Compose 기초 - Composable의 상태 관리

Composable의 상태 관리 (원문)

💡개념이 헷갈리거나 제가 잘 모르거나 많은 사람들이 잘 모를 것 같은 것 위주로 정리

Composition 용어

  • Composition : UI를 기술하는 Composable의 트리구조
  • Initial Composition : 첫 Composition 구성
  • Recomposition : Composition을 재구성

Composable의 상태에 쓰이는 메모리를 관리하기

remember {...}란?

remember {...}에 의해 계산된 값은 Initial Composition 중에 Composition에 저장되고, 저장된 값은 Recomposition 중에 반환되어 재사용된다.

remember {...}를 호출한 Composable이 Composition에서 삭제되면 remember에 의해 계산된 값 또한 제거된다.

remember {...} 앞에 by 위임자를 사용할까?

  • var value = remember { ... } : 사용할 때 state.value로 접근해야 함.
  • var value by remember { ... } : 사용할 때 state로 접근할 수 있음.

rememberSaveable란?

Configuration changes, Activity 및 프로세스가 재생성 되었을 때도 상태를 복원할 수 있도록 해준다.

내부적으로 Bundle을 사용한다.

Bundle이 지원하지 않는 값의 경우엔 맞춤 Saver 객체를 전달하면 된다.

Stateful(상태있는)과 Stateless(상태없는) Composable

remember 등을 사용하는 Composable은 Stateful Composable이 된다.

상태를 갖지 않는 Composable은 Stateless Composable이 된다.

Stateful Composable을 Stateless Composable로 만드는 방법은 State Hoisting을 하는 것이다.

Composable 상태 끌어올리기(State Hoisting)

Composable의 상태를 @Composable 함수의 매개변수로 만들어서 상위 Composable에 의해 상태를 전달받을 수 있도록 하는 게 State Hoisting이다.

fun HelloScreen() {   
    var name by remember { mutableStateOf("") }  

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
// name과 onNameChange를 상위 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.h5    
        )    
        OutlinedTextField(      
            value = name,      
            onValueChange = onNameChange,      
            label = { Text("Name") }    
        )  
    }
}

State Hoisting의 장점

단일 소스 저장소

상위 위치에서 데이터를 내려받기 때문에 코드 변경 범위가 더 한정되며 버그 방지에 도움이 된다.

캡슐화

코드 변경 범위를 Stateful Composable로 한정할 수 있다.

공유 가능함

Hoisting된 상태를 여러 Composable과 공유할 수 있다.

가로채기 가능함

Stateless Composable의 호출자가 상태를 변경하기 전에 이벤트를 어떻게 처리할지 결정할 수 있다.

분리됨

Hoisting된 상태를 어디에나, 특히 State Holder에 저장할 수 있다.

*State Holder : 상태 관리를 책임지는 클래스. 예) 상태 관리 로직을 포함하는 Composable, Android ViewModel

Hositing 원칙

상태는 적어도 그 상태를 사용하는 모든 Composable의 가장 가까운 공통 상위 요소로 끌어올려야 한다.

복원 가능한 상태를 백업하기

Parcelize

@Parcelize 
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {  
    var selectedCity = rememberSaveable {    
        mutableStateOf(City("Madrid", "Spain"))  
    }
}

MapSaver

data class City(val name: String, val country: String) : Parcelable

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

MapSaver에 비해 사용이 간편

data class City(val name: String, val country: String) : Parcelable

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"))  
    }
}

Composable 상태 전달의 의존성 방향

state-dependencies

Composable 상태 생산자로써 ViewModel의 장점

데이터 레이어 같이 다른 레이어에 위치하는 비즈니스 로직에 대한 액세스 권한을 제공한다. 특정 화면에 표시하기 위한 애플리케이션 데이터를 준비(백그라운드 처리)하는 것에 유리하다.

Composition보다 수명이 길기 때문에 탐색 그래프에서의 수명관리에 유리하다.

  1. 화면(navigation destination)이 백 스택에 있는 동안 Navigation이 ViewModel을 캐시한다.
  2. 화면이 백 스택에서 사라질 때 ViewModel도 삭제된다.

ViewModel 사용 시 주의할 점

화면 수준 Composable에서만 ViewModel 인스턴스를 제공하는 것이 좋다. (왜지??? 답을 못찾음)

ViewModel에서 상태 복원 작업은 SavedStateHandle를 사용한다.