1. 시작하기 전에
Jetpack Compose는 UI 개발을 간소화하기 위해 설계된 최신 툴킷입니다. 반응형 프로그래밍 모델을 Kotlin 프로그래밍 언어의 간결함 및 사용 편의성과 결합합니다. 이는 완전히 선언적인 접근 방식으로, 데이터를 UI 계층 구조로 변환하는 일련의 함수를 호출하여 UI를 설명합니다. 기본 데이터가 변경되면 프레임워크가 이러한 함수를 자동으로 다시 실행하여 UI 계층 구조를 업데이트합니다.
Compose 앱은 구성 가능한 함수로 구성됩니다. 구성 가능한 함수는 @Composable
이라고 표시된 일반 함수이며 다른 구성 가능한 함수를 호출할 수 있습니다. 새로운 UI 구성요소를 만들기 위해서는 함수만 있으면 됩니다. 주석은 지속적으로 UI를 업데이트하고 유지관리하기 위해 함수에 특수 지원을 추가하도록 Compose에 알려주는 역할을 합니다. Compose를 사용하면 코드를 작은 청크로 구성할 수 있습니다. 대개 구성 가능한 함수를 줄여서 '컴포저블'이라고 합니다.
재사용이 가능한 작은 컴포저블을 만들면 앱에 사용하는 UI 요소의 라이브러리를 쉽게 빌드할 수 있습니다. 각 요소는 화면의 한 부분을 담당하며 독립적으로 수정할 수 있습니다.
이 Codelab을 진행하는 동안 도움이 추가로 필요한 경우 다음 Code-Along 동영상을 시청하세요.
참고: 이 Codelab은 Material 3을 사용하도록 업데이트된 반면, Code-Along 동영상에서는 Material 2를 사용하고 있습니다. 몇 가지 단계에 차이가 있을 수 있다는 점에 유의하세요.
기본 요건
- 람다를 비롯한 Kotlin 문법 사용 경험
실행할 작업
이 Codelab에서는 다음에 관해 알아봅니다.
- Compose의 정의
- Compose로 UI를 빌드하는 방법
- 구성 가능한 함수의 상태를 관리하는 방법
- 성능 기준에 맞는 목록을 만드는 방법
- 애니메이션을 추가하는 방법
- 앱 스타일과 테마를 지정하는 방법
다음과 같이 애니메이션으로 항목이 펼쳐지는 목록과 온보딩 화면이 포함된 앱을 빌드합니다.
필요한 항목
2. 새 Compose 프로젝트 시작
새 Compose 프로젝트를 시작하려면 Android 스튜디오를 엽니다.
Welcome to Android Studio 창에 있다면 Start a new Android Studio project를 클릭합니다. 이미 Android 스튜디오 프로젝트가 열려 있다면 메뉴 바에서 File > New > New Project를 선택합니다.
새 프로젝트의 경우 제공되는 템플릿에서 Empty Activity를 선택합니다.
Next를 클릭하고 평소 프로젝트를 구성하던 대로 'Basics Codelab'이라는 프로젝트를 구성합니다. minimumSdkVersion으로 API 수준 21 이상을 선택해야 합니다. 이는 Compose에서 지원하는 최소 API 수준입니다.
Empty Activity 템플릿을 선택하면 프로젝트에 다음 코드가 생성됩니다.
- 프로젝트가 이미 Compose를 사용하도록 구성되어 있습니다.
AndroidManifest.xml
파일이 생성됩니다.build.gradle.kts
및app/build.gradle.kts
파일에는 Compose에 필요한 옵션과 종속 항목이 포함되어 있습니다.
프로젝트를 동기화한 후 MainActivity.kt
를 열고 코드를 확인합니다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greeting("Android")
}
}
다음 섹션에서는 각 메서드의 역할과 이러한 메서드를 개선하여 유연하고 재사용 가능한 레이아웃을 만드는 방법을 알아봅니다.
Codelab 솔루션
GitHub에서 이 Codelab의 솔루션 코드를 다운로드할 수 있습니다.
$ git clone https://github.com/android/codelab-android-compose
또는 저장소를 ZIP 파일로 다운로드할 수도 있습니다.
솔루션 코드는 BasicsCodelab
프로젝트에서 확인할 수 있습니다. 자신의 속도에 맞게 Codelab을 단계별로 진행하고 필요한 경우 솔루션을 확인하는 것이 좋습니다. Codelab을 진행하는 중에 프로젝트에 추가해야 하는 코드 스니펫이 제공됩니다.
3. Compose 시작하기
Android 스튜디오에서 생성한 Compose 관련 클래스 및 메서드를 살펴보겠습니다.
구성 가능한 함수
구성 가능한 함수는 @Composable
이라는 주석이 달린 일반 함수입니다. 이렇게 하면 함수가 내부에서 다른 @Composable
함수를 호출할 수 있습니다. Greeting
함수를 @Composable
로 어떻게 표시하는지 확인할 수 있습니다. 이 함수는 지정된 입력(String
)을 표시하는 UI 계층 구조를 생성합니다. Text
는 라이브러리에서 제공하는 구성 가능한 함수입니다.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
Android 앱의 Compose
Compose를 사용하면 Activity
가 Android 앱의 진입점으로 유지됩니다. 이 프로젝트에서는 AndroidManifest.xml
파일에 지정된 대로 사용자가 앱을 열 때 MainActivity
가 실행됩니다. setContent
를 사용하여 레이아웃을 정의하지만, 기존 뷰 시스템에서 하던 것처럼 XML 파일을 사용하는 대신 이 함수에서 구성 가능한 함수를 호출합니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
BasicsCodelabTheme
은 구성 가능한 함수의 스타일을 지정하는 방법입니다. 이에 관한 자세한 내용은 앱 테마 설정 섹션을 참고하세요. 텍스트가 화면에 어떻게 표시되는지 확인하려면 에뮬레이터나 기기에서 앱을 실행하거나 Android 스튜디오 미리보기를 사용하면 됩니다.
Android 스튜디오 미리보기를 사용하려면 매개변수가 없는 구성 가능한 함수 또는 기본 매개변수를 포함하는 함수를 @Preview
주석으로 표시하고 프로젝트를 빌드하기만 하면 됩니다. MainActivity.kt
파일에 이미 Preview Composable
함수가 있는 것을 볼 수 있습니다. 동일한 파일에 미리보기를 여러 개 만들고 이름을 지정할 수 있습니다.
@Preview(showBackground = true, name = "Text preview")
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greeting(name = "Android")
}
}
Code 가 선택되어 있는 경우 미리보기가 표시되지 않을 수 있습니다. 미리보기를 보려면 Split 을 클릭하세요.
4. UI 조정
먼저 Greeting
에 다른 배경 색상을 설정해 보겠습니다. 이렇게 하려면 Text
컴포저블을 Surface
로 래핑하면 됩니다. Surface
는 색상을 사용하므로 MaterialTheme.colorScheme.primary
를 사용합니다.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
}
Surface
내부에 중첩된 구성요소는 배경 색상 위에 그려집니다.
다음과 같이 미리보기에서 새로운 변경사항을 확인할 수 있습니다.
중요한 사항을 놓쳤을 수도 있습니다. 지금 보이는 텍스트는 흰색입니다. 언제 텍스트를 흰색으로 정의했나요?
정의하지 않았습니다. androidx.compose.material3.Surface
와 같은 Material 구성요소는 앱에 넣고자 하는 공통 기능(예: 텍스트에 적절한 색상 선택)을 처리하여 더 나은 환경을 만들도록 빌드됩니다. Material은 대부분의 앱에 공통으로 적용되는 적절한 기본값과 패턴을 제공하므로 유연성이 낮은 편입니다. Compose의 Material 구성요소는 androidx.compose.foundation
의 다른 기본 구성요소를 기반으로 빌드되며 이러한 구성요소는 더 많은 유연성이 필요한 경우 앱 구성요소에서 액세스할 수도 있습니다.
이 경우, Surface
는 배경이 primary
색상으로 설정될 때 배경 위의 모든 텍스트가 테마에도 정의된 onPrimary
색상을 사용해야 한다는 것을 인식합니다. 이에 관한 자세한 내용은 앱 테마 설정 섹션을 참고하세요.
수정자
Surface
및 Text
와 같은 대부분의 Compose UI 요소는 modifier
매개변수를 선택적으로 허용합니다. 수정자는 상위 요소 레이아웃 내에서 UI 요소가 배치되고 표시되고 동작하는 방식을 UI 요소에 알려줍니다. 이미 알겠지만 Greeting
컴포저블에는 이미 기본 수정자가 있으며 이 기본 수정자는 Text
에 전달됩니다.
예를 들어, padding
수정자는 수정자가 데코레이션하는 요소 주변의 공간을 나타냅니다. Modifier.padding()
으로 패딩 수정자를 만들 수 있습니다. 체이닝을 통해 여러 수정자를 추가할 수도 있으므로 여기서는 패딩 수정자를 기본 수정자 modifier.padding(24.dp)
에 추가할 수 있습니다.
이제 화면의 Text
에 패딩을 추가합니다.
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
// ...
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Text(
text = "Hello $name!",
modifier = modifier.padding(24.dp)
)
}
}
정렬, 애니메이션 처리, 배치, 클릭 가능 여부 또는 스크롤 가능 여부 지정, 변환 등에 사용할 수 있는 수십 가지의 수정자가 있습니다. 전체 목록을 확인하려면 Compose 수정자 목록을 참고하세요. 다음 단계에서는 이러한 수정자 중 일부를 사용합니다.
5. 컴포저블 재사용
UI에 추가하는 구성요소가 많을수록 생성되는 중첩 레벨이 더 많아집니다. 함수가 매우 커지면 가독성에 영향을 줄 수 있습니다. 재사용할 수 있는 작은 구성요소를 만들면 앱에서 사용하는 UI 요소의 라이브러리를 쉽게 만들 수 있습니다. 각 요소는 화면의 작은 부분을 담당하며 독립적으로 수정할 수 있습니다.
함수는 기본적으로 빈 수정자가 할당되는 수정자 매개변수를 포함하는 것이 좋습니다. 함수 내에서 호출하는 첫 번째 컴포저블로 이 수정자를 전달합니다. 이렇게 하면 호출 사이트가 구성 가능한 함수 외부에서 레이아웃 안내와 동작을 조정할 수 있습니다.
인사말이 포함된 MyApp
이라는 컴포저블을 만듭니다.
@Composable
fun MyApp(modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
이를 사용하면 MyApp
컴포저블을 재사용하여 코드 중복을 피할 수 있으므로 onCreate
콜백과 미리보기를 정리할 수 있습니다.
미리보기에서 MyApp
을 호출하고 미리보기의 이름을 삭제해 보겠습니다.
MainActivity.kt
파일은 다음과 같이 표시됩니다.
package com.example.basicscodelab
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Text(
text = "Hello $name!",
modifier = modifier.padding(24.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
MyApp()
}
}
6. 열과 행 만들기
Compose의 세 가지 기본 표준 레이아웃 요소는 Column
, Row
, Box
입니다.
이러한 요소는 컴포저블 콘텐츠를 사용하는 구성 가능한 함수이므로 내부에 항목을 배치할 수 있습니다. 예를 들어 Column
내부의 각 하위 요소는 세로로 배치됩니다.
// Don't copy over
Column {
Text("First row")
Text("Second row")
}
이제 아래의 예와 같이 두 개의 텍스트 요소가 있는 열을 표시하도록 Greeting
을 변경해 보겠습니다.
패딩을 이동해야 할 수도 있습니다.
이 솔루션과 결과를 비교해 보세요.
import androidx.compose.foundation.layout.Column
// ...
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Column(modifier = modifier.padding(24.dp)) {
Text(text = "Hello ")
Text(text = name)
}
}
}
Compose와 Kotlin
구성 가능한 함수는 Kotlin의 다른 함수처럼 사용할 수 있습니다. 이는 UI가 표시되는 방식에 영향을 주는 구문을 추가할 수 있으므로 매우 강력한 UI를 제작할 수 있게 해줍니다.
예를 들어, for
루프를 사용하여 Column
에 요소를 추가할 수 있습니다.
@Composable
fun MyApp(
modifier: Modifier = Modifier,
names: List<String> = listOf("World", "Compose")
) {
Column(modifier) {
for (name in names) {
Greeting(name = name)
}
}
}
아직 크기를 설정하지 않았거나 컴포저블의 크기에 제약사항을 추가하지 않았으므로 각 행은 사용할 수 있는 최소 공간을 차지하며 미리보기도 같은 작업을 실행합니다. 미리보기를 변경하여 소형 스마트폰의 일반적인 너비인 320dp로 에뮬레이션해 보겠습니다. 다음과 같이 @Preview
주석에 widthDp
매개변수를 추가합니다.
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
MyApp()
}
}
수정자는 Compose에서 광범위하게 사용되므로 좀 더 고급 과정의 실습을 해보겠습니다. fillMaxWidth
와 padding
수정자를 사용하여 다음 레이아웃을 복제해 보세요.
이제 코드를 솔루션과 비교해 보세요.
import androidx.compose.foundation.layout.fillMaxWidth
@Composable
fun MyApp(
modifier: Modifier = Modifier,
names: List<String> = listOf("World", "Compose")
) {
Column(modifier = modifier.padding(vertical = 4.dp)) {
for (name in names) {
Greeting(name = name)
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
Text(text = "Hello ")
Text(text = name)
}
}
}
참고:
- 예를 들어, 수정자에는 오버로드가 있을 수 있으므로 패딩을 만드는 다양한 방법을 지정할 수 있습니다.
- 요소에 여러 수정자를 추가하려면 수정자를 연결하기만 하면 됩니다.
이러한 결과를 얻는 데는 여러 방법이 있으므로 코드가 이 스니펫과 일치하지 않는다고 해서 코드가 잘못되었다는 의미는 아닙니다. 그러나 Codelab을 계속 진행하려면 이 코드를 복사하여 붙여넣으세요.
버튼 추가
다음 단계에서는 Greeting
을 펼치는 클릭 가능한 요소를 추가하므로 이 버튼을 먼저 추가해야 합니다. 목표는 다음과 같은 레이아웃을 만드는 것입니다.
Button
은 material3 패키지에서 제공하는 컴포저블로, 컴포저블을 마지막 인수로 사용합니다. 후행 람다는 괄호 밖으로 이동할 수 있으므로 모든 콘텐츠를 버튼에 하위 요소로 추가할 수 있습니다. 예를 들어, 다음과 같이 Text
를 추가할 수 있습니다.
// Don't copy yet
Button(
onClick = { } // You'll learn about this callback later
) {
Text("Show less")
}
이렇게 하려면 컴포저블을 행 끝에 배치하는 방법을 알아야 합니다. alignEnd
수정자가 없으므로 시작 시 컴포저블에 약간의 weight
을 제공합니다. weight
수정자는 요소를 유연하게 만들기 위해 가중치가 없는 다른 요소(유연성 부족이라고 함)를 효과적으로 밀어내어 요소의 사용 가능한 모든 공간을 채웁니다. 이 수정자는 fillMaxWidth
수정자와 중복되기도 합니다.
이제 버튼을 추가하고 이전 이미지에 표시된 것처럼 버튼을 배치합니다.
여기에서 솔루션을 확인하세요.
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.ElevatedButton
// ...
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello ")
Text(text = name)
}
ElevatedButton(
onClick = { /* TODO */ }
) {
Text("Show more")
}
}
}
}
7. Compose에서의 상태
이 섹션에서는 화면에 약간의 상호작용을 추가합니다. 지금까지는 정적 레이아웃을 만들었지만, 이제는 사용자 변경사항에 반응하여 화면과 상호작용할 수 있게 합니다.
버튼을 클릭할 수 있게 만드는 방법과 항목의 크기를 조절하는 방법을 알아보기 전에 각 항목이 펼쳐진 상태인지를 가리키는 값을 어딘가에 저장해야 합니다. 이 값을 항목의 상태라고 합니다. 인사말마다 이러한 값 중 하나가 필요하므로 이 값의 논리적 위치는 Greeting
컴포저블에 있습니다. 이 불리언 값 expanded
와 이 값이 코드에서 사용되는 방식을 살펴보세요.
// Don't copy over
@Composable
fun Greeting(name: String) {
var expanded = false // Don't do this!
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
onClick
작업과 동적 버튼 텍스트도 추가했습니다. 이에 관한 설명은 나중에 자세히 다룹니다.
하지만, 이 작업은 예상대로 작동하지 않습니다. expanded
변수에 다른 값을 설정해도 Compose에서 이 값을 상태 변경으로 감지하지 않으므로 아무 일도 일어나지 않습니다.
이 변수를 변경했을 때 리컴포지션을 트리거하지 않는 이유는 이 변수를 Compose에서 추적하고 있지 않기 때문입니다. 또한, Greeting
이 호출될 때마다 변수가 거짓으로 재설정됩니다.
컴포저블에 내부 상태를 추가하려면 mutableStateOf
함수를 사용하면 됩니다. 이 함수를 사용하면 Compose가 이 State
를 읽는 함수를 재구성합니다.
import androidx.compose.runtime.mutableStateOf
// ...
// Don't copy over
@Composable
fun Greeting() {
val expanded = mutableStateOf(false) // Don't do this!
}
하지만, 컴포저블 내의 변수에 mutableStateOf
를 할당하기만 할 수는 없습니다. 앞에서 설명한 것처럼 false
값을 가진 변경 가능한 새 상태로 상태를 재설정하여 컴포저블을 다시 호출하는 때는 언제든지 리컴포지션이 일어날 수 있습니다.
여러 리컴포지션 간에 상태를 유지하려면 remember
를 사용하여 변경 가능한 상태를 기억해야 합니다.
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// ...
@Composable
fun Greeting(...) {
val expanded = remember { mutableStateOf(false) }
// ...
}
remember
는 리컴포지션을 방지하는 데 사용되므로 상태가 재설정되지 않습니다.
화면의 서로 다른 부분에서 동일한 컴포저블을 호출하는 경우 자체 상태 버전을 가진 UI 요소를 만듭니다. 내부 상태는 클래스의 비공개 변수로 보면 됩니다.
구성 가능한 함수는 상태를 자동으로 '구독'합니다. 상태가 변경되면 이러한 필드를 읽는 컴포저블이 재구성되어 업데이트를 표시합니다.
상태 변경 및 상태 변경사항에 반응
상태를 변경하기 위해 Button
이 onClick
이라는 매개변수를 사용한다고 알고 있을 수도 있지만, 값을 사용하지 않고 함수를 사용합니다.
작업에 람다 표현식을 할당하여 클릭 시 실행할 작업을 정의할 수 있습니다. 예를 들어, 펼침 상태의 값을 전환하고 값에 따라 다른 텍스트를 표시해 보겠습니다.
ElevatedButton(
onClick = { expanded.value = !expanded.value },
) {
Text(if (expanded.value) "Show less" else "Show more")
}
앱을 대화형 모드로 실행하여 동작을 확인합니다.
버튼을 클릭하면 expanded
가 전환되어 버튼 내부의 텍스트 리컴포지션을 트리거합니다. 각 Greeting
은 서로 다른 UI 요소에 속하기 때문에 자체적으로 펼쳐진 상태를 유지합니다.
이 지점까지의 코드는 다음과 같습니다.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val expanded = remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
항목 펼치기
이제 실제로 요청을 받은 경우 항목을 펼쳐 보겠습니다. 상태에 따라 달라지는 추가 변수를 추가합니다.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val expanded = remember { mutableStateOf(false) }
val extraPadding = if (expanded.value) 48.dp else 0.dp
// ...
extraPadding
은 간단한 계산을 실행하므로 리컴포지션에 대비하여 이 값을 기억할 필요가 없습니다.
따라서, 이제 Column에 새로운 패딩 수정자를 적용할 수 있습니다.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val expanded = remember { mutableStateOf(false) }
val extraPadding = if (expanded.value) 48.dp else 0.dp
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(
modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
에뮬레이터나 대화형 모드에서 실행하는 경우 각 항목이 독립적으로 펼쳐질 수 있어야 합니다.
8. 상태 호이스팅
구성 가능한 함수에서 여러 함수가 읽거나 수정하는 상태는 공통의 상위 항목에 위치해야 합니다. 이 프로세스를 상태 호이스팅이라고 합니다. 호이스팅이란 들어 올린다 또는 끌어올린다라는 의미입니다.
상태를 호이스팅할 수 있게 만들면 상태가 중복되지 않고 버그가 발생하는 것을 방지할 수 있으며 컴포저블을 재사용할 수 있고 훨씬 쉽게 테스트할 수 있습니다. 이에 반하여, 컴포저블의 상위 요소에서 제어할 필요가 없는 상태는 호이스팅되면 안 됩니다. 정보 소스는 상태를 생성하고 관리하는 대상에 속합니다.
예를 들어, 앱의 온보딩 화면을 만들어 보겠습니다.
다음 코드를 MainActivity.kt
에 추가합니다.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.Button
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
// ...
@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
// TODO: This state should be hoisted
var shouldShowOnboarding by remember { mutableStateOf(true) }
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Basics Codelab!")
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = { shouldShowOnboarding = false }
) {
Text("Continue")
}
}
}
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
BasicsCodelabTheme {
OnboardingScreen()
}
}
이 코드에는 여러 가지 새로운 기능이 포함되어 있습니다.
OnboardingScreen
이라는 새 컴포저블과 새 미리보기를 추가했습니다. 프로젝트를 빌드하면 동시에 여러 개의 미리보기가 있을 수 있습니다. 또한, 콘텐츠가 정확하게 정렬되는지 확인할 수 있도록 고정된 높이를 추가했습니다.- 화면 중앙에 콘텐츠를 표시할 수 있도록
Column
을 구성할 수 있습니다. shouldShowOnboarding
은=
대신by
키워드를 사용하고 있습니다. 이 키워드는 매번.value
를 입력할 필요가 없도록 해주는 속성 위임입니다.- 버튼을 클릭하면
shouldShowOnboarding
이false
로 설정되지만, 아직 어디에서도 이 상태를 읽지 않습니다.
이제 앱에 이 새로운 온보딩 화면을 추가할 수 있습니다. 시작 시 이 화면을 표시하고 사용자가 'Continue'를 누르면 숨깁니다.
Compose에서는 UI 요소를 숨기지 않습니다. 대신, 컴포지션에 UI 요소를 추가하지 않으므로 Compose가 생성하는 UI 트리에 추가되지 않습니다. 간단한 조건부 Kotlin 로직을 사용하여 이 작업을 실행합니다. 예를 들어, 온보딩 화면이나 인사말 목록을 표시하려면 다음과 같이 합니다.
// Don't copy yet
@Composable
fun MyApp(modifier: Modifier = Modifier) {
Surface(modifier) {
if (shouldShowOnboarding) { // Where does this come from?
OnboardingScreen()
} else {
Greetings()
}
}
}
하지만, shouldShowOnboarding
에 액세스할 수 없습니다. OnboardingScreen
에서 만든 상태를 MyApp
컴포저블과 공유해야 합니다.
여기서는 상태 값을 상위 요소와 공유하는 대신 상태를 호이스팅합니다. 즉, 상태 값에 액세스해야 하는 공통 상위 요소로 상태 값을 이동하기만 하면 됩니다.
먼저 MyApp
의 콘텐츠를 Greetings
라는 새 컴포저블로 이동합니다. 대신 Greetings
메서드를 호출하도록 미리보기를 조정합니다.
@Composable
fun MyApp(modifier: Modifier = Modifier) {
Greetings()
}
@Composable
private fun Greetings(
modifier: Modifier = Modifier,
names: List<String> = listOf("World", "Compose")
) {
Column(modifier = modifier.padding(vertical = 4.dp)) {
for (name in names) {
Greeting(name = name)
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingsPreview() {
BasicsCodelabTheme {
Greetings()
}
}
새로운 최상위 MyApp 컴포저블의 미리보기를 추가하여 동작을 테스트할 수 있습니다.
@Preview
@Composable
fun MyAppPreview() {
BasicsCodelabTheme {
MyApp(Modifier.fillMaxSize())
}
}
이제 MyApp
에 다른 화면을 표시하는 로직을 추가하고 상태를 호이스팅합니다.
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var shouldShowOnboarding by remember { mutableStateOf(true) }
Surface(modifier) {
if (shouldShowOnboarding) {
OnboardingScreen(/* TODO */)
} else {
Greetings()
}
}
}
또한, shouldShowOnboarding
을 온보딩 화면과 공유해야 하지만, 직접 전달하지는 않습니다. OnboardingScreen
이 상태를 변경하도록 하는 대신 사용자가 Continue 버튼을 클릭했을 때 앱에 알리도록 하는 것이 더 좋습니다.
이벤트는 어떻게 전달할까요? 아래로 콜백을 전달합니다. 콜백은 다른 함수에 인수로 전달되는 함수로 이벤트가 발생하면 실행됩니다.
MyApp
의 상태를 변경할 수 있도록 onContinueClicked: () -> Unit
으로 정의된 온보딩 화면에 함수 매개변수를 추가해 보세요.
해결 방법:
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var shouldShowOnboarding by remember { mutableStateOf(true) }
Surface(modifier) {
if (shouldShowOnboarding) {
OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
} else {
Greetings()
}
}
}
@Composable
fun OnboardingScreen(
onContinueClicked: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Basics Codelab!")
Button(
modifier = Modifier
.padding(vertical = 24.dp),
onClick = onContinueClicked
) {
Text("Continue")
}
}
}
이 방법은 상태가 아닌 함수를 OnboardingScreen
에 전달하는 방식으로 이 컴포저블의 재사용 가능성을 높이고 다른 컴포저블이 상태를 변경하지 않도록 보호하고 있습니다. 일반적으로 이 방식은 작업을 간단하게 유지합니다. 다음은 이제 OnboardingScreen
을 호출하기 위해 온보딩 미리보기를 어떻게 수정해야 하는지 보여주는 좋은 예입니다.
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
BasicsCodelabTheme {
OnboardingScreen(onContinueClicked = {}) // Do nothing on click.
}
}
onContinueClicked
를 빈 람다 표현식에 할당하는 것은 '아무 작업도 하지 않음'을 의미합니다. 이는 미리보기를 위해 완벽합니다.
아래 그림은 실제 앱에 더 가깝습니다. 수고하셨습니다.
MyApp
컴포저블에서 매번 값을 사용하지 않도록 by
속성 위임을 처음으로 사용했습니다. expanded
속성의 Greeting 컴포저블에서도 =
대신 by
를 사용해 보겠습니다. expanded
를 val
에서 var
로 변경해야 합니다.
지금까지의 전체 코드는 다음과 같습니다.
package com.example.basicscodelab
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var shouldShowOnboarding by remember { mutableStateOf(true) }
Surface(modifier) {
if (shouldShowOnboarding) {
OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
} else {
Greetings()
}
}
}
@Composable
fun OnboardingScreen(
onContinueClicked: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Basics Codelab!")
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = onContinueClicked
) {
Text("Continue")
}
}
}
@Composable
private fun Greetings(
modifier: Modifier = Modifier,
names: List<String> = listOf("World", "Compose")
) {
Column(modifier = modifier.padding(vertical = 4.dp)) {
for (name in names) {
Greeting(name = name)
}
}
}
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
BasicsCodelabTheme {
OnboardingScreen(onContinueClicked = {})
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
val extraPadding = if (expanded) 48.dp else 0.dp
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(
modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greetings()
}
}
@Preview
@Composable
fun MyAppPreview() {
BasicsCodelabTheme {
MyApp(Modifier.fillMaxSize())
}
}
9. 성능 지연 목록 만들기
이제 이름을 더 현실적으로 나열해 보겠습니다. 지금까지 Column
에 두 개의 인사말을 표시했습니다. 하지만 수천 개의 인사말을 처리할 수 있을까요?
목록 크기를 설정하고 람다에 포함된 값으로 목록을 채우도록 허용하는 다른 목록 생성자를 사용하기 위해 Greetings
매개변수의 기본 목록 값을 변경합니다(여기서 $it
은 목록 색인을 나타냄).
names: List<String> = List(1000) { "$it" }
이렇게 하면 화면에 맞지 않는 인사말을 포함하여 1,000개의 인사말이 생성됩니다. 이는 분명히 성능 기준에 맞지는 않습니다. 에뮬레이터에서 실행해 볼 수 있습니다(경고: 이 코드로 인해 에뮬레이터가 중단될 수도 있음).
스크롤이 가능한 열을 표시하기 위해 LazyColumn
을 사용합니다. LazyColumn
은 화면에 보이는 항목만 렌더링하므로 항목이 많은 목록을 렌더링할 때 성능이 향상됩니다.
기본적인 사용법으로 LazyColumn
API는 범위 내에서 items
요소를 제공하며, 여기서 로직을 렌더링하는 개별 항목은 다음과 같이 작성됩니다.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
// ...
@Composable
private fun Greetings(
modifier: Modifier = Modifier,
names: List<String> = List(1000) { "$it" }
) {
LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
items(items = names) { name ->
Greeting(name = name)
}
}
}
10. 상태 유지
앱에는 두 가지 문제가 있습니다.
온보딩 화면 상태 유지
기기에서 앱을 실행하고 버튼을 클릭한 다음 회전하면 온보딩 화면이 다시 표시됩니다. remember
함수는 컴포저블이 컴포지션에 유지되는 동안에만 작동합니다. 기기를 회전하면 전체 활동이 다시 시작되므로 모든 상태가 손실됩니다. 이 현상은 구성이 변경되거나 프로세스가 중단될 때도 발생합니다.
remember
를 사용하는 대신 rememberSaveable
을 사용하면 됩니다. 이 함수는 구성 변경(예: 회전)과 프로세스 중단에도 각 상태를 저장합니다.
이제 shouldShowOnboarding
에서 remember
를 rememberSaveable
로 교체하여 사용합니다.
import androidx.compose.runtime.saveable.rememberSaveable
// ...
var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }
실행 또는 회전하거나 어두운 모드로 변경 또는 프로세스를 종료해 봅니다. 이전에 앱을 종료하지 않았다면 온보딩 화면이 표시되지 않습니다.
목록 항목의 펼쳐진 상태 유지
목록 항목을 펼친 다음 항목이 보이지 않을 때까지 목록을 스크롤하거나, 기기를 회전한 다음 펼쳐진 항목으로 돌아가면 이제 항목이 초기 상태로 돌아온 것을 확인할 수 있습니다.
이 문제의 해결 방법은 펼쳐진 상태에도 rememberSaveable을 사용하는 것입니다.
var expanded by rememberSaveable { mutableStateOf(false) }
지금까지 약 120줄의 코드를 이용하여 각자의 상태를 보유하는 여러 항목으로 구성된 긴 스크롤 목록을 성능 기준에 맞게 표시할 수 있었습니다. 또한, 위에서 볼 수 있는 바와 같이 앱은 코드를 추가하지 않고 완벽하게 올바른 어두운 모드를 표시합니다. 테마 설정에 관해서는 나중에 알아보겠습니다.
11. 목록에 애니메이션 적용
Compose에서는 여러 가지 방법으로 UI에 애니메이션을 지정할 수 있습니다. 간단한 애니메이션을 위한 상위 수준의 API에서 전체 제어 및 복잡한 전환을 위한 하위 수준의 메서드까지 다양한 방법이 있습니다. 자세한 내용은 문서를 참고하세요.
이 섹션에서는 하위 수준의 API 중 하나를 사용하지만 걱정하지 않아도 됩니다. 매우 간단할 수도 있습니다. 이미 구현한 크기 변경에 애니메이션을 적용해 보겠습니다.
이를 위해 animateDpAsState
컴포저블을 사용합니다. 이 컴포저블은 애니메이션이 완료될 때까지 애니메이션에 의해 객체의 value
가 계속 업데이트되는 상태 객체를 반환합니다. 유형이 Dp
인 '목표 값'을 사용합니다.
펼쳐진 상태에 따라 달라지는 extraPadding
을 만들고 애니메이션을 적용합니다.
import androidx.compose.animation.core.animateDpAsState
@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
앱을 실행하고 애니메이션을 시도해 보세요.
animateDpAsState
는 애니메이션을 맞춤설정할 수 있는 animationSpec
매개변수를 선택적으로 사용합니다. 스프링 기반의 애니메이션 추가와 같이 더 재미있는 작업을 해보겠습니다.
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
Surface(
// ...
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding.coerceAtLeast(0.dp))
// ...
)
}
참고로, 패딩이 음수가 되지 않도록 해야 합니다. 패딩이 음수가 되면 앱이 다운될 수 있습니다. 이로 인해 미세한 애니메이션 버그가 발생하며 이 버그는 추후 설정 완료에서 수정할 예정입니다.
spring
사양은 시간과 관련된 매개변수를 사용하지 않습니다. 대신, 물리적 속성(감쇠 및 강성)을 사용하여 애니메이션을 더 자연스럽게 만듭니다. 이제 앱을 실행하여 새 애니메이션을 사용해 보세요.
animate*AsState
를 사용하여 만든 애니메이션은 중단될 수 있습니다. 즉, 애니메이션 중간에 목표 값이 변경되면 animate*AsState
는 애니메이션을 다시 시작하고 새 값을 가리킵니다. 특히 스프링 기반 애니메이션에서는 중단이 자연스럽게 보입니다.
다양한 유형의 애니메이션을 살펴보려면 다양한 spring
용 매개변수, 다양한 사양(tween
, repeatable
), 다양한 함수(animateColorAsState
또는 다양한 유형의 Animation API)를 사용해 보세요.
이 섹션의 전체 코드
package com.example.basicscodelab
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }
Surface(modifier) {
if (shouldShowOnboarding) {
OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
} else {
Greetings()
}
}
}
@Composable
fun OnboardingScreen(
onContinueClicked: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Basics Codelab!")
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = onContinueClicked
) {
Text("Continue")
}
}
}
@Composable
private fun Greetings(
modifier: Modifier = Modifier,
names: List<String> = List(1000) { "$it" }
) {
LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
items(items = names) { name ->
Greeting(name = name)
}
}
}
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
BasicsCodelabTheme {
OnboardingScreen(onContinueClicked = {})
}
}
@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding.coerceAtLeast(0.dp))
) {
Text(text = "Hello, ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greetings()
}
}
@Preview
@Composable
fun MyAppPreview() {
BasicsCodelabTheme {
MyApp(Modifier.fillMaxSize())
}
}
12. 앱의 스타일 지정 및 테마 설정
지금까지 컴포저블의 스타일을 지정하지 않았지만 어두운 모드 지원을 비롯한 적절한 기본값을 얻었습니다. 이제 BasicsCodelabTheme
과 MaterialTheme
이 무엇인지 살펴보겠습니다.
ui/theme/Theme.kt
파일을 열면 BasicsCodelabTheme
이 구현에서 MaterialTheme
을 사용하는 것을 확인할 수 있습니다.
// Do not copy
@Composable
fun BasicsCodelabTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
// ...
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
MaterialTheme
은 Material 디자인 사양의 스타일 지정 원칙을 반영한 구성 가능한 함수입니다. 스타일 지정 정보는 content
의 내부에 있는 구성요소로 하향 적용됩니다. 이러한 구성요소는 이 정보를 읽어 자신의 스타일을 지정합니다. UI에서는 이미 다음과 같이 BasicsCodelabTheme
을 사용하고 있습니다.
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
BasicsCodelabTheme
은 MaterialTheme
을 내부적으로 래핑하므로 MyApp
은 테마에 정의된 속성으로 스타일이 지정됩니다. 모든 하위 컴포저블에서 MaterialTheme
의 세 가지 속성, colorScheme
, typography
, shapes
를 가져올 수 있습니다. 이러한 속성을 사용하여 Text
중 하나에 헤더 스타일을 설정하세요.
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding.coerceAtLeast(0.dp))
) {
Text(text = "Hello, ")
Text(text = name, style = MaterialTheme.typography.headlineMedium)
}
위의 예에서 Text
컴포저블은 새 TextStyle
을 설정합니다. 고유한 TextStyle
을 만들 수도 있고 기본인 MaterialTheme.typography
를 사용하여 테마가 정의된 스타일을 가져올 수도 있습니다. 이 구성을 사용하면 Material에 정의된 텍스트 스타일(displayLarge, headlineMedium, titleSmall, bodyLarge, labelMedium
등)에 액세스할 수 있습니다. 이 예제에서는 테마에 정의된 headlineMedium
스타일을 사용합니다.
이제 빌드하여 새롭게 스타일이 지정된 텍스트를 확인합니다.
일반적으로 MaterialTheme
내부의 색상, 모양, 글꼴 스타일을 유지하는 것이 훨씬 좋습니다. 예를 들어, 어두운 모드는 색상을 하드 코딩하는 경우 구현하기 어렵고 오류가 발생하기 쉬운 작업이 많이 요구됩니다.
그러나 가끔 색상과 글꼴 스타일의 선택에서 약간 벗어나야 할 때도 있습니다. 이러한 상황에서는 기존에 사용하고 있는 색상이나 스타일을 기반으로 하는 것이 좋습니다.
이를 위해 copy
함수를 사용하여 미리 정의된 스타일을 수정할 수 있습니다. 다음과 같이 숫자를 더 굵게 만들어 보세요.
import androidx.compose.ui.text.font.FontWeight
// ...
Text(
text = name,
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.ExtraBold
)
)
이런 방법으로 글꼴 모음이나 다른 headlineMedium
속성을 변경해야 하면 작은 편차는 걱정하지 않아도 됩니다.
이제 미리보기 창에 다음과 같은 결과가 표시됩니다.
어두운 모드 미리보기 설정
현재 미리보기에는 밝은 모드에서의 앱 모양만 표시됩니다. GreetingPreview
에 UI_MODE_NIGHT_YES
와 함께 @Preview
주석을 추가합니다.
import android.content.res.Configuration.UI_MODE_NIGHT_YES
@Preview(
showBackground = true,
widthDp = 320,
uiMode = UI_MODE_NIGHT_YES,
name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greetings()
}
}
이렇게 하면 어두운 모드의 미리보기가 추가됩니다.
앱 테마 조정
ui/theme
폴더에 있는 파일에서 현재 테마와 관련된 모든 항목을 찾을 수 있습니다. 예를 들어, 지금까지 사용한 기본 색상은 Color.kt
에 정의되어 있습니다.
새로운 색상을 정의하는 것부터 시작해 보겠습니다. Color.kt
에 다음을 추가합니다.
val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(0xFFEFF7CF)
이제 Theme.kt
에서 MaterialTheme
팔레트에 색상을 할당합니다.
private val LightColorScheme = lightColorScheme(
surface = Blue,
onSurface = Color.White,
primary = LightBlue,
onPrimary = Navy
)
MainActivity.kt
로 돌아가서 미리보기를 새로고침해도 미리보기 색상은 실제로 변경되지 않습니다. 미리보기에서 기본적으로 동적 색상이 사용되기 때문입니다. dynamicColor
불리언 매개변수를 사용하여 Theme.kt
에서 동적 색상을 추가하는 로직을 확인할 수 있습니다.
색 구성표의 비 적응형 버전을 보려면 API 수준이 31(적응형 색이 도입된 Android S에 해당)보다 낮은 기기에서 앱을 실행하세요. 다음과 같은 새로운 색상이 표시됩니다.
Theme.kt
에서 어두운 색상을 위한 팔레트를 정의합니다.
private val DarkColorScheme = darkColorScheme(
surface = Blue,
onSurface = Navy,
primary = Navy,
onPrimary = Chartreuse
)
이제 앱을 실행하면 어두운 색상이 작동하는 것을 확인할 수 있습니다.
Theme.kt
의 최종 코드
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
private val DarkColorScheme = darkColorScheme(
surface = Blue,
onSurface = Navy,
primary = Navy,
onPrimary = Chartreuse
)
private val LightColorScheme = lightColorScheme(
surface = Blue,
onSurface = Color.White,
primary = LightBlue,
onPrimary = Navy
)
@Composable
fun BasicsCodelabTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
13. 설정 완료
이 단계에서는 이미 알고 있는 내용을 적용하고 몇 가지 힌트만 사용하여 새로운 개념을 학습합니다. 다음과 같이 만들어 보겠습니다.
버튼을 아이콘으로 대체
- 하위 요소인
Icon
과 함께IconButton
컴포저블을 사용합니다. Icons.Filled.ExpandLess
와Icons.Filled.ExpandMore
를 사용합니다. 이는material-icons-extended
아티팩트에서 사용할 수 있습니다.app/build.gradle.kts
파일의 종속 항목에 다음 줄을 추가합니다.
implementation("androidx.compose.material:material-icons-extended")
- 정렬을 고정하기 위해 패딩을 수정합니다.
- 접근성을 위해 콘텐츠 설명을 추가합니다(아래 '문자열 리소스 사용' 참고).
문자열 리소스 사용
'더보기' 및 '간략히 보기'를 위한 콘텐츠 설명이 있어야 하며 간단한 if
문을 사용하여 설명을 추가할 수 있습니다.
contentDescription = if (expanded) "Show less" else "Show more"
하지만 문자열을 하드 코딩하는 것은 바람직하지 않으며 strings.xml
파일에서 이러한 문자열을 가져와야 합니다.
이를 자동으로 하려면 Android 스튜디오의 'Context Actions'에서 사용할 수 있는 'Extract string resource'를 각 문자열에 사용하면 됩니다.
또는 app/src/res/values/strings.xml
을 열고 다음 리소스를 추가합니다.
<string name="show_less">Show less</string>
<string name="show_more">Show more</string>
더보기
각 카드의 크기 변경이 트리거되면 'Composem ipsum' 텍스트가 표시되고 사라집니다.
- 항목을 펼칠 때 표시되는
Greeting
내부의 Column에 새로운Text
를 추가합니다. extraPadding
을 삭제하고 대신animateContentSize
수정자를Row
에 적용합니다. 그러면 애니메이션 생성 프로세스가 자동화되고 이를 수동으로 실행하기가 어려워집니다. 또한coerceAtLeast
가 더 이상 필요하지 않습니다.
고도 및 도형 추가
shadow
수정자를clip
수정자와 함께 사용하여 카드 스타일을 만들 수 있습니다. 그러나 이를 정확하게 실행하는 Material 컴포저블(Card
)이 있습니다.CardDefaults.cardColors
를 호출하고 변경하려는 색상을 재정의하여Card
의 색상을 변경할 수 있습니다.
최종 코드
package com.example.basicscodelab
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }
Surface(modifier, color = MaterialTheme.colorScheme.background) {
if (shouldShowOnboarding) {
OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
} else {
Greetings()
}
}
}
@Composable
fun OnboardingScreen(
onContinueClicked: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Basics Codelab!")
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = onContinueClicked
) {
Text("Continue")
}
}
}
@Composable
private fun Greetings(
modifier: Modifier = Modifier,
names: List<String> = List(1000) { "$it" }
) {
LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
items(items = names) { name ->
Greeting(name = name)
}
}
}
@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primary
),
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
CardContent(name)
}
}
@Composable
private fun CardContent(name: String) {
var expanded by rememberSaveable { mutableStateOf(false) }
Row(
modifier = Modifier
.padding(12.dp)
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
) {
Column(
modifier = Modifier
.weight(1f)
.padding(12.dp)
) {
Text(text = "Hello, ")
Text(
text = name, style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.ExtraBold
)
)
if (expanded) {
Text(
text = ("Composem ipsum color sit lazy, " +
"padding theme elit, sed do bouncy. ").repeat(4),
)
}
}
IconButton(onClick = { expanded = !expanded }) {
Icon(
imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
contentDescription = if (expanded) {
stringResource(R.string.show_less)
} else {
stringResource(R.string.show_more)
}
)
}
}
}
@Preview(
showBackground = true,
widthDp = 320,
uiMode = UI_MODE_NIGHT_YES,
name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greetings()
}
}
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
BasicsCodelabTheme {
OnboardingScreen(onContinueClicked = {})
}
}
@Preview
@Composable
fun MyAppPreview() {
BasicsCodelabTheme {
MyApp(Modifier.fillMaxSize())
}
}
14. 축하합니다
축하합니다. Compose의 기본사항을 살펴보았습니다.
Codelab 솔루션
GitHub에서 이 Codelab의 솔루션 코드를 다운로드할 수 있습니다.
$ git clone https://github.com/android/codelab-android-compose
또는 저장소를 ZIP 파일로 다운로드할 수도 있습니다.
다음 단계
Compose 개발자 과정에서 다른 Codelab을 확인하세요.
추가 자료
- 코드와 함께 알아보는 Jetpack Compose 기본사항
- Compose 멘탈 모델 가이드
- 샘플 앱으로 Compose의 실제 동작 확인하기