
본 포스팅은 아래 Compose essentials codelab을 학습하고 정리한 포스팅 입니다.
Compose essentials - Get started with state
상태에 따라 특정 시점에 UI에 표시되는 항목이 결정됩니다.
WaterCount입니다.count라는 값에 저장해야합니다.@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
val count = 0
Text(
text = "You've had $count glasses.",
modifier = modifier.padding(16.dp)
)
}
WaterCount함수의 상태는 count변수입니다. 그러나 정적 상태(val)는 수정할 수 없기 때문에 유용하지 않습니다.이벤트 라고 부릅니다.이벤트에 대한 응답으로 상태가 업데이트됩니다.이벤트는 앱 외부 또는 내부에서 생성되는 입력입니다. 예를 들면상태는 존재하고, 이벤트는 발생합니다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
var count = 0
Column(modifier = modifier.padding(16.dp)) {
Text(text = "You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text(text = "Add One")
}
}
}
상태 변경(state change)으로 감지하지 않기 때문입니다.컴포지션이라고 합니다.리컴포지션이라고 합니다.리컴포지션하고, 영향을 받지 않은 요소는 건너뛰도록 개별 Composable 함수에 필요한 데이터를 확인합니다.컴포지션 : 컴포저블을 실행할 때 Jetpack Compose가 빌드하는 UI에 대한 설명입니다.
초기 컴포지션 : 컴포저블을 처음 실행하여 컴포지션을 생성합니다.
리컴포지션 : 데이터가 변경되면 컴포저블을 다시 실행하여 컴포저블을 업데이트합니다.
- 위 처럼 프로세스를 진행하려면 Compose가 추적할 상태를 알아야 합니다. 그래야 업데이트를 받을 때 리컴포지션을 예약할 수 있습니다.
- Compose에는 특정 상태를 읽는 컴포저블의 리컴포지션을 예약하는 특별한 상태 추적 시스템이 있습니다.
- 이를 통해 전체 UI가 아닌 변경해야 하는 컴포저블 함수만 리컴포지션할 수 있습니다.
- 위 작업은 write뿐만 아니라 상태에 대한 read도 추적하여 실행됩니다.
- Comopse의
State및MutableState를 사용하여 Compose에서 상태를 관찰할 수 있도록 합니다.
- 상태의
value속성을 읽는 각 컴포저블을 추적하고 해당value가 변경되면 리컴포지션을 트리거합니다.mutableStateOf함수를 사용하여 관찰 가능한MutableState를 만들 수 있습니다.
- 위 함수는 초깃값을
State객체에 래핑된 매개변수로 수신한 다음,value의 값을 관찰 가능한 상태로 만듭니다.
Compose에는 primitive type에 최적화된 mutableIntStateOf, mutableLongStateOf 등이 있습니다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
val count: MutableState<Int> = mutableStateOf(0)
println("리컴포지션!")
Column(modifier = modifier.padding(16.dp)) {
Text(text = "You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text(text = "Add One")
}
}
}
count의 초깃값이 0인 mutateStateOf 함수를 사용하도록 WaterCounter 컴포저블을 업데이트합니다.mutateStateOf 가 MutableState 유형을 반환하므로 value를 업데이트하여 상태를 업데이트할 수 있고, Compose는 value를 읽는 이러한 함수에 리컴포지션을 트리거합니다.count가 변경되면 count의 value를 자동으로 읽는 Composable 함수의 리컴포지션이 예약됩니다. 위 코드의 경우는 버튼을 클릭할 때 마다 WaterCounter 컴포저블 함수는 재구성 됩니다.count 변수는 다시 0으로 초기화되므로 리컴포지션 간에 이 값을 유지할 방법이 필요합니다.@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
println("리컴포지션! : ")
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = remember { mutableStateOf(0) }
Text(text = "You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text(text = "Add One")
}
}
}
// 위임 속성 사용
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
remember를 사용할 수 있습니다.remember로 계산된 값은 초기 컴포지션 중에 컴포지션에 저장되고 저장된 값은 리컴포지션 간에 유지됩니다.remember와 mutableStateOf는 Composable 함수에서 함께 사용됩니다.UI가 사용자가 보는 것이라면, UI 상태는 앱이 사용자에게 보여주어야 한다고 지정하는 항목입니다. 동전의 양면처럼, UI는 UI 상태의 시각적 표현입니다. UI 상태에 대한 어떠한 변경도 즉시 UI에 반영됩니다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = remember { mutableStateOf(0) }
if (count.value > 0) {
Text("You've had ${count.value} glasses.")
}
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp), enabled = count.value < 10) {
Text(text = "Add One")
}
}
}
remember는 컴포지션에 객체를 저장하고, remember가 호출되는 소스 위치가 리컴포지션 중에 다시 호출되지 않으면 객체를 삭제합니다.@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Row(Modifier.padding(top = 8.dp)) {
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
Button(
onClick = { count = 0 },
Modifier.padding(start = 8.dp)) {
Text("Clear water count")
}
}
}
}
count, showTastk 는 remember 변수입니다.count가 증가하고 리컴포지션이 발생합니다.WellnessTaskItem 및 Text 컴포저블의 count가 표시되기 시작합니다.WellnessTaskItem 의 구성요소의 X를 누릅니다. 이때 리컴포지션이 발생하고, showTask가 false이므로 WellnessTaskItem 은 더 이상 표시되지 않습니다.remember를 사용하면 리컴포지션 간에 상태를 유지하는데 도움되지만, 구성 변경 간에는 유지되지 않습니다.remember대신 rememberSaveable을 사용해야 합니다.rememberSaveable 은 Bundle에 저장할 수 있는 모든 값을 자동으로 저장합니다.@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
var count by rememberSaveable { mutableStateOf(0) }
...
}
remember를 사용하여 객체를 저장하는 컴포저블 함수에는 내부 상태가 포함되며 이는 컴포저블 함수를 Stateful하게 만듭니다.value: T : 표시할 현재 값입니다.onValueChange: (T) → Unit : 값이 새 값 T로 변경되도록 요청하는 이벤트입니다.상태가 내려가고 이벤트가 올라가는 패턴을 단방향 데이터 흐름(UDF)이라고하며, 상태 호이스팅은 이 아키텍처를 Compose에서 구현하는 방법입니다.
- 이러한 방식으로 끌어올린 상태에는 중요한 속성이 몇 가지 있습니다.
- 단일 소스 저장소 : 상태를 복제하는 대신 옮겼기 때문에 소스 저장소가 하나만 있습니다.
- 버그 방지에 도움이 됩니다.
- 공유 가능 : 끌어올린 상태를 여러 컴포저블과 공유할 수 있습니다.
- 분리(Decoupling) : Stateless 함수의 상태는 어디에든(ex : ViewModel) 저장할 수 있습니다.
Stateless : 상태를 소유하지 않는 컴포저블입니다. 즉, 새 상태를 보유하거나 정의하거나 수정하지 않습니다.
Stateful : 시간이 지남에 따라 변할 수 있는 상태를 소유하는 컴포저블입니다.
컴포저블이 가능한 적게 상태를 소유하고 적절한 경우 컴포저블 API에 상태를 노출하여 끌어올릴 수 있도록(호이스팅) 컴포저블을 디자인해야 합니다.
@Composable
fun StatelessCounter(
count: Int, // value
onIncrement: () -> Unit, // onValueChange
modifier: Modifier = Modifier
) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(
onClick = onIncrement,
enabled = count < 10,
modifier = Modifier.padding(top = 8.dp)
) {
Text("Add one")
}
}
}
StatelessCount의 역할은 count를 표시하고 count를 늘릴 때 함수를 호출합니다.count 의 상태와 onIncrement 람다를 전달합니다.@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(
count = count,
onIncrement = { count++ },
modifier = modifier
)
}
StatefulCounter 는 상태를 소유합니다. count의 상태를 보유하고 StatelessCounter 함수를 호출할 때 이 상태를 수정합니다.상태 호이스팅을 사용할 때 이동 위치를 쉽게 파악할 수 있는 세 가지 규칙이 있습니다.
상태를 사용하는 모든 컴포저블의 가장 낮은 공통 부모(읽기)로 상태를 올려야 합니다.
상태는 최소한 변경될 수 있는 가장 높은 수준으로 올려야 합니다(쓰기).
동일한 이벤트에 대한 응답으로 두 상태가 변경되는 경우 동일한 레벨로 올려야 합니다.
상태를 충분히 높은 수준으로 끌어올리지 않으면, UDF패턴을 따르기가 어렵거나 불가능 할 수 있습니다.
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
juiceCount++가 호출되면 StatelessCounter(juiceCount, { juiceCount++ })만 리컴포지션 됩니다.호이스팅된 상태는 공유할 수 있으므로 불필요한 리컴포지션을 방지하고 재사용성을 높이려면 컴포저블에 필요한 상태만 전달해야 합니다.
핵심 사항 : 컴포저블 디자인 권장사항은 필요한 매개변수만 전달하는 것입니다.
// Stateless
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
// Statefult
@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
var checkedState by remember { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = {}, // we will implement this later!
modifier = modifier,
)
}
data class WellnessTask(
val id: Int,
val label: String,
)
@Composable
fun WellnessTasksList(
modifier: Modifier = Modifier,
list: List<WellnessTask> = remember { getWellnessTasks() }
) {
LazyColumn(
modifier = modifier
) {
items(
items = list,
) { task ->
WellnessTaskItem(
taskName = task.label,
)
}
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList()
}
}
@Composable
fun WellnessTaskItem(
taskName: String,
modifier: Modifier = Modifier
) {
var checkedState by remember { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = {}, // we will implement this later!
modifier = modifier,
)
}
checkedState 가 변경되면 WellnessTaskItem 의 인스턴스만 재구성되며 LazyColumn의 모든 WellnessTaskItem 인스턴스가 재구성되는 것은 아닙니다.LazyColumn 에 있는 항목의 경우 스크롤하면서 항목을 지나치면 컴포지션을 완전히 종료하므로 체크된 항목의 선택이 해제되어 있습니다.rememberSaveable을 사용하면 됩니다.var checkedState by rememberSaveable { mutableStateOf(false) }
ArrayList<T> 또는 mutableListOf 를 사용하면 작동하지 않습니다.MutableList 인스턴스를 만들어야 합니다.@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
val list = remember { getWellnessTasks().toMutableStateList() }
WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
mutableStateListOf를 사용하여 목록(List)을 구현할 수 있습니다. 그러나 이를 사용하는 방식으로 인해 예기치 않은 리컴포지션이 발생하고 UI 성능이 최적화되지 않을 수 있습니다.
목록을 정의하고 작업을 다른 작업에 추가하면 모든 리컴포지션에 중복된 항목이 추가됩니다.
// Don't do this!
val list = remember { mutableStateListOf<WellnessTask>()}
list.addAll(getWellnessTasks())단일 작업으로 초깃값을 사용하여 만든 후, 다음과 같이 remember 함수에 전달합니다.
// Do this instead. Don't need to copy
val list = remember {
mutableStateListOf<WellnessTask>().apply {addAll(getWellnessTasks()) }
}
ViewModel은 컴포지션의 일부가 아닙니다. 따라서 메모리 누수가 발생할 수 있으므로 컴포저블에서 만든 상태를 보유해서는 안됩니다.
class WellnessViewModel : ViewModel() {
private val _task = getWellnessTasks().toMutableStateList()
val task: List<WellnessTask>
get() = _task
fun remove(item: WellnessTask) {
_task.remove(item)
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
viewModel() 함수를 호출하여 컴포저블에서 ViewModel을 참조할 수 있습니다.implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")
ViewModel 인스턴스를 다른 컴포저블에 전달하는 것은 좋지 않습니다. 필요한 데이터와 필수 로직을 실행하는 함수만 매개변수로 전달해야 합니다.