
Android Developer Codelabs을 기준으로 함.
앱의 '상태'는 시간이 지남에 따라 변할 수 있는 값이다.
이는 매우 광범위한 범위로, RoomDB에서 클래스 변수까지 모든 항목이 포함된다.
Android 앱 상태의 몇가지 예시
핵심: 상태에 따라 특정 시점에 UI에 표시되는 항목이 결정된다.
app 모듈의 루트 패키지에 모든 Kotlin 파일을 추가할 수 있다.
그러나 프로덕션 앱에서는 파일이 하위 패키지에 논리적으로 구조화되어야 한다.
빌드할 첫번째 기능은 하루 동안 마신 물잔 개수를 계산하는 워터 카운터이다.
물잔 개수를 표시하는 Text 컴포저블이 포함된 WaterCounter라는 구성가능한 함수를 만든다.
물잔 개수는 count라는 값에 저장해야 한다.
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
val count = 0
Text(
text = "you've had $count glasses",
modifier = modifier.padding(16.dp)
)
}
모든 구성 가능한 함수에 기본 Modifier를 제공하는 것이 좋다. 재사용성이 높아지기 때문. 모든 필수 매개변수 다음에 매개변수 목록의 첫번째 선택적 매개변수로 표시되어야 한다.
전체 화면을 나타내는 구성 가능한 함수를 만든다. 여기엔 2가지 섹션, 즉 워터 카운터와 웰니스 작업 목록이 있다. 지금은 카운터만 추가.
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
WaterCounter(modifier)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposeStateTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WellnessScreen()
}
}
}
}
}

구성 간으한 WaterCounter 함수의 상태는 count 변수이다.
그러나 정적 상태는 수정할 수 없기 때문에 그다지 유용하지 않다.
이 문제를 해결하기 위해선 Button을 추가하여 개수를 늘리고 하루동안 마신 물잔 개수를 추적한다.
상태(state)는 시간이 지남에 따라 변하는 값이다.
하지만 상태가 없데이트 되는 이유는 무엇일까?
Android에서는 이벤트에 대한 응답으로 상태가 업데이트 된다.
이벤트는 App 외부 또는 내부에서 생성되는 입력이다.
앱 상태로 UI에 표시할 항목에 관한 설명이 제공되고, 이벤트라는 매커니즘을 통해 상태가 변경되고 UI도 변경된다.
핵심: 상태는 존재하고, 이벤트는 발생한다.
이벤트는 어떤 일이 발생했다고 프로그램 일부에 알려준다.

Compose에서 상태 관리는 상태와 이벤트가 서로 상호작용하는 방식을 이해하는 것이 핵심이다.
사용자가 물잔을 더 추가하여 상태를 수정할 수 있도록 버튼을 추가한다.
구성가능한 WaterCounter 함수로 이동하여 Text 컴포저블 아래에 Button을 추가한다. Column을 사용하면 Button 컴포저블에 맞게 Text를 세로로 정렬할 수 있다.
외부 패딩을 Column 컴포저블로 이동하고 Button 상단에 추가 패딩을 더하여 Text에서 분리되도록 할 수 있다.
구성 가능한 Button 함수는 onClick 람다함수를 수신한다.
count를 val 대신 var로 변경하여 변경 가능한 상태가 되도록 한다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(16.dp)
) {
var count = 0
Text(text = "you've had $count glasses")
Button(
onClick = { count++ },
modifier = Modifier.padding(top = 8.dp)
) {
Text(text = "Add one")
}
}
}

