강의 1: 구성 가능한 함수
Jetpack Compose는 구성 가능한 함수를 중심으로 빌드되었습니다. 이러한 함수를 사용하면 UI의 구성 과정(요소 초기화, 상위 요소에 연결 등)에 집중하기보다는 앱 모양을 설명하고 데이터 종속 항목을 제공하여 프로그래매틱 방식으로 앱의 UI를 정의할 수 있습니다. 구성 가능한 함수를 만들려면 함수 이름에 @Composable
주석을 추가하기만 하면 됩니다.
텍스트 요소 추가
시작하려면 최신 버전을 다운로드하세요. Android 스튜디오를 클릭하고 New Project를 선택하여 Phone and Tablet 카테고리에서 Empty Activity를 선택합니다. 앱 이름을 ComposeTutorial로 지정하고 Finish를 클릭합니다. 기본 템플릿에는 이미 일부 Compose 요소가 포함되어 있지만, 이 튜토리얼에서는 단계별로 빌드해 보겠습니다.
먼저 onCreate
메서드 내에 텍스트 요소를 추가하여 'Hello world!' 텍스트를 표시합니다. 이렇게 하려면 콘텐츠 블록을 정의하고 구성 가능한 Text
함수를 호출하면 됩니다. setContent
블록은 구성 가능한 함수가 호출되는 활동의 레이아웃을 정의합니다. 구성 가능한 함수는 다른 구성 가능한 함수에서만 호출할 수 있습니다.
Jetpack Compose는 Kotlin 컴파일러 플러그인을 사용하여 구성 가능한 함수를 앱의 UI 요소로 변환합니다. 예를 들어, Compose UI 라이브러리에서 정의한 구성 가능한 Text
함수는 텍스트 라벨을 화면에 표시합니다.
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-02.png?authuser=0000&hl=ko)
구성 가능한 함수 정의
함수를 구성 가능하게 하려면 @Composable
주석을 추가해야 합니다.
이렇게 하려면 이름을 전달받는 MessageCard
함수를 정의하고 이를 사용하여 텍스트 요소를 구성합니다.
// ... import androidx.compose.runtime.Composable class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard("Android") } } } @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-03.png?authuser=0000&hl=ko)
Android 스튜디오에서 함수 미리보기
@Preview
주석을 사용하면 앱을 빌드해서 Android 기기나 에뮬레이터에 설치하지 않고 Android 스튜디오 내에서 구성 가능한 함수를 미리 볼 수 있습니다. 이 주석은 매개변수를 사용하지 않는 구성 가능한 함수에 사용해야 합니다. 이러한 이유로 MessageCard
함수는 직접 미리 볼 수 없습니다. 대신 적절한 매개변수와 함께 MessageCard
를 호출하는 PreviewMessageCard
라는 두 번째 함수를 만듭니다. @Composable
앞에 @Preview
주석을 추가합니다.
// ... import androidx.compose.ui.tooling.preview.Preview @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") } @Preview @Composable fun PreviewMessageCard() { MessageCard("Android") }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-03.png?authuser=0000&hl=ko)
프로젝트를 다시 빌드합니다. 어디에서도 새 함수 PreviewMessageCard
를 호출하지 않으므로 앱 자체는 변경되지 않지만, Android 스튜디오는 분할(디자인/코드) 보기에서 클릭하여 확장할 수 있는 미리보기 창을 추가합니다. 이 창은 @Preview
주석이 달린 구성 가능한 함수에서 생성한 UI 요소의 미리보기를 표시합니다. 언제든지 미리보기를 업데이트하려면 미리보기 창 상단의 새로고침 버튼을 클릭하면 됩니다.
![Android 스튜디오에서 구성 가능한 함수 미리보기](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-04.png?authuser=0000&hl=ko)
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-02.png?authuser=0000&hl=ko)
// ... import androidx.compose.runtime.Composable class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard("Android") } } } @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-03.png?authuser=0000&hl=ko)
// ... import androidx.compose.ui.tooling.preview.Preview @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") } @Preview @Composable fun PreviewMessageCard() { MessageCard("Android") }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-03.png?authuser=0000&hl=ko)
![Android 스튜디오에서 구성 가능한 함수 미리보기](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-04.png?authuser=0000&hl=ko)
강의 2: 레이아웃
UI 요소는 계층적이며 다른 요소에 포함된 요소가 있습니다. Compose에서는 다른 구성 가능한 함수로부터 구성 가능한 함수를 호출하여 UI 계층 구조를 빌드합니다.
여러 텍스트 추가
지금까지 첫 번째 구성 가능한 함수와 미리보기를 빌드했습니다. 더 많은 Jetpack Compose 기능을 살펴보기 위해 일부 애니메이션으로 확장할 수 있는 메시지 목록이 포함된 간단한 메시지 화면을 빌드해 보겠습니다.
먼저 메시지 작성자의 이름과 메시지 내용을 표시하여 메시지 컴포저블을 더 복잡하게 만듭니다. 우선 컴포저블 매개변수를 변경하여 String
대신 Message
객체를 허용하도록 하고 MessageCard
컴포저블 내에 다른 Text
컴포저블을 추가해야 합니다. 미리보기도 업데이트해야 합니다.
// ... class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard(Message("Android", "Jetpack Compose")) } } } data class Message(val author: String, val body: String) @Composable fun MessageCard(msg: Message) { Text(text = msg.author) Text(text = msg.body) } @Preview @Composable fun PreviewMessageCard() { MessageCard( msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-02.png?authuser=0000&hl=ko)
이 코드는 콘텐츠 뷰 내에 텍스트 요소 두 개를 만듭니다. 그러나 정렬 방법에 관한 정보가 제공되지 않았으므로 텍스트 요소가 서로 위에 겹치게 표시되어 텍스트를 읽을 수 없습니다.
이미지 요소 추가
발신자의 프로필 사진을 추가하여 메시지 카드를 보완합니다. Resource Manager를 사용하여 사진 라이브러리에서 이미지를 가져오거나 이 이미지를 사용합니다. 디자인 구조가 잘 구성된 Row
컴포저블을 추가하고 그 내부에 Image
컴포저블을 추가합니다.
// ... import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Row import androidx.compose.ui.res.painterResource @Composable fun MessageCard(msg: Message) { Row { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", ) Column { Text(text = msg.author) Text(text = msg.body) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-05.png?authuser=0000&hl=ko)
레이아웃 구성
메시지 레이아웃의 구조는 올바르지만 요소의 간격이 균등하지 않고 이미지가 너무 큽니다. 컴포저블을 장식하거나 구성하기 위해 Compose는 수정자를 사용합니다. 이를 통해 컴포저블의 크기, 레이아웃, 모양을 변경하거나 요소를 클릭 가능하게 만드는 등의 상위 수준 상호작용을 추가할 수 있습니다. 또한, 이러한 수정자를 연결하여 더 복잡한 컴포저블을 만들 수 있습니다. 이 중 일부를 사용하여 레이아웃을 개선해 보겠습니다.
// ... import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @Composable fun MessageCard(msg: Message) { // Add padding around our message Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", modifier = Modifier // Set image size to 40 dp .size(40.dp) // Clip image to be shaped as a circle .clip(CircleShape) ) // Add a horizontal space between the image and the column Spacer(modifier = Modifier.width(8.dp)) Column { Text(text = msg.author) // Add a vertical space between the author and message texts Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-06.png?authuser=0000&hl=ko)
// ... class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard(Message("Android", "Jetpack Compose")) } } } data class Message(val author: String, val body: String) @Composable fun MessageCard(msg: Message) { Text(text = msg.author) Text(text = msg.body) } @Preview @Composable fun PreviewMessageCard() { MessageCard( msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-02.png?authuser=0000&hl=ko)
![두 개의 겹치는 Text 컴포저블 미리보기](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-03.png?authuser=0000&hl=ko)
// ... import androidx.compose.foundation.layout.Column @Composable fun MessageCard(msg: Message) { Column { Text(text = msg.author) Text(text = msg.body) } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-04.png?authuser=0000&hl=ko)
// ... import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Row import androidx.compose.ui.res.painterResource @Composable fun MessageCard(msg: Message) { Row { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", ) Column { Text(text = msg.author) Text(text = msg.body) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-05.png?authuser=0000&hl=ko)
// ... import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @Composable fun MessageCard(msg: Message) { // Add padding around our message Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", modifier = Modifier // Set image size to 40 dp .size(40.dp) // Clip image to be shaped as a circle .clip(CircleShape) ) // Add a horizontal space between the image and the column Spacer(modifier = Modifier.width(8.dp)) Column { Text(text = msg.author) // Add a vertical space between the author and message texts Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-06.png?authuser=0000&hl=ko)
강의 3: Material Design
Compose는 Material Design 원칙을 지원하도록 빌드되었습니다. Compose의 많은 UI 요소가 Material Design을 즉시 사용 가능하도록 구현합니다. 이 강의에서는 Material Design 위젯으로 앱의 스타일을 지정합니다.
Material Design 사용
이제 메시지 디자인에 레이아웃이 생겼지만 아직 디자인은 그렇게 좋아 보이지 않습니다.
Jetpack Compose는 Material Design 3 및 UI 요소를 즉시 사용 가능하도록 구현합니다. Material Design 스타일 지정을 사용하여 MessageCard
컴포저블의 디자인을 개선해 보겠습니다.
먼저 프로젝트에서 만든 Material 테마 ComposeTutorialTheme
과 Surface
로 MessageCard
함수를 래핑합니다.
@Preview
함수와 setContent
함수에서도 이 작업을 실행합니다. 이렇게 하면 컴포저블이 앱 테마에 정의된 스타일을 상속하여 앱 전체에서 일관성이 보장됩니다.
Material Design은 Color
, Typography
, Shape
의 세 가지 핵심 요소를 중심으로 이루어집니다.
하나씩 추가해 보겠습니다.
참고: Empty Compose Activity 템플릿은 MaterialTheme
을 맞춤설정할 수 있는 프로젝트의 기본 테마를 생성합니다.
프로젝트 이름을 ComposeTutorial이 아닌 다른 이름으로 지정했다면 ui.theme
하위 패키지의 Theme.kt
파일에서 맞춤 테마를 찾을 수 있습니다.
// ... class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Surface(modifier = Modifier.fillMaxSize()) { MessageCard(Message("Android", "Jetpack Compose")) } } } } } @Preview @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-02.png?authuser=0000&hl=ko)
색상
MaterialTheme.colorScheme
를 사용하여 래핑된 테마의 색상으로 스타일을 지정할 수 있습니다. 색상이 필요한 곳에 테마의 이러한 값을 사용하면 됩니다. 이 예에서는 기기 환경설정에서 정의된 동적 테마 설정 색상을 사용합니다.
MaterialTheme.kt
파일에서 dynamicColor
를 false
로 설정하여 이를 변경할 수 있습니다.
제목 스타일을 지정하고 이미지에 테두리를 추가합니다.
// ... import androidx.compose.foundation.border import androidx.compose.material3.MaterialTheme @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary ) Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-03.png?authuser=0000&hl=ko)
서체
Material 서체 스타일은 MaterialTheme
에서 사용할 수 있으며, Text
컴포저블에 추가하기만 하면 됩니다.
// ... @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Text( text = msg.body, style = MaterialTheme.typography.bodyMedium ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-04.png?authuser=0000&hl=ko)
도형
Shape
을 사용하여 최종 터치를 추가할 수 있습니다. 먼저 Surface
컴포저블을 중심으로 메시지 본문 텍스트를 래핑합니다. 이렇게 하면 메시지 본문의 도형과 높이를 맞춤설정할 수 있습니다. 더 나은 레이아웃을 위해 메시지에 패딩도 추가합니다.
// ... import androidx.compose.material3.Surface @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), style = MaterialTheme.typography.bodyMedium ) } } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-05.png?authuser=0000&hl=ko)
어두운 테마 사용
특히 야간에 밝은 디스플레이를 사용하지 않거나 기기 배터리를 절약하기 위해 어두운 테마(또는 야간 모드)를 사용 설정할 수 있습니다. Material Design 지원 덕분에 Jetpack Compose는 기본적으로 어두운 테마를 처리할 수 있습니다. Material Design 색상, 텍스트, 배경을 사용하면 어두운 배경에 맞춰 자동으로 조정됩니다.
파일에서 별도의 함수로 여러 미리보기를 만들거나 동일한 함수에 여러 주석을 추가할 수 있습니다.
새 미리보기 주석을 추가하고 야간 모드를 사용 설정합니다.
// ... import android.content.res.Configuration @Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-06.png?authuser=0000&hl=ko)
밝은 테마와 어두운 테마의 색상 선택은 IDE로 생성된 Theme.kt
파일에 정의되어 있습니다.
지금까지 하나의 이미지와 스타일이 다른 두 가지 텍스트를 표시하고 밝은 테마와 어두운 테마 모두에서 보기 좋은 메시지 UI 요소를 만들었습니다.
// ... import android.content.res.Configuration @Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-06.png?authuser=0000&hl=ko)
// ... class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Surface(modifier = Modifier.fillMaxSize()) { MessageCard(Message("Android", "Jetpack Compose")) } } } } } @Preview @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-02.png?authuser=0000&hl=ko)
// ... import androidx.compose.foundation.border import androidx.compose.material3.MaterialTheme @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary ) Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-03.png?authuser=0000&hl=ko)
// ... @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Text( text = msg.body, style = MaterialTheme.typography.bodyMedium ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-04.png?authuser=0000&hl=ko)
// ... import androidx.compose.material3.Surface @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), style = MaterialTheme.typography.bodyMedium ) } } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-05.png?authuser=0000&hl=ko)
// ... import android.content.res.Configuration @Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-06.png?authuser=0000&hl=ko)
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-07.png?authuser=0000&hl=ko)
강의 4: 목록 및 애니메이션
목록 및 애니메이션은 앱의 모든 곳에 있습니다. 이 강의에서는 Compose를 사용하여 손쉽게 목록을 만들고 애니메이션으로 재미를 더하는 방법을 배웁니다.
메시지 목록 만들기
메시지 하나로 채팅하면 조금 외롭게 느껴질 수 있으니 메시지를 두 개 이상 포함하도록 대화를 변경해 보겠습니다. 여러 메시지를 표시하는 Conversation
함수를 만들어야 합니다. 이 사용 사례에서는 Compose의 LazyColumn
과 LazyRow
를 사용합니다. 이러한 컴포저블은 화면에 표시되는 요소만 렌더링하므로 긴 목록에 매우 효율적으로 설계되었습니다.
이 코드 스니펫에서는 LazyColumn
에 items
하위 요소가 있음을 알 수 있습니다. 이 하위 요소는 List
를 매개변수로 가져오고 람다는 Message
의 인스턴스인 message
라는 매개변수(원하는 대로 이름을 지정할 수 있음)를 수신합니다.
간단히 말해, 이 람다는 제공된 List
의 항목마다 호출됩니다. 샘플 데이터 세트를 프로젝트에 복사하여 대화를 빠르게 부트스트랩할 수 있습니다.
// ... import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @Composable fun Conversation(messages: List<Message>) { LazyColumn { items(messages) { message -> MessageCard(message) } } } @Preview @Composable fun PreviewConversation() { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson4-02.png?authuser=0000&hl=ko)
확장 중 메시지에 애니메이션 적용
대화가 점점 흥미로워지고 있습니다. 이제 애니메이션을 사용해 볼 차례입니다. 메시지를 확장하여 더 길게 보여주고 콘텐츠 크기와 배경 색상 모두에 애니메이션 효과를 적용하는 기능을 추가해 보겠습니다. 이 로컬 UI 상태를 저장하려면 메시지가 확장되었는지 추적해야 합니다. 이 상태 변경을 추적하려면 remember
와 mutableStateOf
함수를 사용해야 합니다.
구성 가능한 함수는 remember
를 사용하여 메모리에 로컬 상태를 저장하고 mutableStateOf
에 전달된 값의 변경사항을 추적할 수 있습니다. 이 상태를 사용하는 컴포저블 및 하위 요소는 값이 업데이트되면 자동으로 다시 그려집니다. 이를 재구성이라고 합니다.
remember
및 mutableStateOf
와 같은 Compose의 상태 API를 사용하여 상태를 변경하면 UI가 자동으로 업데이트됩니다.
참고: Kotlin의
위임된 속성 구문 (by
키워드) Alt+Enter 또는 Option+Enter를 누르면
있습니다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
// ... import androidx.compose.foundation.clickable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } } } } @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) // We keep track if the message is expanded or not in this // variable var isExpanded by remember { mutableStateOf(false) } // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp, ) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), // If the message is expanded, we display all its content // otherwise we only display the first line maxLines = if (isExpanded) Int.MAX_VALUE else 1, style = MaterialTheme.typography.bodyMedium ) } } } }
이제 메시지를 클릭하면 isExpanded
에 따라 메시지 콘텐츠의 배경을 변경할 수 있습니다. clickable
수정자를 사용하여 컴포저블의 클릭 이벤트를 처리합니다. 단순히 Surface
의 배경색을 전환하는 대신 MaterialTheme.colorScheme.surface
에서 MaterialTheme.colorScheme.primary
로 또는 그 반대로 값을 점진적으로 수정하여 배경색에 애니메이션을 적용할 수 있습니다. 이를 위해 animateColorAsState
함수를 사용합니다. 마지막으로 animateContentSize
수정자를 사용하여 메시지 컨테이너 크기에 부드럽게 애니메이션을 적용합니다.
// ... import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) // We keep track if the message is expanded or not in this // variable var isExpanded by remember { mutableStateOf(false) } // surfaceColor will be updated gradually from one color to the other val surfaceColor by animateColorAsState( if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface, ) // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp, // surfaceColor color will be changing gradually from primary to surface color = surfaceColor, // animateContentSize will change the Surface size gradually modifier = Modifier.animateContentSize().padding(1.dp) ) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), // If the message is expanded, we display all its content // otherwise we only display the first line maxLines = if (isExpanded) Int.MAX_VALUE else 1, style = MaterialTheme.typography.bodyMedium ) } } } }
// ... import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @Composable fun Conversation(messages: List<Message>) { LazyColumn { items(messages) { message -> MessageCard(message) } } } @Preview @Composable fun PreviewConversation() { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson4-02.png?authuser=0000&hl=ko)
// ... import androidx.compose.foundation.clickable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } } } } @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) // We keep track if the message is expanded or not in this // variable var isExpanded by remember { mutableStateOf(false) } // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp, ) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), // If the message is expanded, we display all its content // otherwise we only display the first line maxLines = if (isExpanded) Int.MAX_VALUE else 1, style = MaterialTheme.typography.bodyMedium ) } } } }
// ... import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) // We keep track if the message is expanded or not in this // variable var isExpanded by remember { mutableStateOf(false) } // surfaceColor will be updated gradually from one color to the other val surfaceColor by animateColorAsState( if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface, ) // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp, // surfaceColor color will be changing gradually from primary to surface color = surfaceColor, // animateContentSize will change the Surface size gradually modifier = Modifier.animateContentSize().padding(1.dp) ) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), // If the message is expanded, we display all its content // otherwise we only display the first line maxLines = if (isExpanded) Int.MAX_VALUE else 1, style = MaterialTheme.typography.bodyMedium ) } } } }
다음 단계
축하합니다. Compose 튜토리얼을 완료했습니다. 이미지와 텍스트가 포함된 확장형 및 애니메이션 메시지의 목록을 효율적으로 표시하는 간단한 채팅 화면을 빌드했습니다. 이 화면은 Material Design 원칙으로 디자인했으며 어두운 테마와 미리보기가 포함되어 있고 사용된 코드가 100줄도 되지 않습니다.
지금까지 배운 내용은 다음과 같습니다.
- 구성 가능한 함수 정의
- 컴포저블에 다른 요소 추가
- 레이아웃 컴포저블을 사용하여 UI 구성요소 구조화
- 수정자를 사용한 컴포저블 확장
- 효율적인 목록 만들기
- 상태 추적 유지 및 수정
- 컴포저블에 사용자 상호작용 추가
- 메시지 확장 중 메시지에 애니메이션 적용
이 단계 중 일부에 대해 자세히 알아보려면 아래 리소스를 살펴보세요.