튜토리얼

Jetpack Compose 튜토리얼

Jetpack Compose는 네이티브 Android UI를 빌드하기 위한 최신 도구 키트입니다. Jetpack Compose는 더 적은 수의 코드, 강력한 도구, 직관적인 Kotlin API로 Android에서의 UI 개발을 간소화하고 가속화합니다.

이 튜토리얼에서는 선언형 함수를 사용하여 간단한 UI 구성요소를 빌드합니다. XML 레이아웃을 수정하거나 Layout Editor를 사용하지 않습니다. 대신 Jetpack Compose 함수를 호출하여 원하는 요소를 말하면 Compose 컴파일러에서 나머지 작업을 완료합니다.

전체 미리보기
전체 미리보기

강의 1: 구성 가능한 함수

Jetpack Compose는 구성 가능한 함수를 중심으로 빌드되었습니다. 이러한 함수를 사용하면 UI의 구성 과정(요소 초기화, 상위 요소에 연결 등)에 집중하기보다는 앱 모양을 설명하고 데이터 종속 항목을 제공하여 프로그래매틱 방식으로 앱의 UI를 정의할 수 있습니다. 구성 가능한 함수를 만들려면 함수 이름에 @Composable 주석을 추가하기만 하면 됩니다.

텍스트 요소 추가

시작하려면 Android 스튜디오 Arctic Fox의 최신 버전을 다운로드하고 빈 Compose Activity 템플릿을 사용하여 앱을 만듭니다. 기본 템플릿에는 이미 일부 Compose 요소가 포함되어 있지만 단계별로 빌드해 보겠습니다.

먼저 onCreate 코드 > 메서드에서 내부에 텍스트 요소를 추가하면 'Hello world!'가 표시됩니다. 이렇게 하려면 콘텐츠 블록을 정의하고 Text() 함수를 호출합니다. setContent 블록은 구성 가능한 함수를 호출하는 활동의 레이아웃을 정의합니다. 구성 가능한 함수는 다른 구성 가능한 함수에서만 호출할 수 있습니다.

Jetpack Compose는 Kotlin 컴파일러 플러그인을 사용하여 구성 가능한 함수를 앱의 UI 요소로 변환합니다. 예를 들어 Compose UI 라이브러리에서 정의한 Text() 함수는 화면에 텍스트 라벨을 표시합니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello world!")
        }
    }
}
  
미리보기 표시
미리보기 숨기기

구성 가능한 함수 정의

함수를 구성 가능하게 하려면 @Composable 주석을 추가해야 합니다. 이렇게 하려면 이름을 전달받는 MessageCard() 함수를 정의하고 이를 사용하여 텍스트 요소를 구성합니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MessageCard("Android")
        }
    }
}

@Composable
fun MessageCard(name: String) {
    Text(text = "Hello $name!")
}

  
미리보기 표시
미리보기 숨기기

Android 스튜디오에서 함수 미리보기

Android 스튜디오를 사용하면 Android 기기 또는 에뮬레이터에 앱을 설치하는 대신 IDE 내에서 구성 가능한 함수를 미리 볼 수 있습니다. 구성 가능한 함수는 모든 매개변수의 기본값을 제공해야 합니다. 이러한 이유로 MessageCard() 함수를 직접 미리 볼 수는 없습니다. 대신 적절한 매개변수를 사용하여 MessageCard()를 호출하는 PreviewMessageCard()라는 두 번째 함수를 만들어 보겠습니다. @Composable 앞에 @Preview 주석을 추가합니다.

@Composable
fun MessageCard(name: String) {
    Text(text = "Hello $name!")
}

@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard("Android")
}
  
미리보기 표시
미리보기 숨기기

프로젝트를 다시 빌드합니다. 새로운 PreviewMessageCard() 함수가 어디에서도 호출되지 않기 때문에 앱 자체는 변경되지 않지만 Android 스튜디오는 미리보기 창을 추가합니다. 이 창은 @Preview 주석으로 표시된 구성 가능한 함수에 의해 생성된 UI 요소의 미리보기를 보여줍니다. 언제든지 미리보기를 업데이트하려면 미리보기 창 상단의 새로고침 버튼을 클릭합니다.