앱을 실행하고 버튼을 눌러봐도 아무 일도 발생하지 않는다.
count 변수에 다른 값을 설정해도 Compose에서 이 값을 상태 변경으로 감지하지 않으므로 아무 일도 일어나지 않는다.
이는 상태가 변경될 때, Compose에 화면을 다시 그려야(즉, 구성 가능한 함수를 '재구성') 한다고 알리지 않았기 때문이다.
Compose 앱은 구성 가능한 함수를 호출하여 데이터를 UI로 변환한다.
컴포저블을 실행할 때 Compose에서 빌드한 UI에 관한 설명을 컴포지션이라고 한다.
상태가 변경되면 Compose는 영향을 받는 구성 가능한 함수를 새 상태로 다시 실행한다.
그러면 리컴포지션이라는 업데이트된 UI가 만들어진다.
Compose는 데이터가 변경된 구성요소만 재구성하고 영향을 받지 않는 구성 요소는 건너뛰도록 개별 컴포저블에 필요한 데이터를 확인한다.
컴포지션: 컴포저블을 실행할 때, Jetpack Compose에서 빌드한 UI에 관한 설명.
초기 컴포지션: 처음 컴포저블을 실행하여 컴포지션을 만든다.
리컴포지션: 데이터가 변경될 때 컴포지션을 업데이트하기 위해 컴포저블을 다시 실행하는 것을 말한다.
이렇게 하기 위해선 Compose가 추적할 상태를 알아야 한다.
그래야 업데이트를 받을 때 리컴포지션을 예약할 수 있다.
Compose에는 특정 상태를 읽는 컴포저블의 리컴포지션을 예약하는 특별한 상태 추적 시스템이 있다.
이를 통해 Compose가 세분화되어 전체 UI가 아닌 변경해야 하는 이러한 구성 가능한 함수만 재구성할 수 있다.
상태에 대한 '쓰기' 뿐만 아니라 상태에 대한 '읽기'도 추적하여 실행된다.
Compose의 state 및 MutableState 유형을 사용하여 Compose에서 상태를 관찰할 수 있도록 한다.
Compose는 state value를 읽는 각 컴포저블을 추적하고, 그 value가 변경되면 리컴포지션을 트리거한다.
mutableStateOf 함수를 사용하여 관찰 가능한 MutableState를 만들 수 있다.
이 함수는 초깃값을 State 객체에 래핑된 매개변수로 수신한 다음, value의 값을 관찰 가능한 상태로 만든다.
count를 초깃값이 0인 mutableStateOf를 사용하도록 업데이트한다.
mutableStateOf가 MutableState 유형을 반환하므로 value를 업데이트하여 상태를 업데이트할 수 있고 Compose는 value를 읽는 이러한 함수에 리컴포지션을 트리거한다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(16.dp)
) {
val count: MutableState<Int> = mutableStateOf(0)
Text(text = "you've had ${count.value} glasses")
Button(
onClick = { count.value++ },
modifier = Modifier.padding(top = 8.dp)
) {
Text(text = "Add one")
}
}
}
하지만 앱을 실행해도 아무 일도 일어나지 않는다.
리컴포지션은 잘 작동하지만, 리컴포지션이 작동되면 count 변수가 다시 0으로 초기화 되기 때문.
이를 위해 구성 가능한 인라인 함수 remeber를 통해 값을 유지한다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
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 = Modifier.padding(top = 8.dp)
) {
Text(text = "Add one")
}
}
}
또는 by 키워드를 사용하여 count를 var로 정의할 수도 있다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(16.dp)
) {
var count by remember { mutableStateOf(0)}
Text(text = "you've had $count glasses")
Button(
onClick = { count++ },
modifier = Modifier.padding(top = 8.dp)
) {
Text(text = "Add one")
}
}
}

Compose는 선언형 UI 프레임워크이다.
상태가 변경될 때 UI 구성요소를 삭제하거나 공개 상태를 변경하는 대신 특정 상태의 조건에서 UI가 어떻게 존재하는지를 설명한다.
재구성이 호출되고 UI가 업데이트 된 결과, 컴포저블이 결국 컴포지션을 시작하거나 종료할 수 있다.

이 접근 방식을 사용할 경우 뷰 시스템과 마찬가지로 뷰를 수동으로 업데이트하는 복잡성을 방지할 수 있다.
새 상태에 따라 뷰를 업데이트하는 일이 자동으로 발생하므로 오류도 적게 발생한다.
구성 가능한 함수가 초기 컴포지션 중에 또는 리컴포지션에서 호출되는 경우 컴포지션에 이 함수가 있는것이다.
호출되지 않는 구성 가능한 함수는 컴포지션에 없다.
핵심 아이디어: 사용자에게 표시되는 항목이 UI라면 UI 상태는 앱에서 사용자에게 표시해야 한다고 지정하는 항목이다. 동전의 양면과 마찬가지로 UI는 UI 상태를 시각적으로 나타낸다. UI 상태가 변경되면 변경사항이 즉시 UI에 반영된다.
WaterCounter에서 count가 0보다 크면 Text를 표시한다.

