컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.
튜토리얼

Jetpack Compose 튜토리얼

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

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

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

강의 1: 구성 가능한 함수

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

텍스트 요소 추가

시작하려면 Android 스튜디오 최신 버전을 다운로드하고 New Project를 선택하여 앱을 만든 후 Phone and Tablet 카테고리에서 Empty Compose 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.material.Text

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

구성 가능한 함수 정의

함수를 구성 가능하게 하려면 @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!")
}

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

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

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

Android 스튜디오에서 구성 가능한 함수 미리보기
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.Text

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello world!")
        }
    }
}
  
미리보기 표시
미리보기 숨기기
// ...
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!")
}

  
미리보기 표시
미리보기 숨기기
// ...
import androidx.compose.ui.tooling.preview.Preview

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

@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard("Android")
}
  
미리보기 표시
미리보기 숨기기
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을 추가합니다.
Row를 사용하면 항목을 수평으로 정렬할 수 있고 Box를 사용하면 요소를 쌓을 수 있습니다.

// ...
import androidx.compose.foundation.layout.Column

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

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

이미지 요소 추가

발신자의 프로필 사진을 추가하여 메시지 카드를 보완합니다. 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)
        }
  
    }
  
}
  
미리보기 표시
미리보기 숨기기

레이아웃 구성

메시지 레이아웃의 구조는 올바르지만 요소의 간격이 균등하지 않고 이미지가 너무 큽니다. 컴포저블을 장식하거나 구성하기 위해 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)
        }
    }
}
  
미리보기 표시
미리보기 숨기기
// ...

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

  
미리보기 표시
미리보기 숨기기
두 개의 겹치는 Text 컴포저블 미리보기
// ...
import androidx.compose.foundation.layout.Column

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

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

강의 3: Material Design

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

머티리얼 디자인 사용

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

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

먼저 프로젝트에서 만든 머티리얼 테마 ComposeTutorialThemeSurfaceMessageCard 함수를 래핑합니다. @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("Colleague", "Take a look at Jetpack Compose, it's great!")
            )
        }
    }
}

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

색상

MaterialTheme.colors를 사용하여 래핑된 테마의 색상으로 스타일을 지정할 수 있습니다. 색상이 필요한 곳에 테마의 이러한 값을 사용하면 됩니다.

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

// ...
import androidx.compose.foundation.border
import androidx.compose.material.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
           )

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

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

서체

머티리얼 서체 스타일은 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.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
           )
       }
   }
}

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

도형

Shape을 사용하여 최종 터치를 추가할 수 있습니다. 먼저 Surface 컴포저블을 중심으로 메시지 본문 텍스트를 래핑합니다. 이렇게 하면 메시지 본문의 도형과 높이를 맞춤설정할 수 있습니다. 더 나은 레이아웃을 위해 메시지에 패딩도 추가합니다.

// ...
import androidx.compose.material.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.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
               )
           }
       }
   }
}

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

어두운 테마 사용

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

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

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

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

밝은 테마와 어두운 테마의 색상 선택은 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("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
      )
    }
   }
}
  
미리보기 표시
미리보기 숨기기
// ...

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

  
미리보기 표시
미리보기 숨기기
// ...
import androidx.compose.foundation.border
import androidx.compose.material.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
           )

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

  
미리보기 표시
미리보기 숨기기
// ...
import androidx.compose.material.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.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
               )
           }
       }
   }
}

  
미리보기 표시
미리보기 숨기기
// ...
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("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
      )
    }
   }
}
  
미리보기 표시
미리보기 숨기기
그림 3. 밝은 테마와 어두운 테마의 컴포저블을 모두 보여주는 미리보기

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

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

메시지 목록 만들기

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

이 코드 스니펫에서는 LazyColumnitems 하위 요소가 있음을 알 수 있습니다. 이 하위 요소는 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)
    }
}

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

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

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

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

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

참고: by를 올바르게 사용하려면 다음 import 구문을 추가해야 합니다. 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.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 수정자를 사용하여 메시지 컨테이너 크기에 부드럽게 애니메이션을 적용합니다.

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

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

  
미리보기 표시
미리보기 숨기기
// ...
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.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 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 구성요소 구조화
  • 수정자를 사용한 컴포저블 확장
  • 효율적인 목록 만들기
  • 상태 추적 유지 및 수정
  • 컴포저블에 사용자 상호작용 추가
  • 메시지 확장 중 메시지에 애니메이션 적용

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

다음 단계

설정
Compose 튜토리얼을 완료했으므로 이제 Compose를 사용하여 앱 빌드를 시작할 수 있습니다.
과정
Jetpack Compose를 알아보고 마스터하는 데 도움이 되는 선별된 Codelab 및 동영상 과정을 확인해 보세요.