1. 시작하기 전에
이 Codelab에서는 Jetpack Compose에서 상태를 사용하는 것과 관련된 핵심 개념을 설명합니다. 앱의 상태에 따라 UI에 표시되는 항목이 결정되는 방식, 상태가 변경될 때 다양한 API를 사용해 Compose에서 UI를 업데이트하는 방법, 구성 가능한 함수의 구조를 최적화하는 방법, Compose 환경에서 ViewModel을 사용하는 방법을 보여줍니다.
기본 요건
- Kotlin 문법에 관한 지식
- Compose에 관한 기본 이해(Jetpack Compose 튜토리얼로 시작 가능)
- 아키텍처 구성요소의
ViewModel
에 관한 기본 이해
학습할 내용
- Jetpack Compose UI에서 상태 및 이벤트를 고려하는 방법
- Compose에서 상태를 사용하여 화면에 표시할 요소를 결정하는 방법
- 상태 호이스팅의 정의
- 구성 가능한 스테이트풀(Stateful) 및 스테이트리스(Stateless) 함수의 작동 방식
- Compose에서
State<T>
API를 사용하여 상태를 자동으로 추적하는 방법 - 구성 가능한 함수에서 메모리 및 내부 상태가 작동하는 방식:
remember
및rememberSaveable
API 사용 - 목록 및 상태를 사용하는 방법:
mutableStateListOf
및toMutableStateList
API 사용 - Compose와 함께
ViewModel
을 사용하는 방법
필요한 항목
권장/선택사항
- Compose 이해 읽기
- 이 Codelab 전에 Jetpack Compose 기본사항 Codelab 먼저 알아보기. 이 Codelab에서 상태에 관해 전체적으로 요약해 설명합니다.
빌드할 항목
간단한 Wellness 앱을 구현합니다.
앱에는 두 가지 주요 기능이 있습니다.
- 물 섭취량을 추적하는 워터 카운터
- 하루 동안 해야 할 웰니스 작업 목록
이 Codelab을 진행하는 동안 추가 지원을 받으려면 다음 코드를 함께 확인하세요.
2. 설정
새 Compose 프로젝트 시작
- 새 Compose 프로젝트를 시작하려면 Android 스튜디오를 엽니다.
- Welcome to Android Studio 창에 있다면 Start a new Android Studio project를 클릭합니다. 이미 Android 스튜디오 프로젝트가 열려 있다면 메뉴 바에서 File > New > New Project를 선택합니다.
- 새 프로젝트의 경우 제공되는 템플릿에서 Empty Activity를 선택합니다.
- Next를 클릭하고 'BasicStateCodelab'이라는 프로젝트를 구성합니다.
minimumSdkVersion으로 API 수준 21 이상을 선택해야 합니다. 이는 Compose에서 지원하는 최소 API 수준입니다.
Empty Compose Activity 템플릿을 선택하면 Android 스튜디오는 프로젝트에서 다음을 설정합니다.
- 화면에 일부 텍스트를 표시하는 구성 가능한 함수로 구성된
MainActivity
클래스 - 앱의 권한, 구성요소, 맞춤 리소스를 정의하는
AndroidManifest.xml
파일 - Compose에 필요한 옵션과 종속 항목이 포함되어 있는
build.gradle.kts
및app/build.gradle.kts
파일
Codelab 솔루션
GitHub에서 BasicStateCodelab
의 솔루션 코드를 가져올 수 있습니다.
$ git clone https://github.com/android/codelab-android-compose
또는 저장소를 ZIP 파일로 다운로드할 수 있습니다.
솔루션 코드는 BasicStateCodelab
프로젝트에서 확인할 수 있습니다. 자신의 속도에 맞게 Codelab을 단계별로 진행하고 도움이 필요한 경우 솔루션을 확인하는 것이 좋습니다. Codelab을 진행하는 중에 프로젝트에 추가해야 하는 코드 스니펫이 제공됩니다.
3. Compose의 상태
앱의 '상태'는 시간이 지남에 따라 변할 수 있는 값입니다. 이는 매우 광범위한 정의로서 Room 데이터베이스부터 클래스 변수까지 모든 항목이 포함됩니다.
모든 Android 앱에서는 사용자에게 상태가 표시됩니다. 다음은 Android 앱 상태의 몇 가지 예시입니다.
- 채팅 앱에서 가장 최근에 수신된 메시지
- 사용자의 프로필 사진
- 항목 목록의 스크롤 위치
Wellness 앱을 작성해 보겠습니다.
편의상 Codelab을 진행하면서 다음을 실행합니다.
app
모듈의 루트com.codelabs.basicstatecodelab
패키지에 모든 Kotlin 파일을 추가할 수 있습니다. 그러나 프로덕션 앱에서는 파일이 하위 패키지에 논리적으로 구조화되어야 합니다.- 스니펫에서 모든 문자열을 인라인으로 하드코딩합니다. 실제 앱에서는
strings.xml
파일에 문자열 리소스로 추가하고 Compose의stringResource
API를 사용하여 참조해야 합니다.
빌드해야 하는 첫 번째 기능은 하루 동안 마신 물잔 개수를 계산하는 워터 카운터입니다.
물잔 개수를 표시하는 Text
컴포저블이 포함된 WaterCounter
라는 구성 가능한 함수를 만듭니다. 물잔 개수는 count
라는 값에 저장해야 하며 이는 지금 하드코딩할 수 있습니다.
다음과 같이 구성 가능한 WaterCounter
함수를 사용하여 새 파일 WaterCounter.kt
를 만듭니다.
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)
)
}
전체 화면을 나타내는 구성 가능한 함수를 만들어 보겠습니다. 여기에는 2개의 섹션, 즉 워터 카운터와 웰니스 작업 목록이 있습니다. 지금은 카운터만 추가합니다.
- 기본 화면을 나타내는
WellnessScreen.kt
파일을 만들고WaterCounter
함수를 호출합니다.
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
WaterCounter(modifier)
}
MainActivity.kt
를 엽니다.Greeting
및DefaultPreview
컴포저블을 삭제합니다. 다음과 같이 새로 만들어진WellnessScreen
컴포저블을 Activity의setContent
블록 내에서 호출합니다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicStateCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WellnessScreen()
}
}
}
}
}
- 지금 앱을 실행하면 하드코딩된 물잔 개수와 함께 기본 워터 카운터 화면이 표시됩니다.
구성 가능한 WaterCounter
함수의 상태는 count
변수입니다. 그러나 정적 상태는 수정할 수 없기 때문에 그다지 유용하지 않습니다. 이 문제를 해결하려면 Button
을 추가하여 개수를 늘리고 하루 동안 마신 물잔 개수를 추적합니다.
상태가 수정되도록 하는 작업을 '이벤트'라고 하며 다음 섹션에서 자세히 알아봅니다.
4. Compose의 이벤트
앞서 이야기했듯 상태는 시간이 지남에 따라 변하는 값(예: 채팅 앱에서 마지막으로 받은 메시지)입니다. 하지만 상태가 업데이트되는 이유는 무엇일까요? Android 앱에서는 이벤트에 대한 응답으로 상태가 업데이트됩니다.
이벤트는 애플리케이션 외부 또는 내부에서 생성되는 입력입니다. 예를 들면 다음과 같습니다.
- 버튼 누르기 등으로 UI와 상호작용하는 사용자
- 기타 요인(예: 새 값을 전송하는 센서 또는 네트워크 응답)
앱 상태로 UI에 표시할 항목에 관한 설명이 제공되고, 이벤트라는 메커니즘을 통해 상태가 변경되고 UI도 변경됩니다.
이벤트는 어떤 일이 발생했다고 프로그램 일부에 알려줍니다. 모든 Android 앱에는 다음과 같은 핵심 UI 업데이트 루프가 있습니다.
- 이벤트: 이벤트는 사용자 또는 프로그램의 다른 부분에 의해 생성됩니다.
- 상태 업데이트: 이벤트 핸들러가 UI에서 사용하는 상태를 변경합니다.
- 상태 표시: 새로운 상태를 표시하도록 UI가 업데이트됩니다.
Compose에서 상태 관리는 상태와 이벤트가 서로 상호작용하는 방식을 이해하는 것이 핵심입니다.
이제 사용자가 물잔을 더 추가하여 상태를 수정할 수 있도록 버튼을 추가합니다.
구성 가능한 WaterCounter
함수로 이동하여 라벨 Text
아래에 Button
을 추가합니다. Column
을 사용하면 Button
컴포저블에 맞게 Text
를 세로로 정렬할 수 있습니다. 외부 패딩을 Column
컴포저블로 이동하고 Button
상단에 추가 패딩을 더하여 Text에서 분리되도록 할 수 있습니다.
구성 가능한 Button
함수는 onClick
람다 함수를 수신합니다. 이는 버튼을 클릭할 때 발생하는 이벤트입니다. 람다 함수의 추가 예시는 나중에 알아봅니다.
count
를 val
대신 var
로 변경하여 변경 가능한 상태가 되도록 합니다.
import androidx.compose.material3.Button
import androidx.compose.foundation.layout.Column
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count = 0
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
앱을 실행하고 버튼을 클릭해도 아무 일도 일어나지 않습니다. count
변수에 다른 값을 설정해도 Compose에서 이 값을 상태 변경으로 감지하지 않으므로 아무 일도 일어나지 않습니다. 이는 상태가 변경될 때 Compose에 화면을 다시 그려야 한다고(즉, 구성 가능한 함수를 '재구성') 알리지 않았기 때문입니다. 다음 단계에서 이 문제를 해결합니다.
5. 구성 가능한 함수의 메모리
Compose 앱은 구성 가능한 함수를 호출하여 데이터를 UI로 변환합니다. 컴포저블을 실행할 때 Compose에서 빌드한 UI에 관한 설명을 컴포지션이라고 합니다. 상태가 변경되면 Compose는 영향을 받는 구성 가능한 함수를 새 상태로 다시 실행합니다. 그러면 리컴포지션이라는 업데이트된 UI가 만들어집니다. 또한 Compose는 데이터가 변경된 구성요소만 재구성하고 영향을 받지 않는 구성요소는 건너뛰도록 개별 컴포저블에 필요한 데이터를 확인합니다.
이렇게 하려면 Compose가 추적할 상태를 알아야 합니다. 그래야 업데이트를 받을 때 리컴포지션을 예약할 수 있습니다.
Compose에는 특정 상태를 읽는 컴포저블의 리컴포지션을 예약하는 특별한 상태 추적 시스템이 있습니다. 이를 통해 Compose가 세분화되어 전체 UI가 아닌 변경해야 하는 이러한 구성 가능한 함수만 재구성할 수 있습니다. 이 작업은 '쓰기'(즉, 상태 변경)뿐만 아니라 상태에 대한 '읽기'도 추적하여 실행됩니다.
Compose의 State
및 MutableState
유형을 사용하여 Compose에서 상태를 관찰할 수 있도록 합니다.
Compose는 상태 value
속성을 읽는 각 컴포저블을 추적하고 그 value
가 변경되면 리컴포지션을 트리거합니다. mutableStateOf
함수를 사용하여 관찰 가능한 MutableState
를 만들 수 있습니다. 이 함수는 초깃값을 State
객체에 래핑된 매개변수로 수신한 다음, value
의 값을 관찰 가능한 상태로 만듭니다.
count
가 초깃값이 0
인 mutableStateOf
API를 사용하도록 WaterCounter
컴포저블을 업데이트합니다. mutableStateOf
가 MutableState
유형을 반환하므로 value
를 업데이트하여 상태를 업데이트할 수 있고 Compose는 value
를 읽는 이러한 함수에 리컴포지션을 트리거합니다.
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
// Changes to count are now tracked by Compose
val count: MutableState<Int> = mutableStateOf(0)
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
앞서 언급했듯이 count
가 변경되면 count
의 value
를 자동으로 읽는 구성 가능한 함수의 리컴포지션이 예약됩니다. 이 경우 WaterCounter
는 버튼을 클릭할 때마다 재구성됩니다.
지금 앱을 실행해도 여전히 아무 일도 일어나지 않습니다.
리컴포지션 예약은 잘 작동합니다. 그러나 리컴포지션이 발생하면 count
변수가 다시 0으로 초기화되므로 리컴포지션 간에 이 값을 유지할 방법이 필요합니다.
이를 위해 구성 가능한 인라인 함수 remember
를 사용할 수 있습니다. remember
로 계산된 값은 초기 컴포지션 중에 컴포지션에 저장되고 저장된 값은 리컴포지션 간에 유지됩니다.
일반적으로 remember
와 mutableStateOf
는 구성 가능한 함수에서 함께 사용됩니다.
Compose 상태 문서에 설명된 대로 이를 작성하는 몇 가지 유사한 방법은 다음과 같습니다.
WaterCounter
를 수정하여 인라인 구성 가능한 함수 remember
로 mutableStateOf
호출을 둘러쌉니다.
import androidx.compose.runtime.remember
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = remember { mutableStateOf(0) }
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
또는 Kotlin의 위임된 속성을 사용하여 count
사용을 간소화할 수 있습니다.
by 키워드를 사용하여 count
를 var로 정의할 수 있습니다. 위임의 getter 및 setter 가져오기를 추가하면 매번 MutableState
의 value
속성을 명시적으로 참조하지 않고도 count
를 간접적으로 읽고 변경할 수 있습니다.
이제 WaterCounter
는 다음과 같이 표시됩니다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@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")
}
}
}
작성 중인 컴포저블에서 가장 읽기 쉬운 코드를 생성하는 문법을 선택해야 합니다.
이제 지금까지 한 작업을 살펴보겠습니다.
- 시간이 지남에 따라 기억하는
count
라는 변수를 정의했습니다. - 기억한 숫자를 사용자에게 알려주는 텍스트 표시를 만들었습니다.
- 클릭할 때마다 기억한 숫자를 늘리는 버튼을 추가했습니다.
다음 배열은 사용자와의 데이터 흐름 피드백 루프를 형성합니다.
- UI에서 사용자에게 상태를 표시합니다(현재 개수가 텍스트로 표시됨).
- 사용자가 기존 상태와 결합된 이벤트를 생성하여 새 상태를 생성합니다(버튼을 클릭하면 현재 개수에 하나가 추가됨).
이제 카운터가 작동할 준비를 마쳤습니다.
6. 상태 기반 UI
Compose는 선언형 UI 프레임워크입니다. 상태가 변경될 때 UI 구성요소를 삭제하거나 공개 상태를 변경하는 대신 특정 상태의 조건에서 UI가 어떻게 존재하는지 설명합니다. 재구성이 호출되고 UI가 업데이트된 결과, 컴포저블이 결국 컴포지션을 시작하거나 종료할 수 있습니다.
이 접근 방식을 사용하면 뷰 시스템과 마찬가지로 뷰를 수동으로 업데이트하는 복잡성을 방지할 수 있습니다. 새 상태에 따라 뷰를 업데이트하는 일이 자동으로 발생하므로(개발자가 기억할 필요가 없음) 오류도 적게 발생합니다.
구성 가능한 함수가 초기 컴포지션 중에 또는 리컴포지션에서 호출되는 경우 컴포지션에 이 함수가 있는 것입니다. 호출되지 않는 구성 가능한 함수(예: 함수가 if 문 내에서 호출되는데 조건이 충족되지 않기 때문)는 컴포지션에 없습니다.
컴포저블의 수명 주기에 관한 자세한 내용은 문서를 참고하세요.
컴포지션의 출력은 UI를 설명하는 트리 구조입니다.
Android 스튜디오의 Layout Inspector 도구를 사용하여 Compose에서 생성된 앱 레이아웃을 검사할 수 있습니다. 바로 시작해 보겠습니다.
이를 보여주려면 상태에 따라 UI를 표시하도록 코드를 수정합니다. WaterCounter
를 열고 count
가 0보다 크면 Text
를 표시합니다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
// This text is present if the button has been clicked
// at least once; absent otherwise
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
앱을 실행하고 Android 스튜디오에서 Tools > Layout Inspector로 이동하여 Layout Inspector 도구를 엽니다.
분할 화면이 표시됩니다. 왼쪽에는 구성요소 트리가, 오른쪽에는 앱 미리보기가 있습니다.
화면 왼쪽에서 루트 요소 BasicStateCodelabTheme
을 탭하여 트리를 탐색합니다. 모두 펼치기 버튼을 클릭하여 구성요소 트리 전체를 펼칩니다.
오른쪽 화면의 요소를 클릭하면 트리에서 상응하는 요소로 이동합니다.
앱에서 Add one 버튼을 누르면 다음 작업이 실행됩니다.
- 개수가 1로 증가하고 상태가 변경됩니다.
- 리컴포지션이 호출됩니다.
- 화면이 새 요소로 재구성됩니다.
Android 스튜디오의 Layout Inspector 도구로 구성요소 트리를 검사하면 이제 Text
컴포저블도 표시됩니다.
상태는 특정 순간에 UI에 표시되는 요소를 유도합니다.
UI의 여러 부분이 동일한 상태에 종속될 수 있습니다. Button
을 수정하여 count
가 10이 될 때까지 사용 설정되고 그 후에 사용 중지되도록 합니다. 그러면 목표를 달성하는 것입니다. Button
의 enabled
매개변수를 사용하면 됩니다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
...
}
이제 앱을 실행합니다. count
상태의 변경으로 인해 Text
의 표시 여부와 Button
의 사용 설정 여부가 결정됩니다.
7. 컴포지션의 Remember
remember
는 컴포지션에 객체를 저장하고, remember
가 호출되는 소스 위치가 리컴포지션 중에 다시 호출되지 않으면 객체를 삭제합니다.
이 동작을 시각화하려면 앱에 다음 기능을 구현합니다. 사용자가 물을 한 잔 이상 마셨을 때 사용자가 할 웰니스 작업을 표시하고 닫을 수 있도록 합니다. 컴포저블은 작고 재사용 가능해야 하므로 WellnessTaskItem
이라는 새 컴포저블을 만듭니다. 이 컴포저블은 매개변수로 수신된 문자열에 기반하여 웰니스 작업을 표시하고 닫기 아이콘 버튼을 표시합니다.
새 파일 WellnessTaskItem.kt
를 만들고 다음 코드를 추가합니다. 이 Codelab의 후반부에서 이 구성 가능한 함수를 사용합니다.
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding
@Composable
fun WellnessTaskItem(
taskName: String,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f).padding(start = 16.dp),
text = taskName
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
WellnessTaskItem
함수는 내장된 Button
컴포저블이 onClick
을 수신하는 것과 마찬가지로 작업 설명과 onClose
람다 함수를 수신합니다.
WellnessTaskItem
은 다음과 같이 표시됩니다.
더 많은 기능으로 앱을 개선하려면 count
가 0보다 클 때 WellnessTaskItem
이 표시되도록 WaterCounter
를 업데이트하세요.
count
가 0보다 크면 WellnessTaskItem
표시 여부를 결정하는 showTask
변수를 정의하고 true로 초기화합니다.
showTask
가 true인 경우 WellnessTaskItem
을 표시하도록 새 if 문을 추가합니다. 이전 섹션에서 배운 API를 사용하여 showTask
값이 리컴포지션에도 유지되도록 합니다.
@Composable
fun WaterCounter() {
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("You've had $count glasses.")
}
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
}
}
X 버튼을 누르면 showTask
변수가 false
로 변경되어 작업이 더 이상 표시되지 않도록 WellnessTaskItem
의 onClose
람다 함수를 사용합니다.
...
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
...
그런 다음 'Clear water count'라는 텍스트가 포함된 새 Button
을 추가하고 'Add one' Button
옆에 배치합니다. Row
를 사용하면 두 버튼을 더 쉽게 정렬할 수 있습니다. Row
에 패딩을 추가할 수도 있습니다. 'Clear water count' 버튼을 누르면 count
변수가 다시 0으로 재설정됩니다.
구성 가능한 함수 WaterCounter
는 다음과 같습니다.
import androidx.compose.foundation.layout.Row
@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
및 showTask
는 기억된 값입니다.
이제 앱에서 다음 단계를 따르면 됩니다.
- Add one 버튼을 누릅니다. 그러면
count
가 증가하고(리컴포지션이 발생함)WellnessTaskItem
및 카운터Text
가 모두 표시되기 시작합니다.
WellnessTaskItem
구성요소의 X를 누릅니다(또 다른 리컴포지션이 발생함). 이제showTask
가 false이므로WellnessTaskItem
이 더 이상 표시되지 않습니다.
- Add one 버튼을 누릅니다(또 다른 리컴포지션 발생).
showTask
는 잔 개수를 계속 추가하면 다음 리컴포지션에서WellnessTaskItem
을 닫았음을 기억합니다.
- Clear water count 버튼을 눌러
count
를 0으로 재설정하면 리컴포지션이 발생합니다.count
를 표시하는Text
와WellnessTaskItem
과 관련된 모든 코드가 호출되지 않고 컴포지션을 종료합니다.
- remember
showTask
가 호출되는 코드 위치가 호출되지 않았으므로showTask
가 삭제되었습니다. 다시 첫 번째 단계로 돌아왔습니다.
- Add one 버튼을 눌러
count
를 0보다 크게 만듭니다(리컴포지션).
WellnessTaskItem
컴포저블이 다시 표시됩니다. 위의 컴포지션을 종료할 때showTask
의 이전 값이 삭제되었기 때문입니다.
count
가 0으로 돌아간 후 remember
에서 허용하는 것보다 더 오래 showTask
를 유지해야 한다면(즉, remember
가 호출되는 코드 위치가 리컴포지션 중에 호출되지 않더라도) 어떻게 될까요? 다음 섹션에서는 이러한 시나리오를 해결하는 방법과 더 많은 예를 살펴봅니다.
이제 UI와 상태가 컴포지션을 종료할 때 재설정되는 방식을 이해했으므로 코드를 삭제하고 이 섹션의 시작 부분에 있었던 WaterCounter
로 돌아갑니다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
8. Compose에서 상태 복원
앱을 실행하고 물잔을 카운터에 추가한 다음 기기를 회전합니다. 기기의 자동 회전 설정이 켜져 있어야 합니다.
활동은 구성 변경(이 경우 방향) 후에 다시 생성되므로 저장된 상태는 삭제됩니다. 카운터가 0으로 돌아가면서 사라집니다.
언어를 변경하거나 어두운 모드와 밝은 모드 간에 전환하거나 실행 중인 활동을 Android에서 다시 생성하게 하는 다른 구성 변경을 하는 경우에도 같은 상황이 발생합니다.
remember
를 사용하면 리컴포지션 간에 상태를 유지하는 데 도움이 되지만 구성 변경 간에는 유지되지 않습니다. 이를 위해서는 remember
대신 rememberSaveable
을 사용해야 합니다.
rememberSaveable
은 Bundle
에 저장할 수 있는 모든 값을 자동으로 저장합니다. 다른 값의 경우에는 맞춤 Saver 객체를 전달할 수 있습니다. Compose에서 상태 복원에 관한 자세한 내용은 문서를 참고하세요.
WaterCounter
에서 remember
를 rememberSaveable
로 바꿉니다.
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
var count by rememberSaveable { mutableStateOf(0) }
...
}
지금 앱을 실행하여 몇 가지 구성 변경을 시도해 보세요. 카운터가 제대로 저장되는 것을 확인할 수 있습니다.
활동 재생성은 rememberSaveable
의 사용 사례 중 하나일 뿐입니다. 목록을 사용하면서 나중에 다른 사용 사례를 살펴보겠습니다.
앱의 상태 및 UX 요구사항에 따라 remember
를 사용할지 rememberSaveable
을 사용할지 고려합니다.
9. 상태 호이스팅
remember
를 사용하여 객체를 저장하는 컴포저블에는 내부 상태가 포함되며 이는 컴포저블을 스테이트풀(Stateful)로 만듭니다. 이는 호출자가 상태를 제어할 필요가 없고 상태를 직접 관리하지 않아도 상태를 사용할 수 있는 경우에 유용합니다. 그러나 내부 상태를 갖는 컴포저블은 재사용 가능성이 적고 테스트하기가 더 어려운 경향이 있습니다.
상태를 보유하지 않는 컴포저블을 스테이트리스(Stateless) 컴포저블이라고 합니다. 상태 호이스팅을 사용하면 스테이트리스(Stateless) 컴포저블을 쉽게 만들 수 있습니다.
Compose에서 상태 호이스팅은 컴포저블을 스테이트리스(Stateless)로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴입니다. Jetpack Compose에서 상태 호이스팅을 위한 일반적 패턴은 상태 변수를 다음 두 개의 매개변수로 바꾸는 것입니다.
- value: T - 표시할 현재 값입니다.
- onValueChange: (T) -> Unit - 값이 새 값 T로 변경되도록 요청하는 이벤트입니다.
여기서 이 값은 수정할 수 있는 모든 상태를 나타냅니다.
이러한 방식으로 끌어올린 상태에는 중요한 속성이 몇 가지 있습니다.
- 단일 소스 저장소: 상태를 복제하는 대신 옮겼기 때문에 소스 저장소가 하나만 있습니다. 버그 방지에 도움이 됩니다.
- 공유 가능함: 끌어올린 상태를 여러 컴포저블과 공유할 수 있습니다.
- 가로채기 가능함: 스테이트리스(Stateless) 컴포저블의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정할 수 있습니다.
- 분리됨: 구성 가능한 스테이트리스(Stateless) 함수의 상태는 어디에든(예: ViewModel) 저장할 수 있습니다.
위의 모든 이점을 활용하려면 WaterCounter
에 이를 구현해 보세요.
스테이트풀(Stateful)과 스테이트리스(Stateless) 비교
구성 가능한 함수에서 모든 상태를 추출할 수 있는 경우 결과로 생성되는 구성 가능한 함수를 스테이트리스(Stateless)라고 합니다.
스테이트풀(Stateful)과 스테이트리스(Stateless) 카운터라는 두 부분으로 분할하여 WaterCounter
컴포저블을 리팩터링합니다.
StatelessCounter
의 역할은 count
를 표시하고 count
를 늘릴 때 함수를 호출하는 것입니다. 이렇게 하려면 위에 설명된 패턴을 따르고 count
상태(구성 가능한 함수에 매개변수로)와 onIncrement
람다(상태가 증가해야 할 때 호출됨)를 전달합니다. StatelessCounter
는 다음과 같이 표시됩니다.
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
StatefulCounter
는 상태를 소유합니다. 즉, count
상태를 보유하고 StatelessCounter
함수를 호출할 때 이 상태를 수정합니다.
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(count, { count++ }, modifier)
}
잘하셨습니다. count
를 StatelessCounter
에서 StatefulCounter
로 끌어올렸습니다.
이를 앱에 연결하고 StatefulCounter
로 WellnessScreen
을 업데이트할 수 있습니다.
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
StatefulCounter(modifier)
}
앞서 설명했듯이 상태 호이스팅에는 몇 가지 이점이 있습니다. 이 코드를 변경하여 이점을 설명해 보겠습니다. 앱에서 다음 스니펫을 복사하지 않아도 됩니다.
- 이제 스테이트리스(Stateless) 컴포저블을 재사용할 수 있습니다. 다음 예를 살펴보겠습니다.
물과 주스의 잔 개수를 계산하려면 waterCount
와 juiceCount
를 기억하고 동일한 StatelessCounter
구성 가능한 함수를 사용하여 서로 다른 두 가지 독립 상태를 표시합니다.
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
juiceCount
가 수정되면 StatefulCounter
가 재구성됩니다. 리컴포지션 중에 Compose는 juiceCount
를 읽는 함수를 식별하고 이러한 함수의 리컴포지션만 트리거합니다.
사용자가 탭하여 juiceCount
를 늘리면 StatefulCounter
가 재구성되고 juiceCount
를 읽는 StatelessCounter
도 재구성됩니다. 하지만 waterCount
를 읽는 StatelessCounter
는 재구성되지 않습니다.
- 구성 가능한 스테이트풀(Stateful) 함수는 여러 구성 가능한 함수에 동일한 상태를 제공할 수 있습니다.
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count, { count++ })
AnotherStatelessMethod(count, { count *= 2 })
}
이 경우 개수가 StatelessCounter
또는 AnotherStatelessMethod
에 의해 업데이트되면 예상대로 모든 항목이 재구성됩니다.
끌어올린 상태는 공유할 수 있으므로 불필요한 리컴포지션을 방지하고 재사용성을 높이려면 컴포저블에 필요한 상태만 전달해야 합니다.
상태 및 상태 호이스팅에 관한 자세한 내용은 Compose 상태 문서를 참고하세요.
10. 목록 사용
이제 앱의 두 번째 기능인 웰니스 작업 목록을 추가합니다. 목록에 있는 항목으로 다음 두 가지 작업을 할 수 있습니다.
- 작업을 완료로 표시하려면 목록 항목을 선택합니다.
- 완료하는 데 관심이 없는 작업을 목록에서 삭제합니다.
설정
- 먼저 목록 항목을 수정합니다. 컴포지션의 Remember 섹션의
WellnessTaskItem
을 재사용하여Checkbox
를 포함하도록 업데이트하면 됩니다. 함수를 스테이트리스(Stateless)로 만들려면checked
상태와onCheckedChange
콜백을 호이스팅해야 합니다.
이 섹션의 WellnessTaskItem
컴포저블은 다음과 같습니다.
import androidx.compose.material3.Checkbox
@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")
}
}
}
- 같은 파일에서 상태 변수
checkedState
를 정의하고 동일한 이름의 스테이트리스(Stateless) 메서드에 이를 전달하는 구성 가능한 스테이트풀(Stateful)WellnessTaskItem
함수를 추가합니다. 지금은onClose
에 관해 걱정하지 마세요. 빈 람다 함수를 전달해도 됩니다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@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,
)
}
WellnessTask.kt
파일을 만들어 ID와 라벨이 포함된 작업을 모델링합니다. 이를 데이터 클래스로 정의합니다.
data class WellnessTask(val id: Int, val label: String)
- 작업 목록 자체의 경우
WellnessTasksList.kt
라는 새 파일을 만들고 가짜 데이터를 생성하는 메서드를 추가합니다.
fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
실제 앱에서는 데이터 영역에서 데이터를 가져옵니다.
WellnessTasksList.kt
에서 목록을 만드는 구성 가능한 함수를 추가합니다. 직접 만든 목록 메서드에서LazyColumn
및 항목을 정의합니다. 도움이 필요한 경우 목록 문서를 참고하세요.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember
@Composable
fun WellnessTasksList(
modifier: Modifier = Modifier,
list: List<WellnessTask> = remember { getWellnessTasks() }
) {
LazyColumn(
modifier = modifier
) {
items(list) { task ->
WellnessTaskItem(taskName = task.label)
}
}
}
- 목록을
WellnessScreen
에 추가합니다.Column
을 사용하면 이미 있는 카운터와 목록을 세로로 정렬하는 데 도움이 됩니다.
import androidx.compose.foundation.layout.Column
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList()
}
}
- 앱을 실행하여 사용해 보세요. 이제 작업을 선택할 수 있지만 삭제할 수는 없습니다. 이 작업은 이후 섹션에서 구현합니다.
LazyList에서 항목 상태 복원
이제 WellnessTaskItem
컴포저블의 몇 가지 요소를 자세히 살펴보겠습니다.
checkedState
는 비공개 변수처럼 각 WellnessTaskItem
컴포저블에 독립적으로 속합니다. checkedState
가 변경되면 WellnessTaskItem
의 그 인스턴스만 재구성되며 LazyColumn
의 모든 WellnessTaskItem
인스턴스가 재구성되는 것은 아닙니다.
다음 단계에 따라 시도해 보세요.
- 이 목록 상단에 있는 요소(예: 요소 1, 2)를 선택합니다.
- 화면 밖으로 나가도록 목록 하단으로 스크롤합니다.
- 앞서 선택한 항목까지 상단으로 다시 스크롤합니다.
- 선택 해제되어 있습니다.
이전 섹션에서 살펴본 것처럼 항목이 컴포지션을 종료하면 기억된 상태가 삭제된다는 문제가 있습니다. LazyColumn
에 있는 항목의 경우 스크롤하면서 항목을 지나치면 항목이 컴포지션을 완전히 종료하므로 더 이상 항목이 표시되지 않습니다.
이 문제를 해결하려면 어떻게 해야 할까요? rememberSaveable
을 다시 사용하세요. 저장된 인스턴스 상태 메커니즘을 사용하여 활동 또는 프로세스 재생성 후에도 상태가 유지됩니다. rememberSaveable
이 LazyList
와 함께 작동하는 방식 덕분에 항목은 컴포지션을 종료해도 유지될 수 있습니다.
스테이트풀(Stateful) WellnessTaskItem
에서 remember
를 rememberSaveable
로 바꾸기만 하면 됩니다.
import androidx.compose.runtime.saveable.rememberSaveable
var checkedState by rememberSaveable { mutableStateOf(false) }
Compose의 일반적인 패턴
LazyColumn
의 구현을 확인합니다.
@Composable
fun LazyColumn(
...
state: LazyListState = rememberLazyListState(),
...
구성 가능한 함수 rememberLazyListState
는 rememberSaveable
을 사용하여 목록의 초기 상태를 만듭니다. 활동이 다시 생성되면 스크롤 상태는 아무런 코딩을 하지 않아도 유지됩니다.
많은 앱이 스크롤 위치, 항목 레이아웃 변경사항, 목록의 상태와 관련된 기타 이벤트에 반응하고 이를 수신 대기해야 합니다. LazyColumn
또는 LazyRow
와 같은 지연 구성요소는 LazyListState
를 끌어올려 이 사용 사례를 지원합니다. 이 패턴에 관한 자세한 내용은 목록의 상태 문서를 참고하세요.
공개 rememberX
함수에서 제공하는 기본값이 포함된 상태 매개변수가 있는 것이 구성 가능한 내장 함수에서 일반적인 패턴입니다. rememberBottomSheetScaffoldState
를 사용하여 상태를 끌어올리는 BottomSheetScaffold
에서 또 다른 예를 확인할 수 있습니다.
11. 관찰 가능한 MutableList
이제 목록에서 작업을 삭제하는 동작을 추가하려면 먼저 목록을 변경 가능한 목록으로 만들어야 합니다.
이를 위해 변경 가능한 객체(예: ArrayList<T>
또는 mutableListOf,
)를 사용하면 작동하지 않습니다. 이러한 유형은 목록의 항목이 변경되었고 UI의 리컴포지션을 예약한다고 Compose에 알리지 않습니다. 다른 API가 필요합니다.
Compose에서 관찰할 수 있는 MutableList
인스턴스를 만들어야 합니다. 이 구조를 사용하면 Compose가 항목이 추가되거나 목록에서 삭제될 때 변경사항을 추적하여 UI를 재구성할 수 있습니다.
먼저 관찰 가능한 MutableList
를 정의합니다. 확장 함수 toMutableStateList()
를 사용하면 변경 가능하거나 변경 불가능한 초기 Collection
(예: List
)에서 관찰 가능한 MutableList
를 만들 수 있습니다.
또는 팩토리 메서드 mutableStateListOf
를 사용하여 관찰 가능한 MutableList
를 만들고 초기 상태의 요소를 추가할 수도 있습니다.
WellnessScreen.kt
파일을 엽니다.getWellnessTasks
메서드를 이 파일로 이동해야 사용할 수 있습니다. 먼저getWellnessTasks()
를 호출하고 이전에 배운 확장 함수toMutableStateList
를 사용하여 목록을 만듭니다.
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
@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") }
- 목록의 기본값을 삭제하여 구성 가능한
WellnessTasksList
함수를 수정합니다. 목록이 화면 수준으로 끌어올려지기 때문입니다. 새 람다 함수 매개변수onCloseTask
를 추가합니다(삭제할WellnessTask
수신).onCloseTask
를WellnessTaskItem
에 전달합니다.
한 가지를 더 변경해야 합니다. items
메서드는 key
매개변수를 수신합니다. 기본적으로 각 항목의 상태는 목록에 있는 항목의 위치를 기준으로 키가 지정됩니다.
변경 가능한 목록에서는 데이터 세트가 변경될 때 문제가 발생합니다. 위치를 변경하는 항목은 기억된 상태를 사실상 잃기 때문입니다.
이 문제는 각 WellnessTaskItem
의 id
를 각 항목의 키로 사용하면 쉽게 해결할 수 있습니다.
목록의 항목 키에 관한 자세한 내용은 문서를 참고하세요.
WellnessTasksList
는 다음과 같습니다.
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
}
}
}
WellnessTaskItem
을 수정합니다.onClose
람다 함수를 스테이트풀(Stateful)WellnessTaskItem
에 매개변수로 추가하고 이를 호출합니다.
@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,
)
}
잘하셨습니다. 기능이 완료되어 목록에서 항목을 삭제할 수 있습니다.
각 행에서 X를 클릭하면 이벤트가 상태를 소유하는 목록까지 위로 이동하므로 목록에서 항목을 삭제할 수 있고 Compose는 화면을 재구성할 수 있습니다.
rememberSaveable()
를 사용하여 WellnessScreen
에 목록을 저장하려고 하면 런타임 예외가 발생합니다.
이 오류는 맞춤 Saver를 제공해야 함을 나타냅니다. 그러나 긴 직렬화 또는 역직렬화가 필요한 복잡한 데이터 구조나 대량의 데이터를 저장하는 데 rememberSaveable
을 사용해서는 안 됩니다.
Activity의 onSaveInstanceState
를 사용할 때도 유사한 규칙이 적용됩니다. 자세한 내용은 UI 상태 저장 문서를 참고하세요. 이 작업을 실행하려면 대체 저장 메커니즘이 필요합니다. 다양한 UI 상태 유지 옵션에 관한 자세한 내용은 문서를 참고하세요.
이제 앱 상태 홀더로서 ViewModel의 역할을 살펴보겠습니다.
12. ViewModel의 상태
화면 또는 UI 상태는 화면에 표시할 내용을 나타냅니다(예: 작업 목록). 이 상태는 애플리케이션 데이터를 포함하므로 대개 계층 구조의 다른 레이어에 연결됩니다.
UI 상태는 화면에 표시할 내용을 설명하지만 앱의 로직은 앱의 동작 방식을 설명하고 상태 변경에 반응해야 합니다. 로직 유형에는 두 가지가 있습니다. UI 동작 또는 UI 로직과 비즈니스 로직입니다.
- UI 로직은 화면에 상태 변경을 표시하는 방법(예: 탐색 로직 또는 스낵바 표시)과 관련이 있습니다.
- 비즈니스 로직은 상태 변경 시(예: 결제하기 또는 사용자 환경설정 저장) 실행할 작업입니다. 이 로직은 대개 비즈니스 레이어나 데이터 영역에 배치되고 UI 레이어에는 배치되지 않습니다.
ViewModel은 UI 상태와 앱의 다른 레이어에 있는 비즈니스 로직에 대한 액세스 권한을 제공합니다. 또한 ViewModel은 구성 변경 후에도 유지되므로 컴포지션보다 전체 기간이 더 깁니다. Compose 콘텐츠 호스트의 수명 주기(즉, 활동이나 프래그먼트, Compose Navigation을 사용하는 경우 탐색 그래프의 대상)를 따를 수 있습니다.
아키텍처 및 UI 레이어에 관한 자세한 내용은 UI 레이어 문서를 참고하세요.
목록 이전 및 메서드 삭제
이전 단계에서는 구성 가능한 함수에서 상태를 직접 관리하는 방법을 보여주었지만 UI 로직과 비즈니스 로직을 UI 상태와 분리하여 ViewModel로 이전하는 것이 좋습니다.
UI 상태, 목록을 ViewModel로 이전하고 비즈니스 로직도 ViewModel로 추출해 보겠습니다.
WellnessViewModel.kt
파일을 만들어 ViewModel 클래스를 추가합니다.
'데이터 소스' getWellnessTasks()
를 WellnessViewModel
로 이동합니다.
이전과 마찬가지로 toMutableStateList
를 사용하여 내부 _tasks
변수를 정의하고 tasks
를 목록으로 노출하여 ViewModel 외부에서 수정할 수 없도록 합니다.
목록의 내장 remove 함수에 위임하는 간단한 remove
함수를 구현합니다.
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
class WellnessViewModel : ViewModel() {
private val _tasks = getWellnessTasks().toMutableStateList()
val tasks: List<WellnessTask>
get() = _tasks
fun remove(item: WellnessTask) {
_tasks.remove(item)
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
viewModel()
함수를 호출하여 컴포저블에서 이 ViewModel에 액세스할 수 있습니다.
이 함수를 사용하려면 app/build.gradle.kts
파일을 열고 다음 라이브러리를 추가한 후 Android 스튜디오에서 새 종속 항목을 동기화합니다.
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")
Android 스튜디오 Giraffe를 사용할 때는 2.6.2
버전을 사용하세요. 또는 여기에서 최신 버전의 라이브러리를 확인하세요.
WellnessScreen
을 엽니다. 화면 컴포저블의 매개변수로viewModel()
을 호출하여wellnessViewModel
ViewModel을 인스턴스화합니다. 따라서 이 컴포저블을 테스트할 때 교체하고 필요에 따라 끌어올릴 수 있습니다.WellnessTasksList
에 작업 목록을 제공하고onCloseTask
람다에 remove 함수를 제공합니다.
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCloseTask = { task -> wellnessViewModel.remove(task) })
}
}
viewModel()
은 기존 ViewModel
을 반환하거나 지정된 범위에서 새 ViewModel을 생성합니다. ViewModel 인스턴스는 범위가 활성화되어 있는 동안 유지됩니다. 예를 들어 컴포저블이 활동에서 사용되는 경우 viewModel()
은 활동이 완료되거나 프로세스가 종료될 때까지 동일한 인스턴스를 반환합니다.
이제 모두 완료되었습니다. 상태 일부와 비즈니스 로직이 포함된 ViewModel을 화면과 통합했습니다. 상태는 컴포지션 외부에 유지되고 ViewModel에 의해 저장되므로 목록의 변형은 구성이 변경되어도 유지됩니다.
ViewModel은 어떤 시나리오에서도 앱의 상태를 자동으로 유지하지 않습니다(예: 시스템에서 시작된 프로세스 중단의 경우). 앱의 UI 상태 유지에 관한 자세한 내용은 문서를 참고하세요.
선택한 상태 이전
마지막 리팩터링은 선택된 상태와 로직을 ViewModel로 이전하는 것입니다. 이렇게 하면 모든 상태가 ViewModel에서 관리되므로 코드가 더 간단해지고 테스트하기 쉬워집니다.
- 먼저 선택된 상태를 저장하고 false를 기본값으로 설정할 수 있도록
WellnessTask
모델 클래스를 수정합니다.
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
- ViewModel에서 선택된 상태의 새 값으로 수정할 작업을 수신하는
changeTaskChecked
메서드를 구현합니다.
class WellnessViewModel : ViewModel() {
...
fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
_tasks.find { it.id == item.id }?.let { task ->
task.checked = checked
}
}
WellnessScreen
에서 ViewModel의changeTaskChecked
메서드를 호출하여 목록의onCheckedTask
동작을 제공합니다. 이제 함수가 다음과 같이 표시됩니다.
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCheckedTask = { task, checked ->
wellnessViewModel.changeTaskChecked(task, checked)
},
onCloseTask = { task ->
wellnessViewModel.remove(task)
}
)
}
}
WellnessTasksList
를 열고WellnessTaskItem.
에 전달할 수 있도록onCheckedTask
람다 함수 매개변수를 추가합니다.
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCheckedTask: (WellnessTask, Boolean) -> Unit,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
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) }
)
}
}
}
WellnessTaskItem.kt
파일을 정리합니다. CheckBox 상태가 목록 수준으로 끌어올려지므로 더 이상 스테이트풀(Stateful) 메서드가 필요하지 않습니다. 파일에는 다음과 같은 구성 가능한 함수만 있습니다.
@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")
}
}
}
- 앱을 실행하고 작업을 선택해 봅니다. 작업 선택이 아직 잘 작동하지 않습니다.
이는 Compose에서 MutableList
를 위해 추적하는 것이 요소 추가 및 삭제와 관련된 변경사항이기 때문입니다. 삭제가 작동하는 이유가 바로 이것입니다. 하지만 추적하도록 지시하지 않는 한 행 항목 값(여기서는 checkedState
)의 변경사항을 인식하지 못합니다.
문제를 해결하는 두 가지 방법은 다음과 같습니다.
- 데이터 클래스
WellnessTask
를 변경하여checkedState
가Boolean
대신MutableState<Boolean>
이 되도록 합니다. 그러면 Compose에서 항목 변경사항을 추적합니다. - 변경하려는 항목을 복사하고 목록에서 항목을 삭제한 후 변경된 항목을 다시 목록에 추가합니다. 그러면 Compose에서 이 목록 변경사항을 추적합니다.
두 가지 방법에는 모두 장단점이 있습니다. 예를 들어 사용 중인 목록의 구현에 따라 요소를 삭제하고 읽는 데 비용이 많이 들 수 있습니다.
잠재적으로 비용이 많이 드는 목록 작업을 피하고, 더 효율적이고 Compose 직관적이므로 checkedState
를 관찰 가능하도록 만들고자 한다고 가정해 보겠습니다.
새 WellnessTask
는 다음과 같이 표시될 수 있습니다.
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))
이전에 본 것처럼 위임된 속성을 사용하면 이 경우에 checked
변수를 더 간단하게 사용할 수 있습니다.
WellnessTask
를 데이터 클래스가 아닌 클래스가 되도록 변경합니다. WellnessTask
가 생성자에서 기본값이 false
인 initialChecked
변수를 수신하도록 하면 팩토리 메서드 mutableStateOf
로 checked
변수를 초기화하여 initialChecked
를 기본값으로 사용할 수 있습니다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class WellnessTask(
val id: Int,
val label: String,
initialChecked: Boolean = false
) {
var checked by mutableStateOf(initialChecked)
}
이제 완료됐습니다. 이 솔루션은 효과가 있고 리컴포지션 및 구성 변경 후에도 모든 변경사항이 유지됩니다.
테스트
이제 결합된 내부 구성 가능한 함수가 아닌 ViewModel로 비즈니스 로직이 리팩터링되므로 단위 테스트가 훨씬 간단해집니다.
계측 테스트를 사용하여 Compose 코드의 올바른 동작을 확인하고 UI 상태가 올바르게 작동하는지 확인할 수 있습니다. Compose의 테스트 Codelab을 통해 Compose UI를 테스트하는 방법을 알아보세요.
13. 축하합니다
잘하셨습니다. 이 Codelab을 성공적으로 완료하고 Jetpack Compose 앱에서 상태를 사용하는 모든 기본 API를 알아봤습니다.
상태와 이벤트를 고려하여 Compose에서 스테이트리스(Stateless) 컴포저블을 추출하는 방법과 Compose에서 상태 업데이트를 사용하여 UI 변경을 유도하는 방법을 살펴봤습니다.
다음 단계
Compose 개발자 과정의 다른 Codelab을 확인하세요.
샘플 앱
- JetNews는 이 Codelab에 설명된 권장사항을 보여줍니다.
추가 문서
- Compose 이해
- 상태 및 Jetpack Compose
- Jetpack Compose의 단방향 데이터 흐름
- Compose에서 상태 복원
- ViewModel 개요
- Compose 및 기타 라이브러리