본 포스팅은 아래 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 인스턴스를 다른 컴포저블에 전달하는 것은 좋지 않습니다. 필요한 데이터와 필수 로직을 실행하는 함수만 매개변수로 전달해야 합니다.