Week 1 Tutorial: Jetpack Compose basics

jihyo·2021년 11월 10일

DevFest 2021

목록 보기
1/8

Jetpack Compose는 네이티브 Android UI를 빌드하기 위한 최신 도구 킷이다. 더 적은 수의 코드, 직관적인 Kotlin API로 UI 개발을 간소화하고 가속화할 수 있다.

1. Composable 함수

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

Text 요소 추가

Android Arctic Fox의 최신 버전을 다운로드하고 Empty Composable Activity 템플릿을 사용하면 시작할 수 있다.

OnCreate 메서드 내부에 Text 요소를 추가하면 'Hello world!'가 표시된다. 콘텐츠 블록을 정의하고 Text() 함수를 호출하고 setContent 블록은 Composable 함수를 호출하는 활동의 레이아웃을 정의한다. Composable 함수는 다른 구성 가능한 함수에서만 호출 가능하다.

class MainActivity: ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstaceState)
        setContent {
            Text("Hello world!")
        }
    }
}

Composable 함수 정의

Composable 함수를 만들려면 앞서 말한 것처럼 @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 Studio에서 함수 미리보기(Preview)

Android 기기나 애뮬레이터를 사용하지 않고 IDE 내에서 Composable 함수를 미리 볼 수 있다. Composable 함수는 모든 매개변수의 기본값을 제공해야 하기 때문에 앞선 예제인 MessageCard() 함수는 미리보기가 불가하다. 미리보기 위해서는 @Composable 앞에 @Preview 주석을 추가하면 된다.

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

@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard("Android")
}

프로젝트를 빌드하면 새로운 PreviewMessageCard() 함수가 어디에도 호출되지 않기 때문에 앱 자체는 변경되지 않지만 Android Studio는 Privew 창을 추가한다. @Preview 주석으로 표시된 Composable 함수에 의해 생성된 UI의 Preview를 보여준다. Preview를 업데이트 하려면 Preview 창 상단의 새로고침 버튼을 클릭하면 된다.
Android Studio를 사용하여 Composable 함수 미리보기

2. Layout

Compose에서는 다른 Composable 함수로부터 Composable 함수를 호출하여 UI 계층 구조를 빌드한다.

여러 Text 추가

애니메이션으로 확장할 수 있는 메시지 목록이 포함된 간단한 화면 예시를 빌드할 것이다. 메시지 작성자, 내용을 포함하는 코드를 작성해볼 것이다. String이 아닌 Message 객체를 받도록 매개변수를 정하고 MessageCard Composable 내에 다른 Text Composable을 추가해야 한다. 또한, Preview도 업데이트해야 한다.

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 사용

Column 함수를 사용하면 요소를 수직으로 정렬할 수 있다. 더 나아가 Row를 사용하여 가로로 정렬할 수 있고 Box를 사용하여 요소를 쌓을 수 있다.

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

Image 요소 추가

발신자의 프로필 사진을 추가해 메시지를 보내는 코드를 추가해보려 한다. Resource Manager에서 제공하는 Image를 사용하거나 Codelab에서 제공하는 Image를 사용할 것이고 구역을 나누기 위해 Row Composable도 추가할 것이다.

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

Layout 구성

Image를 추가해 보여줄 것을 더 늘렸지만 정리가 되어있지 않다. Composable을 커스텀하기 위해 modifier를 사용한다. Composable의 크기, 레이아웃, 모양을 변경하거나 요소를 클릭 가능하게 만드는 등의 상호작용을 추가할 수 있다.

@Composable
fun MessageCard(msg: Message) {
    // 패딩 추가
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
            modifier = Modifier
                // Image 크기 40dp
                .size(40.dp)
                // Image를 원형으로 자름
                .clip(CircleShape)
        )

        // Image와 Column 사이에 수평 공간 추가
        Spacer(modifier = Modifier.width(8.dp))

        Column {
            Text(text = msg.author)
            // 메시지 작성자와 내용 사이에 수직 공간 추가
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = msg.body)
        }
    }
}

3. 머티리얼 디자인 Material Design

