Jetpack Compose Doc 읽기 — Part1[기초]

hongbeom
hongbeomi dev
Published in
10 min readJun 14, 2021

Compose 공식 가이드 읽기(2/4)

State 관리

앱의 상태란 시간이 지남에 따라 변할 수 있는 값을 의미합니다.

Composition과 State

상태 개념은 Compose의 핵심입니다. 간단한 예를 들어 보겠습니다. 사용자가 이름을 입력하면 응답으로 인사말이 표시되는 화면이 있다고 가정하겠습니다. 다음 코드에는 인사말 텍스트와 함께 이름 입력을 위한 텍스트 필드가 포함되어 있습니다.

이 코드를 실행하면 아무 일도 일어나지 않습니다. 당연하게도 TextField가 자체적으로 업데이트되지 않기 때문입니다. OutlinedTextFieldvalue 매개변수가 변경될 때야만 비로소 업데이트가 실행될 것입니다. 이는 Compose에서의 Composition 및 Recomposition의 작동 방식 때문입니다.

Composition 및 Recomposition

Composition은 UI를 기술하는 역할을 하며 Composable을 실행하면 생성됩니다. Composition은 UI를 기술하는 Composable의 트리 구조입니다.

Initial Composition시 Compose는 Composition에서 UI를 기술하기 위해 호출하는 Composable을 추적합니다. 그런 다음 앱 상태가 변경되면 Jetpack Compose는 Recomposition을 예약합니다. Recomposition에서는 상태 변경에 관한 응답으로 변할 수 있는 Composable을 실행하고, Jetpack Compose는 변경사항을 반영하도록 Composition을 업데이트합니다.

Composition은 Initial Composition을 통해서만 생성되고 Recomposition을 통해서만 업데이트될 수 있습니다. Composition을 수정하는 유일한 방법은 Recomposition을 통하는 것입니다.

  • Composition: Jetpack Compose가 Composable을 실행할 때 빌드한 UI에 관한 설명을 뜻합니다(트리 구조).
  • Initial Composition: 처음으로 Composable을 실행하여 Composition을 구성하는 작업을 뜻합니다.
  • ReComposition: 데이터가 변경될 때 Composition을 업데이트하기 위해 Composable을 다시 실행하는 것을 말합니다.

Composable의 State

Composable 함수는 remember Composable을 사용하여 메모리에 단일 객체를 저장할 수 있습니다. remember에 의해 계산된 값은 Initial Composition 중에 Composition에 저장되고 저장된 값은 Recomposition 중에 반환됩니다. remember는 변경 가능한 객체뿐만 아니라 변경할 수 없는 객체를 저장하는 데에도 사용할 수 있습니다. 또한 remember는 Composable이 Composition에서 제거되면 객체를 잊습니다.

mutableStateOf는 Compose 런타임과 통합된 Observable 타입인 Observable을 만듭니다.(MutableState<T>)

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

value 의 모든 변경사항은 Composable의 Recomposing 시기에 예약됩니다.

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

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

remember 값을 다른 Composable의 매개변수로 사용하거나 구문의 로직으로 사용하여 표시할 Composable을 변경할 수 있습니다. 예를 들어 이름이 비어 있는 경우 인사말을 표시하지 않으려면 if 문에 상태를 사용합니다.

remember가 Recomposition 과정 전체에서 상태를 유지하는 데 도움은 되지만 구성 변경 시(ex: 화면 회전)에는 상태가 유지되지 않습니다. 이 경우에는 rememberSaveable을 사용해야 합니다. rememberSaveableBundle에 저장할 수 있는 모든 값을 자동으로 저장합니다. 다른 값의 경우에는 맞춤 Saver객체를 전달할 수 있습니다(아래에서 설명됨).

기타 지원되는 State 타입

Compose State<T>는 Android 앱에서 사용되는 일반적인 observe 타입에서 생성하는 함수와 함께 제공됩니다.

  • LiveData
  • Flow
  • RxJava2

Compose는 State<T> 객체를 읽은 후 자동으로 Recomposing 됩니다. ArrayList<T> 또는 mutableListOf() 같은 가변 객체를 사용하면 사용자는 앱에서 잘못되거나 오래된 데이터를 확인하게 될 수 있습니다. 옵저브 불가능한 가변 객체를 사용하는 대신 State<List<T>> 및 immutable한 listOf()를 사용하는 것이 좋습니다.

Stateful과 Stateless

Composable에서는 remember를 사용하여 객체와 상태를 저장할 수 있습니다. 이는 호출자가 상태를 제어할 필요가 없고 상태를 직접 관리하지 않고도 사용하는 상황에서 유용할 수 있습니다. 하지만 내부 상태를 가지는 Composable는 재사용성이 낮고 테스트하기가 더 어렵습니다.

Stateless Composable은 상태를 유지하지 않습니다. stateless를 달성하는 가장 쉬운 방법은 state hoisting을 사용하는 것입니다.