상태는 특정 순간에 UI에 표시되는 요소를 유도한다.
UI의 여러 부분이 동일한 상태에 종속될 수 있음.
Button을 수정하여 count가 10이 될 때까지 사용 설정되고 그 이후에 사용 중지되도록 한다.
Button의 enabled 매개변수를 사용한다.


count 상태의 변경으로 인해 Text의 표시 여부와 Button의 사용 설정 여부가 결정된다.
remember는 컴포지션에 객체를 저장하고, remember가 호출되는 소스 위치가 리컴포지션 중에 다시 호출되지 않으면 객체를 삭제한다.
@Composable
fun WellnessTaskItem(
taskName: String,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = taskName,
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
IconButton(onClick = onClose) {
Icon(
Icons.Filled.Close,
contentDescription = "Close"
)
}
}
}
count가 0보다 클 때 WellnessTaskItem이 표시 되도록 WaterCounter를 업데이트한다.
count가 0보다 크면 WellnessTaskItem 표시 여부를 결정하는 showTask 변수를 정의하고 true로 초기화한다.
showTask가 true인 경우 WellnessTaskItem을 표시하도록 새 if문을 추가한다.
@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 = { },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text(text = "you've had $count glasses")
}
Button(
onClick = { count++ },
modifier = Modifier.padding(top = 8.dp),
enabled = count < 10
) {
Text(text = "Add one")
}
}
}
X 버튼을 누르면 showTask 변수가 false로 변경되어 작업이 더 이상 표시되지 않도록 한다.
그리고 'Clear Water Count'라는 텍스트가 포함된 새 Button을 추가하고 'Add One' 옆에 배치한다.
클릭 시 count가 0으로 재설정된다.
@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(text = "you've had $count glasses")
}
Row(modifier = Modifier.padding(top = 8.dp)) {
Button(onClick = { count++ }, enabled = count < 10) { Text(text = "Add one") }
Button(onClick = { count = 0 }, Modifier.padding(start = 8.dp)) { Text(text = "Clear Water Count") }
}
}
}
앱 실행 시

Add one 클릭 시 (WellnessTaskItem과 counter가 나타남)

X 클릭 시

count 10 이상

Clear Water Count

WaterCounter 코드는 Remember 이전의 코드로 원복한다.
앱을 실행하고 카운터를 추가 후 기기를 회전하면 활동이 구성 변경 후에 다시 생성되므로 저장된 상태가 삭제된다.
언어를 변경하거나 모드 변경 등의 활동을 하더라도 같은 상황이 발생한다.
remember를 사용하면 리컴포지션 간의 상태를 유지하는 데 도움이 되지만, 구성 변견 간에는 유지되지 않는다.
이를 위해서는 remember 대신 rememberSaveable을 사용해야 한다.
rememberSaveable은 Bundle에 저장할 수 있는 모든 값을 자동으로 저장한다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(16.dp)
) {
var count by rememberSaveable { mutableStateOf(0)}
Text(text = "you've had $count glasses")
Button(
onClick = { count++ },
modifier = Modifier.padding(top = 8.dp)
) {
Text(text = "Add one")
}
}
}

