Android

[Android/Kotlin] Compose 와 State

YusAOS 2024. 10. 23. 16:26

State

앱의 State는 시간이 지남에 따라 변할 수 있는 값입니다. Room Database -> Room의 변수까지 모든 것을 포괄합니다.

Composable은 새로운 State에 따라 업데이트를 하려면 새로운 State에 대해 명시적으로 알려야 합니다.

remember API를 사용하여 메모리에 객체를 저장할 수 있습니다. remeber에 의해 계산된 값은 초기 Composition 중에 Composition에 저장되고 저장된 값은 reComposition 중에 반환됩니다. remember은 변경 가능한 객체뿐만 아니라 변경할 수 없는 객체를 저장하는 데 사용할 수 있습니다.

interface MutableState<T> : State<T> {
    override var value: T
}

mutableStateOf는 관찰 가능한 MutableState<T>를 생성하는데, 이는 런타임 시 Compose에 통합되는 관찰 가능한 유형입니다.

value가 변경되면 value를 읽는 Composable 함수의 reComposition이 예약됩니다.

 

Composable에서 MutableState 객체를 선언하는 데는 3가지 방법이 있습니다.

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

Compose에서 ArrayList<T> 또는 mutableListOf() 같은 변경 가능 객체를 State로 사용하면 앱에 잘못되었거나 오래된 데이터가 표시될 수 있습니다. 변경 가능한 객체 중 ArrayList 또는 변경 가능한 데이터 클래스 같은 관찰 불가능한 객체는 Compose에서 관찰할 수 없으며 객체가 변경될 때 reComposition을 트리거하지 않습니다.

 

관찰 불가능하면서 변경 가능한 객체를 사용하는 대신 State<List<T>> 및 변경 불가능한 listOf() 같은 관찰 가능한(Observer) 데이터 홀더를 사용하는 것이 좋습니다.

 

- 변경 가능 객체 ( ArrayList - add(), remove() 같은 변수를 사용 가능 )

- 관찰 가능한 데이터 홀더 (Android) -> LiveData 나 State 객체

 

Compose에는 Android 앱에 사용되는 관찰 가능한 일반 유형에서 State<T>를 만들 수 있는 함수가 내장되어 있습니다.

 

Compose는 주로 상태 기반 UI 시스템이기 때문에, 컴포넌트의 상태를 직접적으로 관리할 때는 State가 간단하고 효율적입니다. 특히 로컬 UI 상태를 관리할 때 자주 사용됩니다.

 

Flow는 비동기 데이터 처리에 적합하며, 서버나 데이터베이스에서 데이터를 가져오는 작업과 같은 비동기 작업에서 주로 사용됩니다. Jetpack Compose에서는 이 Flow의 결과를 collectAsState()를 통해 UI에 반영할 수 있습니다. 하지만, 이 API는 다른 플랫폼을 위해 개발을 할 때 사용하는 것이 좋다고 하며 Android 앱을 개발할 때에는 collectAsStateWithLifecycle()를 사용하는 것이 좋다고 합니다.

 

collectAsStateWithLifecycle()은 Compose UI가 현재 활성 상태일 때만 데이터를 수집하도록 도와줍니다.

Compose에서 UI는 액티비티나 프래그먼트의 라이프사이클에 의해 영향을 받습니다. collectAsStateWithLifecycle()을 사용하면, UI가 보이지 않거나, 비활성화 상태일 때 Flow 수집을 일시 중단하여 리소스 낭비를 줄이고 안전하게 상태를 관리할 수 있습니다. 이는 특히 네트워크 요청이나 데이터베이스 조회처럼 리소스를 많이 사용하는 작업에서 중요합니다.

 

Stateful 과 Stateless

remember를 사용하여 객체를 저장하는 Composable은 내부 State를 생성하여 Composable을 Stateful로 만듭니다.

내부 State를 갖는 Composable은 재사용 가능성이 적고 테스트하기 어려운 경향이 있습니다.

Stateless Composable은 State를 갖지 않는 Composable입니다. Stateless를 달성하는 한 가지 쉬운 방법은 State hoisting

을 사용하는 것입니다. Stateful은 State를 염두에 두지 않는 호출자에게 편리하며, Stateless은 State를 제어하거나 hoisting 하는 호출자에게 필요합니다.

 

State Hoisting

하나의 상태 변수를 두개의 매개변수로 바꾸는 것을 말합니다.

  • value: T: 표시할 현재 값
  • onValueChange: (T) -> Unit: T가 제안된 새 값인 경우 값을 변경하도록 요청하는 이벤트