그림 1. Android 스튜디오를 사용하여 구성 가능한 함수 미리보기
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello world!")
        }
    }
}
  
미리보기 표시
미리보기 숨기기
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MessageCard("Android")
        }
    }
}

@Composable
fun MessageCard(name: String) {
    Text(text = "Hello $name!")
}

  
미리보기 표시
미리보기 숨기기
@Composable
fun MessageCard(name: String) {
    Text(text = "Hello $name!")
}

@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard("Android")
}
  
미리보기 표시
미리보기 숨기기
그림 1. Android 스튜디오를 사용하여 구성 가능한 함수 미리보기

강의 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("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
    )
}

  
미리보기 표시
미리보기 숨기기

이 코드는 콘텐츠 뷰 내에 텍스트 요소 두 개를 만듭니다. 그러나 정렬 방법에 관한 정보가 제공되지 않았으므로 텍스트 요소가 서로 위에 겹치게 표시되어 텍스트를 읽을 수 없습니다.

열 사용

Column 함수를 사용하면 요소를 수직으로 정렬할 수 있습니다. MessageCard() 함수에 Column을 추가합니다.
행을 사용하여 항목을 가로로 정렬하고 Box를 사용하여 요소를 쌓을 수 있습니다.

@Composable
fun MessageCard(msg: Message) {
    Column {
        Text(text = msg.author)
        Text(text = msg.body)
    }
}

미리보기 표시
미리보기 숨기기

이미지 요소 추가

발신자의 프로필 사진을 추가하여 Google 메시지 카드를 보완해 보겠습니다. Resource Manager를 사용하여 사진 라이브러리에서 이미지를 가져오거나 이 이미지를 사용합니다. 구조화가 잘 된 디자인과 내부에 Image 컴포저블을 갖추기 위해 Row 컴포저블을 추가합니다.

@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)
        }
  
    }
  
}
  
미리보기 표시
미리보기 숨기기

레이아웃 구성

메시지 레이아웃의 구조가 올바르지만 요소의 간격이 균등하지 않고 이미지가 너무 큽니다. 컴포저블을 장식하거나 구성하기 위해 Compose는 수정자를 사용합니다. 이를 통해 컴포저블의 크기, 레이아웃, 모양을 변경하거나 요소를 클릭 가능하게 만드는 등의 상위 수준 상호작용을 추가할 수 있습니다. 연결하여 보다 복잡한 컴포저블을 만들 수 있습니다. 그중 일부를 사용하여 레이아웃을 개선해 보겠습니다.

@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)
        }
    }
}
  
미리보기 표시
미리보기 숨기기
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("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
    )
}

  
미리보기 표시
미리보기 숨기기
@Composable
fun MessageCard(msg: Message) {
    Column {
        Text(text = msg.author)
        Text(text = msg.body)
    }
}

미리보기 표시
미리보기 숨기기
@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)
        }
  
    }
  
}
  
미리보기 표시
미리보기 숨기기
@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)
        }
    }
}
  
미리보기 표시
미리보기 숨기기

강의 3: 머티리얼 디자인

Compose는 머티리얼 디자인 원칙을 지원하도록 빌드되었습니다. 많은 UI 요소가 머티리얼 디자인을 즉시 사용 가능하도록 구현합니다. 이 강의에서는 머티리얼 위젯으로 앱의 스타일을 지정합니다.

머티리얼 디자인 사용

이제 메시지 디자인에 레이아웃이 생겼지만 아직 디자인은 그렇게 좋지는 않습니다.

Jetpack Compose는 머티리얼 디자인 및 UI 요소를 즉시 사용 가능하도록 구현합니다. 머티리얼 디자인 스타일링을 사용하여 MessageCard 컴포저블의 디자인을 개선합니다.

먼저 프로젝트(이 경우 ComposeTutorialTheme)에서 생성한 머티리얼 테마로 MessageCard 함수를 래핑합니다. @Preview 및 setContent 함수 모두에서 이 작업을 실행합니다.

머티리얼 디자인은 색상, 서체, 도형의 세 가지 핵심 요소를 중심으로 이루어집니다. 하나씩 추가해 보겠습니다.

