오늘도 더 나은 코드를 작성하였습니까?

jetpack compose State 본문

Compose

jetpack compose State

hik14 2024. 2. 6. 03:11

State

- 시간이 지남에 따라 변할 수 있는 값을 의미(Room 데이터베이스 ~ 클래스 변수까지 포함)

-  State가 업데이트(변경)될 때마다 Recomposition이 실행

-  컴포저블이 새로운 State 에 따라 업데이트되려면 명시적으로 알려야 합니다.

 

* MutableState

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

 

 - 런타임 시 Compose에 통합되고,  Composable 함수에서 관찰할 수 있는, Observable State생성,

-  value가 변경되면 value를 매개변수로 받아 읽는 composable함수의 recomposition이 예약된다.

 

* remember API를 사용하여 "메모리"에 객체를 저장

remember에 의해 계산된 값은 초기 컴포지션 중에 컴포지션에 저장되고 저장된 값은 리컴포지션 중에 값을 반환

 

* property 위임을 통해 좀 더 kotlin 스럽게 사용하자! (MutableState .value 호출 대신)

 var state "by" rememeberSaveable{ mutableStateOf(.. ) }  

 

스테이트풀(Stateful)과 스테이트리스(Stateless)

스테이트풀(Stateful)

- remember를 사용하여 객체를 저장하는 컴포저블은 내부 상태를 생성 

- 함수 외부에서 상태를 제어할 필요가 없고, 상태를 직접 관리하지 않아도 상태를 사용할 수 있는 경우에 유용.

- 내부 상태를 갖는 Composable 함수은 재사용 가능성이 적고 테스트하기가 더 어렵다.

 

스테이트리스(Stateless)

- 모든 상태를 매개변수로 받는 Composable 함수

- 스테이트리스(Stateless)를 달성하는 한 가지 쉬운 방법은 상태 호이스팅을 사용하는 것입니다.

 

재사용 가능한 컴포저블을 개발할 때는 동일한 컴포저블의 스테이트풀(Stateful) 버전과 스테이트리스(Stateless) 버전을 모두 노출해야 하는 경우가 있습니다. 스테이트풀(Stateful) 버전은 상태를 염두에 두지 않는 호출자에게 편리하며, 스테이트리스(Stateless) 버전은 상태를 제어하거나 끌어올려야 하는 호출자에게 필요합니다.

state hosting

Compose에서 상태 호이스팅은 컴포저블을 스테이트리스(Stateless)로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴

jetpack Compose에서 상태 호이스팅을 하는 일반적 패턴은 상태 변수를 다음 두 개의 매개변수로 바꾸는 것

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

hosting state의 속성

 

- Single source of truth: 상태를 복제하는 대신 옮겼기 때문에 data source는 하나만 있습니다. 버그 방지에 도움이 됩니다.

- Encapsulated: 스테이트풀(Stateful) 컴포저블만 상태를 수정할 수 있습니다. (철저히 내부적 속성입니다)

- Shareable: 호이스팅한 상태를 여러 컴포저블과 공유할 수 있습니다. 

- Interceptable: 스테이트리스(Stateless) 컴포저블을 호출하는곳 에서 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정할 수 있습니다.

- Decoupled:  스테이트리스(Stateless) ExpandingCard의 상태는 어디에나 저장할 수 있습니다.

 

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

 

state hosting시 어디에, 어떻게 hosting 할지 정하기.

 

1. 상태는 적어도 그 상태를 사용하는 모든 컴포저블의 가장 낮은 공통 상위 요소로 끌어올려야 합니다(읽기).

2. 상태는 최소한 변경될 수 있는 가장 높은 수준(그 아래에서는 더 이상 상태를 변경하지 않음)으로 끌어올려야 합니다(쓰기).

3. 동일한 이벤트에 대한 응답으로 두 상태가 변경되는 경우 두 상태를 함께 끌어올려야 합니다.

 

Compose에서 State 복원

Activity/Process가 다시 생성된 이후 rememberSaveable을 사용하여 UI 상태를 복원합니다.

rememberSaveable은 재구성 과정 전체에서 상태를 유지합니다.

