2022년 4월 27일 수요일

[안드로이드] Compose 3 - 상태(State)

 

목차

  1. Compose 시작
  2. Compose의 생명주기
  3. Compose에서의 상태(State)
  4. 렌더링 수행 과정
  5. 부수효과(Side-Effect)
  6. 시맨틱(Semantics)
  7. 아키텍쳐 레이어
  8. 로컬 범위지정(Scope) 데이터

Compose에서의 상태(State)

Composable함수에서의 상태

  • 동일한 Composable 함수를 다른 파라미터로 호출해야만 UI가 업데이트(리컴포지션)됨.
    • 즉, 함수에 전달되는 파라미터는 UI가 무엇을 렌더링해야하는지를 전달하므로 UI 상태를 의미
  • 일반적으로, 파라미터를 State 타입으로 감싸서 remember() Composable 함수를 이용해 값을 저장하는 방식으로 사용함
@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        // name값을 상태로 저장
        var name by remember { mutableStateOf("") }
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.h5
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

State

  • Composable 함수의 수행시에 값을 읽기위해 Compose에서 제공하는 타입.

  • Compose는 내부적으로, State타입이 가진 값의 변화가 발생하면 RecomposeScope를 이용하여 리컴포지션을 요청

  • State는 변경불가능한(immutable) 값을 저장하는 타입으로, 변경가능한(mutable) 값을 저장하려면 하위타입인 MutableState 를 이용

    • MutableState는 이전값과 다른값이 저장되는 경우에만 리컴포지션을 요청하고, 이전값과 동일한 값이 저장되는 경우에는 리컴포지션을 스킵
  • Composable 함수 내에서 MutableState를 선언하는 3가지 방법

// by 위임키워드를 사용하려면 import 필요
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

val mutableState = remember { mutableStateOf(defaultValue) }
var value by remember { mutableStateOf(defaultValue) }
val (value, setValue) = remember { mutableStateOf(defaultValue) }
  • Compose에서는 다른 Observable 타입을 State로 변환하는 확장함수를 지원

remember

  • 함수 파라미터는 함수가 수행되는 동안에만 값을 유지하고 사용가능.
  • Compose에서는 메모리에 단일 객체를 저장하는 remember Composable 함수를 제공
    • 가변(mutable), 불변(immutable) 모두 저장가능
  • 최초 컴포지션 수행시에 값을 저장하고, 리컴포지션이 수행될 때마다 저장된 값을 반환.
  • 단, 구성변경(Configuration Change)시에는 상태를 저장하지 않음.
    • rememberSaveable 을 사용해야 내부적으로 Bundle 객체에 저장하는 로직 수행

State Hoisting

Stateful vs Stateless

  • Stateful : Composable함수 내부에서 remember로 상태를 저장하고 관리
    • 함수사용의 입장에서는 UI상태를 알아서 관리하므로 외부에서 관리할 필요가 없으나, 재사용성이 떨어지고 테스트가 어려움
  • Stateless : Composable함수 내부에서 어떤 상태도 관리하지 않음.
    • 외부에서 상태관리를 할 필요가 있지만, 재사용하기에 용이하고 테스트가 쉬움

State Hoisting

  • State의 관리를 외부(함수 호출한 곳)에서 하도록 구성하여, Stateless한 Composable을 만드는 디자인패턴
  • Composable이 state관리를 하면 안되므로, 내부에서 발생한 액션(이벤트)처리도 밖으로 전달해야 함
// 위의 HelloContent() 함수를 State Hoisting 패턴으로 변경한 예시
// HelloScreen() 함수에서 상태를 관리하는(Stateful) 역할을 담당.
@Composable
fun HelloScreen() {
    // name값을 상태로 저장
    var name by remember { mutableStateOf("") }
    
    HelloContent(
        name = name,
        onNameChange = { name = it }
    )
}

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


이 패턴을 왜 사용하는가?

  • 앱의 데이터/상태를 관리하는 부분과 화면에 렌더링하는 UI로직을 분리할 수 있다.
    • 데이터와 UI가 각각의 변경에 독립적
  • stateless한 Composable함수로 변경하여 재사용성, 테스트가능성을 높일 수 있다.

Compose에서의 상태 복원

  • Activity나 프로세스가 재생성될 때, 상태를 유지하기 위해 rememberSaveable Composable 함수를 사용
  • rememberSaveable은 내부적으로 Bundle을 이용하므로, Bundle에 저장할 수 없는 객체라면 변경해주어야 함

Parcelize

  • 저장할 클래스에 @Parcelize어노테이션을 명시하여 Parcelable을 지원
// City 클래스를 Parcelable 지원하도록 변경
@Parcelize
data class City(val name: String, val country: String) : Parcelable

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

Saver

  • Parcelable을 지원할 수 없는 객체인 경우, Saver를 이용하여 Bundle에 저장/복원 로직을 직접 지정하도록 Compose에서 지원함

  • mapSaver : 객체의 저장/복원로직으로 Map을 사용한 Saver를 반환하는 함수

    • save : 객체의 값을 Map에 저장한 후에 Map을 반환하도록 로직 구성
    • restore : 데이터 복원시에 save에서 반환한 Map을 받아서 다시 객체로 구성
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() {
    // stateSaver 파라미터를 이용하여 상태 저장/복원에 사용할 Saver를 지정
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}
  • ListSaver : 객체의 저장/복원로직으로 List를 사용한 Saver를 반환하는 함수
    • save : 객체의 값을 List에 저장한 후에 List를 반환하도록 로직 구성
    • restore : 데이터 복원시에 save에서 반환한 List를 받아서 다시 객체로 구성
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"))
    }
}

댓글 없음:

댓글 쓰기