Compose는 머티리얼 디자인 원칙을 지원하도록 빌드되어 있다. 그래서 많은 UI 요소가 머티리얼 디자인을 즉시 사용 가능하도록 구현할 수 있다.

머티리얼 디자인 사용

Jetpack Compose는 머티리얼 디자인 스타일링을 사용해 MessageCard Composable의 디자인을 개선할 수 있다. 이번 예시의 경우 ComposeTutorialTheme에서 생성한 머티리얼 테마로 MessageCard 함수를 래핑한다. @PreviewsetContent 함수 모두에서 이 작업을 실행한다.
머티리얼 디자인은 색상, 서체, 도형의 세 가지 핵심 요소에 따라 이루어진다.

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

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

색상 Color

래핑된 테마의 색상으로 간단하게 스타일을 지정할 수 있으며 색상이 필요한 모든 곳에 테마의 값을 사용할 수 있다. 아래 코드는 제목 스타일을 지정하고 이미지에 테두리를 추가하는 코드이다.

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

서체 Typography

머티리얼 서체 스타일은 MaterialTheme에서 사용할 수 있으며 Text Composable에 추가하면 된다.

       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(shape = MaterialTheme.shapes.medium, elevation = 1.dp) {
               Text(
                   text = msg.body,
                   modifier = Modifier.padding(all = 4.dp),
                   style = MaterialTheme.typography.body2
               )
           }

어두운 테마 사용

야간에 밝은 디스플레이를 사용하지 않거나 기기 배터리를 절약하기 위해 어두운 테마를 사용 설정할 수 있다. 머티리얼 색상, 텍스트, 배경을 사용하면 어두운 배경에 맞춰 자동으로 조정된다. 밝은 테마와 어두운 테마의 색상 선택은 IDE로 생성된 Theme.kt 파일에 정의되어 있다.

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

4. 리스트 및 애니메이션

메시지 목록 만들기

메시지로 대화하는 앱을 만들어 보기 위해 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() {
    JetpackComposeTutorialTheme {
        Conversation(SampleData.conversationSample)
    }
}

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

메시지를 확장하여 더 길게 보여주고 콘텐츠 크기와 배경 색상에 애니메이션 효과를 적용할 것이다. UI 상태를 저장하려면 메시지가 확장되었는지 여부를 추적해야 한다. 이 상태 변경을 추적하려면 remembermutableStateOf 함수를 사용해야 한다.

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

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

class MainActivity: ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           JetpackComposeTutorialTheme {
               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))

        // 메시지가 확장되었는 지 추적
        // 변수
        var isExpanded by remember { mutableStateOf(false) }

        // 해당 열을 클릭할 경우 isExpanded 변수를 토글 
        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),
                    // 메시지가 확장되면 모든 내용이 표시
                    // 그렇지 않으면 첫 번째 줄만 표시
                    maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                    style = MaterialTheme.typography.body2
                )
            }
        }
    }
}

위 코드로 인해 메시지를 클릭하면 isExpanded에 따라 메시지 콘텐츠의 배경을 변경할 수 있다. clickable 수정자를 사용하여 Composable의 클릭 이벤트를 처리한다. 단순히 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))

        // 메시지가 확장되었는 지 추적
        // 변수
        var isExpanded by remember { mutableStateOf(false) }
        // surfaceColor는 한 색상에서 다른 색상으로 점진적으로 업데이트
        val surfaceColor: Color by animateColorAsState(
            if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface,
        )

        // 해당 열을 클릭할 경우 isExpanded 변수를 토글
        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 색상은 primary에서 surface으로 점진적으로 변경
                color = surfaceColor,
                // animateContentSize는 Surface 크기를 점진적으로 변경
                modifier = Modifier.animateContentSize().padding(1.dp)
            ) {
                Text(
                    text = msg.body,
                    modifier = Modifier.padding(all = 4.dp),
                    // 메시지가 확장되면 모든 내용이 표시
                    // 그렇지 않으면 첫 번째 줄만 표시
                    maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                    style = MaterialTheme.typography.body2
                )
            }
        }
    }
}  

0개의 댓글