참고: Empty Compose Activity는 MaterialTheme를 맞춤설정할 수 있는 프로젝트의 기본 테마를 생성합니다. 프로젝트의 이름을 ComposeTutorial과 다르게 지정한 경우 ui.theme 패키지에서 맞춤 테마를 찾을 수 있습니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTutorialTheme {
                MessageCard(Message("Android", "Jetpack Compose"))
            }
        }
    }
}

@Preview
@Composable
fun PreviewMessageCard() {
    ComposeTutorialTheme {
        MessageCard(
            msg = Message("Colleague", "Take a look at Jetpack Compose, it's great!")
        )
    }
}

  
미리보기 표시
미리보기 숨기기

색상

래핑된 테마의 색상으로 간단하게 스타일을 지정할 수 있으며 색상이 필요한 모든 곳에 이 테마의 값을 사용할 수 있습니다.

제목 스타일을 지정하고 이미지에 테두리를 추가해 보겠습니다.

@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.colors.secondary, CircleShape)
       )

       Spacer(modifier = Modifier.width(8.dp))

       Column {
           Text(
               text = msg.author,
               color = MaterialTheme.colors.secondaryVariant
           )

           Spacer(modifier = Modifier.height(4.dp))
           Text(text = msg.body)
       }
   }
}

  
미리보기 표시
미리보기 숨기기

서체

머티리얼 서체 스타일은 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.colors.secondary, CircleShape)
       )
       Spacer(modifier = Modifier.width(8.dp))

       Column {
           Text(
               text = msg.author,
               color = MaterialTheme.colors.secondaryVariant,
               style = MaterialTheme.typography.subtitle2
           )

           Spacer(modifier = Modifier.height(4.dp))

           Text(
               text = msg.body,
               style = MaterialTheme.typography.body2
           )
       }
   }
}

  
미리보기 표시
미리보기 숨기기

도형

도형을 사용하여 최종 터치를 추가할 수 있습니다. 또한 더 나은 레이아웃을 위해 메시지에 패딩을 추가하기도 합니다.

@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.colors.secondary, CircleShape)
       )
       Spacer(modifier = Modifier.width(8.dp))

       Column {
           Text(
               text = msg.author,
               color = MaterialTheme.colors.secondaryVariant,
               style = MaterialTheme.typography.subtitle2
           )

           Spacer(modifier = Modifier.height(4.dp))

           Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) {
               Text(
                   text = msg.body,
                   modifier = Modifier.padding(all = 4.dp),
                   style = MaterialTheme.typography.body2
               )
           }
       }
   }
}

  
미리보기 표시
미리보기 숨기기

어두운 테마 사용

특히 야간에 밝은 디스플레이를 사용하지 않거나 기기 배터리를 절약하기 위해 어두운 테마(또는 야간 모드)를 사용 설정할 수 있습니다. 머티리얼 디자인 지원 덕분에 Jetpack Compose는 기본적으로 어두운 테마를 처리할 수 있습니다. 머티리얼 색상, 텍스트, 배경을 사용하면 어두운 배경에 맞춰 자동으로 조정됩니다.

파일에서 별도의 함수로 여러 미리보기를 만들거나 동일한 함수에 여러 주석을 추가할 수 있습니다.

새 미리보기 주석을 추가하고 야간 모드를 사용 설정해 보겠습니다.

@Preview(name = "Light Mode")
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "Dark Mode"
)
@Composable
fun PreviewMessageCard() {
   ComposeTutorialTheme {
       MessageCard(
           msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
       )
   }
}
  
미리보기 표시
미리보기 숨기기

밝은 테마와 어두운 테마의 색상 선택은 IDE로 생성된 Theme.kt 파일에 정의되어 있습니다.

지금까지 스타일이 다른 두 텍스트와 이미지를 표시하고, 밝은 테마와 어두운 테마 모두에서 보기 좋은 메시지 UI 요소를 만들었습니다.

@Preview(name = "Light Mode")
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "Dark Mode"
)
@Composable
fun PreviewMessageCard() {
   ComposeTutorialTheme {
       MessageCard(
           msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
       )
   }
}
  
미리보기 표시
미리보기 숨기기
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTutorialTheme {
                MessageCard(Message("Android", "Jetpack Compose"))
            }
        }
    }
}