Activity가 다시 생성된 후 UI 상태를 복원하려면 rememberSaveable을 사용한다. 리컴포지션 간에 상태를 유지하는 것 외에도 rememberSaveable은 Activity 재생성 및 시스템에서 시작된 프로세스 전반에 걸쳐 상태를 유지한다.
remember를 사용하여 객체를 저장하는 컴포저블에는 내부 상태가 포함되며 이는 컴포저블을 스테이트풀(Stateful)로 만든다.
호출자가 상태를 제어할 필요가 없고 상태를 직접 관리하지 않아도 사용할 수 있는 경우에 유용하다.
그러나, 내부 상태를 갖는 컴포저블은 재사용 가능성이 적고 테스트하기가 더 어려운 경향이 있다.
상태를 보유하지 않는 컴포저블을 Stateless Composable이라고 한다.
상태 호이스팅을 사용하면 stateless composable을 쉽게 만들 수 있다.
-> 재사용이 쉬워진다.
Compose에서 상태 호이스팅은 컴포저블을 stateless로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴이다.
Jetpack Compose에서 상태 호이스팅을 위한 일반적 패턴은 상태 변수를 다음 두 개의 매개변수로 바꾸는 것이다.
여기서 이 값은 수정할 수 있는 모든 상태를 의미한다.
상태가 내려가고 이벤트가 올라가는 패턴을 단방향 데이터 흐름(UDF)이라고 하며, 상태 호이스팅은 이 아키텍처를 Compose에서 구현하는 방법이다.
이런 방식으로 끌어올린 상태에는 중요한 속성이 몇 있다.
구성 가능한 함수에서 모든 상태를 추출할 수 있는 경우, 결과로 생성되는 구성 가능한 함수를 스테이트리스(Stateless)라고 한다.
스테이트리스(Stateless) 컴포저블은 상태를 소유하지 않는 컴포저블이다. 즉, 새 상태를 보유하거나 정의하거나 수정하지 않는다.
스테이트풀(Stateful) 컴포저블은 시간이 지남에 따라 변할 수 있는 상태를 소유하는 컴포저블이다.
실제 앱에서는 컴포저블의 기능에 따라 컴포저블을 100% stateless로 하는 것은 어려울 수 있다.
컴포저블이 가능한 한 적게 상태를 소유하고 적절한 경우 컴포저블의 API에 상태를 노출하여 상태를 끌어올릴 수 있도록 컴포저블을 디자인해야 한다.
WaterCounter를 StatefulCounter, StatelessCounter로 분할하여 리팩토링한다.
Stateless의 역할은 count를 표시하고, count를 늘릴 때 함수를 호출하는 것이다.
이렇게 하려면 위에 설명된 패턴을 따르고, count 상태(구성 가능한 함수에 매개변수로)와 onIncrement 람다(상태가 증가해야 할 때 호출됨)를 전달한다.
@Composable
fun StatelessCounter(
modifier: Modifier = Modifier,
count: Int,
onIncrement: () -> Unit
) {
Column(
modifier = modifier.padding(16.dp)
) {
if (count > 0) {
Text(text = "you've had $count glasses")
}
Button(
onClick = onIncrement,
modifier = Modifier.padding(top = 8.dp),
enabled = count < 10
) {
Text(text = "Add one")
}
}
}
StatefulCounter는 상태를 소유한다.
즉, count 상태를 보유하고 StatelessCounter 함수를 호출할 때 이 상태를 수정한다.
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(count = count, onIncrement = { count++ }, modifier = modifier)
}
count를 StatelessCounter에서 StatefulCounter로 끌어올렸다.
이를 앱에 연결하고 StatefulCounter로 WellnessScreen을 업데이트 할 수 있다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
StatefulCounter(modifier)
}
핵심 사항: 상태를 끌어올릴 때 상태의 이동 위치를 쉽게 파악할 수 있는 세가지 규칙이 있다.
1. 상태는 적어도 그 상태를 사용하는 모든 컴포저블의 가장 낮은 공통 상위 요소로 끌어올려야 한다.
2. 상태는 최소한 변경될 수 있는 가장 높은 수준으로 끌어올려야 한다.
3. 두 상태가 동일한 이벤트에 대한 응답으로 변경되는 경우 두 상태는 동일한 수준으로 끌어올려야 한다.
이러한 규칙에서 요구하는 것보다 더 높은 수준으로 상태를 끌어올릴 수 있다. 하지만 상태를 충분히 높은 수준으로 끌어올리지 않으면 단방향 데이터 흐름을 따르기가 어렵거나 불가능할 수 있다.
Stateless 컴포저블의 재사용
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
위와 같은 waterCounter와 juiceCounter를 기억하고 동일한 StatelessCounter를 사용하는 코드가 있다면

