ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] Compose State 관리 - 2
    안드로이드 2021. 8. 2. 17:45
    반응형

    개요

    Compose State 관리를 이전 포스팅에 이어 정리해보자. (안드로이드 개발자 사이트 참고)

    2021.08.02 - [안드로이드] - [Android] Compose State 관리 - 1

     

    [Android] Compose State 관리 - 1

    State와 Composition Compose는 선언적이므로 Compose를 업데이트하는 유일한 방법은 새 파라메터로 동일한 Composable을 호출하는 것이다. 이 파라메터는 UI 상태를 나타난다. 상태가 업데이트될때마다 재

    growup-lee.tistory.com

     

    ViewModel 과 State

    ViewModel은 UI 트리 위에 있는 Composable이나 Navigation 라이브러리의 도착지에 있는 Composable을 위해 권장하는 State Holder이다. ViewModel은 구조 변경에도 살아있어서 Activity, Fragment 생명주기에 영향을 받지 않아 UI와 관련된 State 및 Event를 캡슐화 할 수 있다.

     

    ViewModel은 LiveData 또는 StateFlow와 같은 Observable Holder에서 상태를 드러내야한다. Composition 하는 동안 State 객체를 읽을 때, Composition의 현재 재구성 범위는 자동으로 State object에 업데이트된다.

     

    하나 이상의 Observable State Holder를 가질 수 있다. 각 Observable State Holder는 개념적으로 연관되어 있고 함께 변경되어야하는 화면 부분의 State를 유지해야한다. 그리고 한 State로 여러개의 Composable에서 사용할 수 있도록 해야한다.

     

     단방향 데이터 플로우를 구현하기 위해 Jetpack Compose에서 LiveData와 ViewModel을 사용할 수 있다. 다음의 HelloScreen 예제를 보면 ViewModel은 아래 코드와 같이 구현되어 있다.

    class HelloViewModel : ViewModel() {
    
        // LiveData holds state which is observed by the UI
        // (state flows down from ViewModel)
        private val _name = MutableLiveData("")
        val name: LiveData<String> = _name
    
        // onNameChange is an event we're defining that the UI can invoke
        // (events flow up from UI)
        fun onNameChange(newName: String) {
            _name.value = newName
        }
    }
    
    @Composable
    fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
        // by default, viewModel() follows the Lifecycle as the Activity or Fragment
        // that calls HelloScreen(). This lifecycle can be modified by callers of HelloScreen.
    
        // name is the current value of [helloViewModel.name]
        // with an initial value of ""
        val name: String by helloViewModel.name.observeAsState("")
        HelloContent(name = name, onNameChange = { helloViewModel.onNameChange(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") }
            )
        }
    }

    LiveData<T>의 observeAsState는 LiveData를 관찰하고 LiveData가 언제든 변경되도 업데이트되는 State<T> Object를 리턴한다. State<T>는 Jetpack Compose에서 직접 사용할 수 있는 Observable 타입이다. observeAsState는 LiveData를 composition 하는 동안만 관찰할 것이다.

     

    아래의 선언 줄을 보면

    val name: String by helloViewModel.name.observeAsState("")

    observeAsState에서 리턴된 State Object를 자동으로 래핑 해제하는 syntactic sugar이다. 이 구문은 아래의 = 를 사용하여 State<T> 를 리턴하는 구문과 같지만 아래 구문에서는 String이 아닌 State<String>을 return한다.

    val nameState: State<String> = helloViewModel.name.observeAsState("")

     

    Compose에서 State 복원

    Activity나 Process가 재생성된 후 UI state를 복원하기 위해서 rememberSaveable을 사용한다. rememberSaveable은 Recomposition(재구성)을 전반에 걸쳐 상태가 유지되고 Activity나 Process가 재생성되는 전반에 걸쳐 State가 유지된다.

     

    State 저장 방법

    Bundle에 추가되는 모든 데이터 타입은 자동으로 저장된다. 만약 Bundle에 추가할 수 없는 데이터를 저장하고 싶다면, 아래의 몇가지 옵션이 있다.

     

    Parcelize

    가장 심플한 해결책은 @Parcelize 어노테이션을 object에 추가하는 것이다. object는 parcelable 될것이고 bundle이될 수 있다. 아래 예제 처럼 City 데이터 타입을 Parcelable로 만들고 State에 저장한다.

    @Parcelize
    data class City(val name: String, val country: String) : Parcelable
    
    @Composable
    fun CityScreen() {
        var selectedCity = rememberSaveable {
            mutableStateOf(City("Madrid", "Spain"))
        }
    }

    MapSaver

    만약 몇가지 이유로 @Parceliza에 맞지 않다면, mapSaver를 사용할 수 있다. 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"))
        }
    }

    mapSaver를 이용해 CitySaver를 정의하였고, rememberSaveable에서 stateSaver를 CitySaver로 지정하면서 State를 저장 및 복원할 수 있다.

    ListSaver

    map에서 key를 정의하는 필요성을 피하기 위해 index를 key로 사용하는 listSaver를 사용할 수 있다.

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

    mapSaver에서 key를 정의하여 값을 설정했다면 listSaver는 리스트의 순서대로 값을 저장하고 있다.  그러면서 저장된 순서에 따라 값을 가져와 복원하고 있다.

     

    마치며

    Compose의 State 관리를 모두 살펴보았다. 다음에는 Compose의 Lifecycle을 알아보기로 하자.

    반응형

    댓글

Designed by Tistory.