@Preview
@Composable
fun PreviewMessageCard() {
    ComposeTutorialTheme {
        MessageCard(
            msg = Message("Colleague", "Take a look at Jetpack Compose, it's great!")
        )
    }
}

  
미리보기 표시
미리보기 숨기기
@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.colors.secondary, CircleShape)
       )

       Spacer(modifier = Modifier.width(8.dp))

       Column {
           Text(
               text = msg.author,
               color = MaterialTheme.colors.secondaryVariant
           )

           Spacer(modifier = Modifier.height(4.dp))
           Text(text = msg.body)
       }
   }
}

  
미리보기 표시
미리보기 숨기기
@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.colors.secondary, CircleShape)
       )
       Spacer(modifier = Modifier.width(8.dp))

       Column {
           Text(
               text = msg.author,
               color = MaterialTheme.colors.secondaryVariant,
               style = MaterialTheme.typography.subtitle2
           )

           Spacer(modifier = Modifier.height(4.dp))

           Text(
               text = msg.body,
               style = MaterialTheme.typography.body2
           )
       }
   }
}

  
미리보기 표시
미리보기 숨기기
@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.colors.secondary, CircleShape)
       )
       Spacer(modifier = Modifier.width(8.dp))

       Column {
           Text(
               text = msg.author,
               color = MaterialTheme.colors.secondaryVariant,
               style = MaterialTheme.typography.subtitle2
           )

           Spacer(modifier = Modifier.height(4.dp))

           Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) {
               Text(
                   text = msg.body,
                   modifier = Modifier.padding(all = 4.dp),
                   style = MaterialTheme.typography.body2
               )
           }
       }
   }
}

  
미리보기 표시
미리보기 숨기기
@Preview(name = "Light Mode")
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "Dark Mode"
)
@Composable
fun PreviewMessageCard() {
   ComposeTutorialTheme {
       MessageCard(
           msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
       )
   }
}
  
미리보기 표시
미리보기 숨기기

강의 4: 목록 및 애니메이션

목록 및 애니메이션은 앱의 모든 곳에 있습니다. 이 강의에서는 Compose를 사용하여 손쉽게 목록을 만들고 애니메이션으로 재미를 더하는 방법을 배웁니다.

메시지 목록 만들기

메시지 하나로 채팅하면 조금 외롭게 느껴질 수 있으니 메시지를 두 개 이상 포함하도록 대화를 변경해 보겠습니다. 여러 메시지를 표시하는 Conversation 함수를 만들어야 합니다. 이 사용 사례에서는 Compose의 LazyColumnLazyRow.를 사용할 수 있습니다. 이러한 컴포저블은 화면에 표시되는 요소만 렌더링하므로 긴 목록에 매우 효율적으로 설계되었습니다. 그와 동시에 XML 레이아웃으로 RecyclerView의 복잡성을 피합니다.

이 코드 스니펫에서는 LazyColumn에 항목 하위 요소가 있음을 알 수 있습니다. List를 매개변수로 가져오고 람다가 Message의 인스턴스인 message라는 매개변수(원하는 대로 이름을 지정할 수 있음)를 수신합니다. 간단히 말해 이 람다는 제공된 List의 항목마다 호출됩니다. 이 샘플 데이터 세트를 프로젝트로 가져와 대화를 빠르게 부트스트랩할 수 있습니다.

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)
    }
}

  
미리보기 표시
미리보기 숨기기

확장 중 메시지에 애니메이션 적용

대화가 점점 흥미로워지고 있습니다. 이제 애니메이션을 사용해 볼 차례입니다. 메시지를 확장하여 더 길게 보여주고 콘텐츠 크기와 배경 색상 모두에 애니메이션 효과를 적용하는 기능을 추가할 예정입니다. 이 로컬 UI 상태를 저장하려면 메시지가 확장되었는지 여부를 추적해야 합니다. 이 상태 변경을 추적하려면 remembermutableStateOf 함수를 사용해야 합니다.

구성 가능한 함수는 remember를 사용하여 메모리에 로컬 상태를 저장하고 mutableStateOf에 전달된 값의 변경사항을 추적할 수 있습니다. 이 상태를 사용하는 컴포저블 및 하위 요소는 값이 업데이트되면 자동으로 다시 그려집니다. 이를 재구성이라고 합니다.