juiceCounter가 수정되면 StatefulCounter가 재구성된다.
리컴포지션 중에 Compose는 juiceCounter를 읽는 함수를 식별하고 이러한 함수의 리컴포지션만 트리거한다.

사용자가 탭하여 juiceCount를 늘리면 StatefulCoutner가 재구성되고, juiceCount를 읽는 StatelessCounter도 재구성된다.
하지만 waterCounter를 읽는 StatelessCounter는 재구성되지 않는다.

구성 가능한 스테이트풀(Stateful) 함수는 여러 구성 가능한 함수에 동일한 상태를 제공할 수 있다.
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count, { count++ })
AnotherStatelessMethod(count, { count *= 2 })
}
count가 Stateless 또는 AnotherStatelessMethod에 의해 업데이트 되면 예상되로 모든 항목들이 재구성된다.
호이스팅된 상태는 공유할 수 있으므로 불필요한 리컴포지션을 방지하고 재사용성을 높이려면 컴포저블에 필요한 상태만 전달해야 한다.
핵심 사항: 컴포저블 디자인 권장사항은 필요한 매개변수만 전달하는 것이다.
앱의 두번째 기능인 웰니스 작업 목록을 추가한다.
목록에 있는 항목으로 2가지 작업을 할 수 있다.

@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
text = taskName,
modifier = Modifier.weight(1f).padding(start = 16.dp)
)
Checkbox(checked = checked, onCheckedChange = onCheckedChange)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
@Composable
fun WellnessTaskItem( taskName: String, modifier: Modifier = Modifier) {
var checkedState by remember { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue ->
checkedState = newValue
},
onClose = {},
modifier = modifier
)
}
data class WellnessTask(
val id: Int,
val label: String,
)
fun getWellnessTask() {
List(30) { i ->
WellnessTask(i, "Task # $i")
}
}
@Composable
fun WellnessTaskList(
modifier: Modifier = Modifier,
list: List<WellnessTask> = remember { getWellnessTask() }
) {
LazyColumn(modifier = modifier) {
items(list) { task ->
WellnessTaskItem(taskName = task.label)
}
}
}
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
WaterCounter()
WellnessTaskList()
}
}