State hoisting

Compose의 state hoisting은 Composable 상태를 저장하지 않는 방식으로 만들기 위해 Composable 호출자에게 상태를 이동시키는 패턴입니다. Jetpack Compose에서 상태를 위로 보내기 위한 일반적인 패턴은 상태 변수를 두 개의 매개 변수로 바꾸는 것입니다.

  • value: T : 현재 표시할 값
  • onValueChange: (T) → Unit : 값 변경을 요청하는 이벤트

이 방식으로 올라간 상태에는 몇 가지 중요한 속성이 있습니다.

  • 신뢰성을 가지는 단일 소스 : 상태를 복제하는 대신 이동함으로써 우리는 오직 하나의 신뢰할 수 있는 소스를 사용할 수 있도록 보장합니다. 이는 버그를 방지하는데 도움이 됩니다.
  • 캡슐화 : stateful Composable만 상태를 수정할 수 있습니다.
  • 공유 가능: 올라간 상태를 여러 Composable과 공유할 수 있습니다.
  • 가로 채기 : stateless Composable에 대한 호출자는 상태를 변경하기 전에 이벤트를 무시하거나 수정할 수 있습니다.
  • 분리됨 : stateless의 상태가 어디에나 저장될 수 있습니다. 아래 예시에서 nameViewModel로 이동할 수 있습니다.

아래 예시의 경우, 우리는 name을 추출하여 onValueChanbe를 통해 HelloContent 밖으로 이동시켜서 HelloScreen에서 HelloContent를 호출할 수 있습니다.

상태를 HelloContent에서 올려보냄으로써 Composable에 대해 추론하고 다른 상황에서 재사용하고 테스트하기가 더 쉽습니다. HelloContent의 상태가 저장되는 방식에서 분리됩니다.

상태가 내려가고 이벤트가 올라가는 흐름을 단방향 데이터 흐름이라고 합니다. 단방향 데이터 흐름을 따르면 상태를 저장하고 변경하는 앱의 부분에서 UI에 상태를 표시하는 Composable을 분리할 수 있습니다.

ViewModel 및 State

ViewModel은 구성 변경 사항(화면 회전 등등)을 유지하므로 Compose 코드를 호스팅하는 Activity 또는 Fragment의 수명주기를 처리할 필요없이 UI와 관련된 상태 및 이벤트를 캡슐화 할 수 있습니다.

우리의 ViewModel이 LiveDataStateFlow같은 홀더에서 상태를 노출시키는데, Composition 중에 상태 객체를 읽게 되면 Composition의 Recomposing 범위가 해당 상태 객체의 업데이트를 자동으로 구독합니다.

또한 하나 이상의 옵저버블한 State 홀더(LiveData, StateFlow...)를 가질 수 있습니다. 각 홀더는 개념적으로 관련되고 함께 변경되는 부분적인 화면에 대한 상태를 보유해야 합니다. 이렇게 하면 상태가 여러 Composable에서 사용되는 경우에도 신뢰성있는 단일 소스를 보존할 수 있습니다.

아래 처럼 Jetpack Compose에서 ViewModel의 LiveData를 사용하여 단방향 데이터 흐름을 구현할 수 있습니다.

observeAsStateLiveData<T>를 관찰하며 LiveData가 변경될 때마다 State<T> 객체를 반환합니다. State<T>는 관찰가능한 타입이며 Jetpack Compose에서 직접적으로 사용할 수 있습니다. observeAsState는 오직 composition에 있는 동안에만 LiveData를 관찰합니다.

이 라인을 확인해봅시다.

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

이것은 observeAsState에 의해 반환된 상태 객체를 자동으로 꺼내어주는 구문입니다. 또한 String 대신 State<String>으로 타입을 변경하여 할당 연산자(=)를 사용할 수도 있습니다.

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

Compose에서 상태 복원

액티비티 또는 프로세스가 다시 생성된 후 UI 상태를 복원하는데에는 rememberSaveable이 사용됩니다.

상태를 저장하는 방법

Bundle에 추가된 모든 데이터 타입은 자동으로 저장됩니다. Bundle에 추가할 수 없는 타입을 저장하려면 몇 가지 옵션을 사용하면 됩니다.

  • Parcelize

제일 간단한 해결책은 @Parcelize 어노테이션을 추가하는 것입니다. 아래는 예시입니다.

  • MapSaver

모종의 이유로 @Parcelize를 사용할 수 없는 경우 mapSaver를 사용하여 객체를 시스템이 Bundle에 저장할 수 있는 값 집합으로 변환하기 위한 고유한 규칙을 정의할 수 있습니다.

  • ListSaver

맵에 대한 키를 정의할 필요없이 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"))
}
}

3부 Lifecycle로 이어서 작성하겠습니다.

읽어주셔서 감사합니다🙌

--

--