rememberSaveable은 Activity/Process 재생성 전반에 걸쳐 상태를 유지합니다.

Bundle에 추가되는 모든 데이터 유형은 자동으로 저장.

Parcelize

 @Parcelize 주석을 달아서 Bundle에 저장될 수 있는 형태로 만든다.

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

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

 

MapSaver

Bundle에 저장할 수 있는 value 집합으로 객체를 변환하는 고유한 규칙을 정의.

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

listSaver를 사용하고 index를 key로 사용하면 맵의 키를 정의하지 않아도 된다.

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

 

Compose StateHolder

간단한 state hosting은 composable 함수 자체에서 관리 가능합니다.

그러나 추적할 상태의 양이 늘어나거나 구성 가능한 함수에서 실행할 로직이 발생하는 경우 로직과 상태 책임을 다른 클래스, 즉 상태 홀더에 위임하는 것이 좋습니다.

 

상태 홀더의 적절한 구현(예: ViewModel 또는 간단한 클래스)

 

Retrigger remember calculations when keys change

remember{ .. } 는 calculation lambda함수를 parameter로 받습니다.

@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

 

remember가 처음 실행되면 계산 람다를 호출하고 그 결과를 저장합니다.

재구성 중에 remember는 마지막으로 저장된 값을 반환합니다.

 

상태를 메모리에 캐싱하는 용도 말고도, remember를 사용하여 초기화나 계산에 비용이 많이 드는 객체나 작업 결과를 컴포지션에 저장할 수도 있습니다. 

 

예를 들어 이 ShaderBrush 객체를 만드는 것은 비용이 많이 드는 작업입니다.

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

 

ShaderBrush가 생성되고 Box 컴포저블의 배경 페인트로 사용됩니다.

remember는 앞에서 설명한 대로 ShaderBrush 인스턴스를 저장합니다.(인스턴스를 다시 만드는 데 비용이 많이 들기 때문) 

remember avatarRes를 선택된 배경 이미지인 key1 매개변수로 사용합니다. 

 

avatarRes가 변경되면 브러시는 새 이미지로 재구성되고 Box에 다시 적용됩니다.

이는 사용자가 배경으로 할 다른 이미지를 선택할 때 발생할 수 있습니다.

즉, key로 설정한 avatarRes가 변경되지 않는 이상 생성비용이 많이드는 객체를 그대로 저장하여 사용한다.

@Composable
fun BackgroundBanner(
   @DrawableRes avatarRes: Int,
   modifier: Modifier = Modifier,
   res: Resources = LocalContext.current.resources
) {
   val brush = remember(key1 = avatarRes) {
       ShaderBrush(
           BitmapShader(
               ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
               Shader.TileMode.REPEAT,
               Shader.TileMode.REPEAT
           )
       )
   }

   Box(
       modifier = modifier.background(brush)
   ) {
       // ...
   }
}

 

 상태가 일반 상태 홀더 클래스 MyAppState로 끌어올려집니다.

 

RememberMyAppState 함수

 

- windowSizeClass를 매개변수 받음. remember의 key 역활을 한다.

- remember를 사용하여 클래스의 인스턴스를 초기화하여 반환.

 

매개변수(windowSizeClass)가 변경되면 앱은 최신 값으로 일반 상태 홀더 클래스(MyAppState)를 다시 생성해야 합니다.

예를 들어 사용자가 장치를 회전하는 경우 이러한 현상이 발생할 수 있습니다

 

재구성 후에도 유지되는 인스턴스를 만드는 것은 Compose의 일반적인 패턴입니다.

 

리컴포지션 외에 키와 함께 상태 저장

rememberSaveable remember keys를 받는 것과 같은 목적으로 input 매개변수를 받습니다. 

입력이 변경되면 캐시는 무효화됩니다. 다음에 함수가 재구성될 경우 rememberSaveable는 calculation 람다 블록을 다시 실행합니다.

 

'Compose' 카테고리의 다른 글

재사용 가능한 Composable 함수 만들기.  (0) 2024.11.05
Compose UI(Composable) 생명주기  (3) 2024.09.26
jetpack compose Side-effects(부수효과)  (1) 2024.02.07
jetpack compose Basic  (2) 2024.02.06