Hoisting을 사용하는 이유는 

  • 단일 정보 소스: 상태를 복제하는 대신 옮겼기 때문에 정보 소스가 하나만 있습니다. 버그 방지에 도움이 됩니다.
  • 캡슐화됨: 스테이트풀(Stateful) 컴포저블만 상태를 수정할 수 있습니다. 철저히 내부적 속성입니다.
  • 공유 가능함: 호이스팅한 상태를 여러 컴포저블과 공유할 수 있습니다. 다른 컴포저블에서 name을 읽으려는 경우 호이스팅을 통해 그렇게 할 수 있습니다.
  • 가로채기 가능함: 스테이트리스(Stateless) 컴포저블의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정할 수 있습니다.
  • 분리됨: Stateless Composable의 state가 저장될 수 있습니다. 액세스할 수 있습니다 예를 들어 이제 name를 ViewModel로 이동할 수 있습니다.

아래는 예시입니다.

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

 

단방향 데이터 흐름을 따르면 UI에 상태를 표시하는 컴포저블과 상태를 저장하고 변경하는 앱 부분을 서로 분리할 수 있습니다.

State Hoisting일 때 그 State는 공통 부모 컴포넌트에 위치해야 합니다. 즉, State를 읽는 컴포넌트들이 모두 접근할 수 있는 가장 낮은 위치에서 읽고, State를 변경하는 작업은 그 상태를 소유하는 부모 컴포넌트에서 수행합니다.

 

 rememberSaveable VS remember

 

 - remember : Compose Composable의 State를 구성하는 동안만 유지합니다. 앱을 재구성하거나 프로세스가 종료되면 remember로 저장한 State는 사라집니다. 이 경우, 화면 회전 구성 변경이 발생하면 상태가 초기화됩니다.

val counter = remember { mutableStateOf(0) }

var name by remember { mutableStateOf("") }

val state = remember { derivedStateOf { … } } 
// derivedStateOf는 UI를 업데이트하는 것보다 상태나 키가 더 많이 변경될 때
// 사용된다는 것을 기억하세요. 입력량과 출력량에 차이가 없다면 사용할 필요가 없습니다.
// https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b

 

remember은 composition을 종료할 때까지 값을 저장합니다. 하지만 캐시된 값을 무효화하는 방법이 있습니다. 

 

 - rememberSaveable : remember와 동일하게 State를 저장하지만, 구성 변경이 발생해도 State를 유지합니다. 화면 회전, 다크 모드 변경, 프로세스 종료와 같은 구성 변경에 영향을 받지 않고 다시 복원됩니다. 

val counter = rememberSaveable { mutableStateOf(0) }

 

rememberSaveable API는 Bundle에 데이터를 저장할 수 있는 remember 코드의 래퍼입니다. 이 API를 사용하면 재구성뿐만 아니라 활동 재생성 및 시스템에서 시작된 프로세스 종료 시에도 상태를 유지할 수 있습니다. rememberSaveable remember keys를 받는 것과 같은 목적으로 input 매개변수를 받습니다. 입력이 변경되면 캐시는 무효화됩니다. 다음에 함수가 재구성될 경우 rememberSaveable는 계산 람다 블록을 다시 실행합니다.

 

따라서 구성 변경 시에도 유지해야 하는 상태라면 rememberSaveable을 사용하는 것이 적합합니다.


State를 저장하는 방법 3가지 방법

  •  Parcelize

커스텀 객체를 직렬화 할 수 있는 어노테이션입니다. Parcelize를 사용하면 Parcelize 인터페이스를 통해 상태를 번들에 저장 할 수 있습니다. remeberSaveable이나 Intent, Bundle을 통해 데이터를 전달하거나 상태를 유지할 때 유용합니다.

@Parcelize
data class User(val name: String, val age: Int) : Parcelable
  •  MapSaver

Compose의 Saver에서 객체를 변환하여 저장하는 방법으로 rememberSaveable로 커스텀 객체를 저장할 때 사용합니다. 객체를 Map<String, Any>로 변환해 저장할 수 있도록 도와줍니다. Map의 key-value 쌍으로 상태를 저장하기 때문에 Parcelize 없이도 다양한 데이터 타입을 쉽게 저장할 수 있습니다.

val userSaver = mapSaver(
    save = { mapOf("name" to it.name, "age" to it.age) },
    restore = { User(it["name"] as String, it["age"] as Int) }
)

val user = rememberSaveable(stateSaver = userSaver) { User("Yus", 25) }
  •  ListSaver

Compose의 Saver에서 객체를 변환하여 저장하는 방법으로 rememberSaveable로 객체를 리스트 형태로 저장할 때 사용합니다. 커스텀 객체를 List<Any>에 저장할 수 있어 데이터 복원이 가능합니다. 객체의 필드를 리스트로 저장하기 때문에 순서가 중요한 경우 유리합니다.  ListSaver를 사용할 때는 필드를 리스트 형태로 변환해 저장하고, 복원 시 순서대로 필드를 복구하도록 설정합니다.

 

val userSaver = listSaver<User, Any>(
    save = { listOf(it.name, it.age) },
    restore = { User(it[0] as String, it[1] as Int) }
)

val user = rememberSaveable(stateSaver = userSaver) { User("Yus", 25) }