WellnessTaskItem 컴포저블의 몇가지 요소를 자세히 살펴보면, checkedState는 비공개 변수처럼 각 WellnessTaskItem 컴포저블에 독립적으로 속한다.
checkState가 변경되면 WellnessTaskItem의 그 인스턴스만 재구성되며 LazyColumn의 모든 WellnessTaskItem 인스턴스가 재구성되는 것은 아니다.
목록의 체크박스를 선택하고 스크롤하여 다시 돌아오게 되면 선택이 해제 되어 있다.
이전 섹션들과 동일하게 항목이 컴포지션을 종료하면 기억된 상태가 삭제된다는 문제가 있다.
LazyColumn에 있는 항목의 경우 스크롤하면서 항목을 지나치면 항목이 컴포지션을 완전히 종료하므로 더이상 항목이 표시되지 않는다.
이 문제를 해결하기 위해선 rememberSaveable을 다시 사용하면 된다.
저장된 인스턴스 상태 메커니즘을 사용하여 활동 또는 프로세스 재생성 후에도 상태가 유지된다.
rememberSaveable이 LazyList와 함께 작동하는 방식 덕분에 항목은 컴포지션을 종료해도 유지될 수 있다.
var checkedState by rememberSaveable { mutableStateOf(false) }
LazyColumn의 구현을 확인한다.
@Composable
fun LazyColumn(
...
state: LazyListState = rememberLazyListState(),
...
구성 가능한 함수 rememberLazyListState는 rememberSaveable을 사용하여 목록의 초기 상태를 만든다. 활동이 다시 생성되면 스크롤 상태는 아무런 코딩을 하지 않아도 유지된다.
LazyColumn 또는 LazyRow와 같은 지연 구성요소는 LazyListState를 끌어올려 이 사용 사례를 지원한다.
이제 목록에서 작업을 삭제하는 동작을 추가하려면 먼저 목록을 변경 가능한 목록으로 만들어야 한다.
이를 위해 변경 가능한 객체(ex: ArrayList< T > 또는 mutableListOf)를 사용하면 작동하지 않는다.
이러한 유형은 목록의 항목이 변경되었고 UI의 리컴포지션을 예약한다고 Compose에 알리지 않는다.
먼저 관찰 가능한 MutableList를 정의한다.
확장 함수 toMutableListStateList()를 사용하면 변경 가능하거나 변경 불가능한 초기 Collection(ex:List)에서 관찰 가능한 MutableList를 만들 수 있다.
mutableStateOf 함수는 MutableState< T > 유형의 객체를 반환한다.
mutableStateListOf 및 toMutableStateList 함수는 SnapshotList< T > 유형의 객체를 반환한다.
이 섹션에서 '관찰 가능한 MutableList'는 이 클래스를 나타낸다.
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
WaterCounter()
val list = remember { getWellnessTask().toMutableStateList() }
WellnessTaskList(list = list, onCloseTask = { task -> list.remove(task)})
}
}
private fun getWellnessTask() = List(30) { i -> WellnessTask(i, "Task # $i") }
items 메서드는 key 매개변수를 수신한다.
기본적으로 각 항목의 상태는 목록에 있는 항목의 위치를 기준으로 키가 지정된다.
변경 가능한 목록에서는 데이터 세트가 변경될 때 문제가 발생한다.
위치를 변경하는 항목은 기억된 상태를 사실상 잃기 때문이다.
이 문제는 각 WellnessTaskItem의 id를 각 항목의 키로 사용하면 쉽게 해결할 수 있다.
@Composable
fun WellnessTaskList(
modifier: Modifier = Modifier,
list: List<WellnessTask>,
onCloseTask: (WellnessTask) -> Unit
) {
LazyColumn(modifier = modifier) {
items(
items = list,
key = {task -> task.id }
) { task ->
WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
}
}
}
@Composable
fun WellnessTaskItem( taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier) {
var checkedState by rememberSaveable { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = onClose,
modifier = modifier
)
}
이와 같이 수정하면 목록을 삭제할 수 있게 된다.