remembermutableStateOf와 같은 Compose의 상태 API를 사용하여 상태를 변경하면 UI가 자동으로 업데이트됩니다.

참고: `by`를 올바르게 사용하려면 다음 가져오기를 추가해야 합니다. Alt+Enter를 누르면 자동으로 추가됩니다.

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.colors.secondaryVariant, 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.colors.secondaryVariant,
                style = MaterialTheme.typography.subtitle2
            )

            Spacer(modifier = Modifier.height(4.dp))

            Surface(
                shape = MaterialTheme.shapes.medium,
                elevation = 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.body2
                )
            }
        }
    }
}

  
미리보기 표시
미리보기 숨기기

이제 메시지를 클릭하면 isExpanded에 따라 메시지 콘텐츠의 배경을 변경할 수 있습니다. clickable 수정자를 사용하여 컴포저블의 클릭 이벤트를 처리합니다. 단순히 Surface의 배경색을 전환하는 대신 MaterialTheme.colors.surface에서 MaterialTheme.colors.primary로 또는 그 반대로 값을 점진적으로 수정하여 배경색에 애니메이션을 적용할 수 있습니다. 이를 위해 animateColorAsState 함수를 사용합니다. 마지막으로 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.colors.secondaryVariant, 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: Color by animateColorAsState(
            if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface,
        )

        // We toggle the isExpanded variable when we click on this Column
        Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            Text(
                text = msg.author,
                color = MaterialTheme.colors.secondaryVariant,
                style = MaterialTheme.typography.subtitle2
            )

            Spacer(modifier = Modifier.height(4.dp))

            Surface(
                shape = MaterialTheme.shapes.medium,
                elevation = 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.body2
                )
            }
        }
    }
}

  
미리보기 표시
미리보기 숨기기
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)
    }
}

  
미리보기 표시
미리보기 숨기기
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.colors.secondaryVariant, 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.colors.secondaryVariant,
                style = MaterialTheme.typography.subtitle2
            )

            Spacer(modifier = Modifier.height(4.dp))

            Surface(
                shape = MaterialTheme.shapes.medium,
                elevation = 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.body2
                )
            }
        }
    }
}

  
미리보기 표시
미리보기 숨기기
@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.colors.secondaryVariant, 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: Color by animateColorAsState(
            if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface,
        )

        // We toggle the isExpanded variable when we click on this Column
        Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            Text(
                text = msg.author,
                color = MaterialTheme.colors.secondaryVariant,
                style = MaterialTheme.typography.subtitle2
            )

            Spacer(modifier = Modifier.height(4.dp))

            Surface(
                shape = MaterialTheme.shapes.medium,
                elevation = 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.body2
                )
            }
        }
    }
}

  
미리보기 표시
미리보기 숨기기

다음 단계

축하합니다. Compose 튜토리얼을 완료했습니다. 이미지와 텍스트가 포함된 확장형 및 애니메이션 메시지의 목록을 효율적으로 표시하는 간단한 채팅 화면을 빌드했습니다. 이 화면은 머티리얼 디자인 원칙으로 디자인했으며 어두운 테마와 미리보기가 포함되어 있고 사용된 코드가 100줄도 안 됩니다.

지금까지 배운 내용은 다음과 같습니다.

  • 구성 가능한 함수 정의
  • 컴포저블에 다른 요소 추가
  • 레이아웃 컴포저블을 사용하여 UI 구성요소 구조화
  • 수정자를 사용한 컴포저블 확장
  • 효율적인 목록 만들기
  • 상태 추적 유지 및 수정
  • 컴포저블에 사용자 상호작용 추가
  • 메시지 확장 중 메시지에 애니메이션 적용

이러한 단계 중 일부에 관해 자세히 알아보려면 아래 리소스를 살펴보세요.

학습 계속하기

과정
Jetpack Compose를 알아보고 마스터하는 데 도움이 되는 선별된 Codelab 및 동영상 과정을 확인해 보세요.
가이드
이 튜토리얼에서 언급하는 Jetpack Compose API에 관해 알아보려면 문서를 살펴보세요.
샘플
강력한 Compose 기능을 사용하는 방법을 보여주는 다음 샘플 앱을 통해 아이디어를 얻어 보세요.