Jetpack Compose의 State
State
시간이 지남에 따라 변할 수 있는 값
- 네트워크 연결되지 않을 때 표시되는 스낵바
- 게시글 및 댓글
- 사용자가 클릭하면 재생되는 애니메이션
Compose와 상태
Compose를 업데이트하는 유일한 방법은 새로운 인수로 동일한 Composable 함수를 호출하는 것이다(Recomposition). 이 과정은 자동으로 업데이트 되는 것이 아니기 때문에 상태를 업데이트하려면 새 상태를 명시적으로 지정해야 한다. 이는 Compose에서 composition과 recomposition의 작동 방식과 관련이 있다.
Composition : Jetpack Compose가 Composable을 실행할 때 빌드한 UI에 대한 명세 Initial composition : 처음 Composable을 실행하여 생성된 composition Recomposition : 데이터가 변경될 때 composition을 업데이트하기 위해 Composable을 다시 실행하는 것
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("Name") }
)
}
}
TextField
는 자동으로 업데이트되지 않는다. TextField의 업데이트는 value
변경에 의해 일어난다.
State
Composable 함수는 remember
API를 이용하여 메모리에 객체를 저장한다.
MutableState<T>
interface MutableState<T> : State<T> {
override var value: T
}
value
가 변경되면 value
를 사용하는 Composable의 recomposition이 예약된다.
Composable에서 MutableState
를 선언하는 데에는 3가지 방법이 있다.
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
Delegation property를 이용하기 위해선 아래 두 구문을 import해야 한다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
MutableState<T>
의 value
는 다른 Composable의 매개변수로 사용하거나 문의 로직으로 사용할 수 있다.
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
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") }
)
}
}
rememberSaveable
rememberSaveable
에는 Bundle
에 저장할 수 있는 모든 값을 자동으로 저장한다. Bundle
에 저장할 수 없을 경우 맞춤 Saver 객체를 전달할 수 있다.
remember
와 List
Compose에서 변경 가능한 객체(mutableListOf()
, ArrayList<T>
등)를 상태로 사용할 경우 오래된 데이터가 표시될 수 있다. Mutable 객체의 내부 변화는 Compose에서 observe할 수 없으며 Recomposition이 일어나지 않는다.
기타 상태 유형
Compose는 Flow, RxJava(Kotlin), LiveData 등에서 상태를 observe하는 기능을 라이브러리 형태로 지원한다. https://developer.android.com/jetpack/compose/state?hl=ko#use-other-types-of-state-in-jetpack-compose
Stateful, Stateless
remember
를 사용하여 객체를 저장하는 Composable은 Stateful한 Composable이다. Stateful한 Composble은 상태를 제어할 필요가 없고 상태를 직접 관리하지 않아도 상태를 사용할 수 있을 때 유용하다. 그러나 Stateful한 Composable은 재사용성이 적고 테스트가 어려운 경향이 있다.
State를 갖지 않는 Composable은 Stateless한 Composable이다. Stateless를 달성하는 쉬운 방법은 State Hoisting을 이용하는 것이다.
State Hoisting
Hoisting : 코드가 실행되기 전 변수/함수 선언이 해당 스코프의 최상단으로 끌어올려진 것 같은 현상을 의미한다.
Compose의 State hoisting은 Composable을 stateless로 만들기 위해 state를 Composable의 호출자로 옮기는 패턴이다. 일반적인 방법은 상태 변수를 아래 두 매개변수로 바꾸는 것이다(이름이나 형식 등은 상황에 맞게 사용).
value: T
: 표시할 값onValueChange: (T) -> Unit
: 값 변경 이벤트
Hoisted state는 몇가지 중요한 속성이 있는데,
- 단일 정보 소스(Single source of truth): 상테를 복제하지 않고 옮기기 때문에 정보 소스가 하나이다.
- 캡슐화(Encapsulated) : Stateful Composable만 상태 수정 가능
- 공유 가능(Shareable) : Hoisted state는 여러 Composable과 공유 가능
- 가로채기 가능(Interceptable) : Stateless composable의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정 가능
- 분리됨(Decoupled) : Stateless Composable의 상태는 어디에나 저장할 수 있다(
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.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
상태가 내려가고 이벤트가 올라가는 패턴을 단방향 데이터 흐름이라고 한다.