화면 또는 UI 상태는 화면에 표시할 내용을 나타낸다.
이 상태는 애플리케이션 데이터를 포함하므로 대개 계층구조의 다른 레이어에 연결된다.
UI 상태는 화면에 표시할 내용을 설명하지만, 앱의 로직은 앱의 동작 방식을 설명하고 상태 변경에 반응해야 한다.
로직 유형에는 2가지가 있는데, UI 동작 또는 UI 로직과 비즈니스 로직이다.
ViewModel은 UI 상태와 앱의 다른 레이어에 있는 비즈니스 로직에 대한 액세스 권한을 제공한다. 또한 ViewModel은 구성 변경 후에도 유지되므로 컴포지션보다 전체 기간이 더 길다. Compose 콘텐츠 호스트의 수명주기를 따를 수 있다.
경고: ViewModel은 컴포지션의 일부가 아니기 때문에, 메모리 누수가 발생할 수 있으므로 컴포저블에서 만든 상태(ex: 기억된 값)를 보유해선 안된다.
getWellnessTask()를 WellnessViewModel로 이동.
이전과 마찬가지로 toMutaleStateList를 사용하여 내부 _task 변수를 정의하고 task를 목록으로 노출하여 ViewModel 외부에서 수정할 수 없도록 한다.
목록의 내장 remove 함수에 위임하는 간단한 remove 함수를 구현한다.
class WellnessViewModel: ViewModel() {
private val _task = getWellnessTask().toMutableStateList()
val task : List<WellnessTask>
get() = _task
fun remove(item: WellnessTask) {
_task.remove(item)
}
}
private fun getWellnessTask() = List(30) { i -> WellnessTask(i, "Task # $i") }
implementation(libs.androidx.lifecycle.viewmodel.compose)
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
WaterCounter()
WellnessTaskList(list = wellnessViewModel.task, onCloseTask = { task -> wellnessViewModel.remove(task) })
}
}
viewModel()은 ViewModel을 반환하거나 지정된 범위에서 새 ViewModel을 생성한다.
ViewModel 인스턴스는 범위가 활성화 되어 있는 동안 유지된다.
예를 들어 컴포저블이 활동에서 사용되는 경우 viewModel()은 활동이 완료되거나 프로세스가 종료될 때까지 동일한 인스턴스를 반환한다.
이제 상태 일부와 비즈니스 로직이 포함된 ViewModel을 화면과 통합했다.
상태는 컴포지션 외부에 유지되고, ViewModel에 의해 저장되므로 목록의 변형은 구성이 변경되어도 유지된다.
ViewModel은 어떤 시나리오에서도 앱의 상태를 자동으로 유지하지 않는다.
ViewModel은 탐색 그래프의 활동이나 프래그먼트, 대상에서 호출되는 루트 컴포저블에 가까운 화면 수준 컴포저블에서 사용하는 것이 좋다. ViewModel은 다른 컴포저블로 전달하면 안된다. 대신 필요한 데이터와 필수 로직을 실행하는 함수만 매개변수로 전달해야 한다.
마지막 리팩토링은 선택된 상태와 로직을 ViewModel로 이전하는 것이다.
이렇게 하면 모든 상태가 ViewModel에서 관리되므로 코드가 더 간단해지고 테스트하기 쉬워진다.
data class WellnessTask(
val id: Int,
val label: String,
var checked: Boolean = false
)
fun changeTaskChecked(item: WellnessTask, checked: Boolean) {
_task.find { it.id == item.id }?.let { task ->
task.checked = checked
}
}
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
WaterCounter()
WellnessTaskList(
list = wellnessViewModel.task,
onCheckedTask = { task, checked ->
wellnessViewModel.changeTaskChecked(task, checked)
},
onCloseTask = {
task -> wellnessViewModel.remove(task)
}
)
}
}
경고: ViewModel 인스턴스를 다른 컴포저블에 전달하는 것은 좋지 않다. 필요한 데이터와 필수 로직을 실행하는 함수만 매개변수로 전달해야 한다.
@Composable
fun WellnessTaskList(
modifier: Modifier = Modifier,
list: List<WellnessTask>,
onCheckedTask: (WellnessTask, Boolean) -> Unit,
onCloseTask: (WellnessTask) -> Unit
) {
LazyColumn(modifier = modifier) {
items(
items = list,
key = {task -> task.id }
) { task ->
WellnessTaskItem(
taskName = task.label,
checked = task.checked,
onCheckedChange = { checked -> onCheckedTask(task, checked)},
onClose = { onCloseTask(task) }
)
}
}
}
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
text = taskName,
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
이는 Compose에서 MutableList를 위해 추적하는 것이 요소 추가 및 삭제와 관련된 변경사항이기 때문이다.
삭제가 작동하는 이유가 바로 이것 때문이다.
하지만 추적하도록 지시하지 않는 한 행 항목 값(여기서는 checkState)의 변경사항을 인식하지 못한다.
두 가지 방법엔 모두 장단점이 있다. 예를 들면 사용중인 목록의 구현에 따라 요소를 삭제하고 읽는데 비용이 많이 들 수 있다.
잠재적으로 비용이 많이 드는 목록 작업을 피하고, 더 효율적이고 직관적이므로 checkState를 관찰 가능하도록 만들고자 한다고 가정한다.
새 WellnessTask는 다음과 같이 표시할 수 있다.
data class WellnessTask(
val id: Int,
val label: String,
val checked: MutableState<Boolean> = mutableStateOf(false)
)
이전에 본 것처럼 위임된 속성을 사용하면 이 경우에 checked 변수를 더 간단하게 사용할 수 있다.
WellnessTask를 데이터 클래스가 아닌 클래스가 되도록 변경한다.
WellnessTask가 생성자에서 기본값이 false인 initialChecked 변수를 수신하도록 하면 팩토리 메서드 mutableStateOf로 checked 변수를 초기화하여 initialChecked를 기본값으로 사용할 수 있다.
class WellnessTask(
val id: Int,
val label: String,
initialChecked: Boolean = false
) {
var checked by mutableStateOf(initialChecked)
}
이와 같이 변경하게 되면 리컴포지션 및 구성 변경 후에도 모든 변경사항이 유지된다.