[Android] Jetpack Compose 때려잡기 - 튜토리얼

Doyeon Lim·2022년 5월 12일
5

Android

목록 보기
4/4
post-thumbnail

android jetpack compose 공식 문서의 자료를 따라 학습합니다.
compose 공식 문서 : https://developer.android.com/courses/pathways/compose
컴때잡 github : https://github.com/dddooo9/compose-practice

📖 Compose란?

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

XML 레이아웃을 수정하거나 Layout Editor를 사용하지 않고, Composable function을 호출해 원하는 요소를 정의하면 Compose 컴파일러가 나머지 작업을 완료합니다.

📕 1. Composable functions

compose는 composable function을 사용해 UI의 구성 과정(요소 초기화, 부모에 연결 등) 보다는, 어떻게 생겼는지를 묘사하고 데이터 종속 항목을 제공해 프로그래매틱 방식으로 앱의 UI를 정의할 수 있습니다.

composable function을 만들기 위해서는 함수 이름에 @Composable 어노테이션을 추가하면 됩니다.

1-1. Compose Project 생성하기

Compose 앱을 생성하기 위해서는 Android Studio에서 새 프로젝트를 생성해야 합니다.

프로젝트 생성 시, 아래와 같이 Empty Compose Activity를 선택합니다.

이렇게 생성된 compose 프로젝트에는 기존에 생성했던 프로젝트의 MainActivity와는 매우 다른 코드들이 작성되어 있습니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposePracticeTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

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

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ComposePracticeTheme {
        Greeting("Android")
    }
}

*공식문서의 튜토리얼을 따라 단계적으로 학습하기 위해 해당 코드는 지우고 튜토리얼을 따랐습니다.

1-2 Text 추가하기

compose UI를 통해 Text를 추가하는 방법은 아래와 같습니다.

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

Text 함수에 넘겨주는 텍스트 라벨을 화면에 표시합니다.

1-3 composable function 정의하기

composable function을 정의하기 위해서는 @Composable annotation을 추가하면 됩니다.

아래는 파라미터로 받은 이름을 Text로 표시하는 composable function인 MessageCard를 정의한 예제입니다.

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

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

결과는 아래와 같습니다.

1-4 compose preview 확인하기

composable function은 @Preview annotation을 사용해 Android Studio 내에서 그 결과를 미리 볼 수 있습니다.

대신 매개변수를 사용하지 않는 composable function에 사용해야 합니다.

따라서 위에서 작성한 MessageCard가 아닌, preview 하기 위한 composable function으르 새롭게 작성합니다.

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

해당 함수를 작성 후, 프로젝트를 다시 빌드한 뒤에 오른쪽 상단에 위치한 split 탭을 눌러 아래와 같이 preview를 확인할 수 있습니다.

📗 2. Layouts

UI 요소는 계층적이며 다른 요소에 포함된 요소가 있습니다.
compose에서는 다른 composable function에서 composable function을 호출해 UI 계층 구조를 빌드합니다.

2-1. Text 여러 개 추가하기

해당 튜토리얼은 메시지 목록이 포함된 간단한 메시지 화면을 빌드하여 다양한 compose의 기능을 살펴보는 것이 목표입니다.

먼저 메시지 작성자의 이름과 내용을 표시해 message composable을 더 복잡하게 만들어봅니다.

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

data class로 정의한 Message 객체를 매개변수로 넘겨 author, body를 텍스트로 표시합니다.

위와 같이 텍스트 두 개를 정상적으로 표시하지만, 정렬에 대한 정보가 없어 서로 위에 겹치게 표시되어있다는 문제가 있습니다.

2-2 Column, Row, Box 사용하기

Column 함수를 사용해 요소를 수직으로, Row 함수를 사용해 수평으로 정렬할 수 있고, Box를 사용해 요소를 쌓을 수 있습니다.

겹쳐진 텍스트를 정렬하여 표시하기 위해 Column을 사용합니다.

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

2-3 Image 추가하기

메시지 발신자의 프로필 사진을 추가합니다.
정렬을 위해 Row로 감싸고, Image 를 추가합니다.

@Composable
fun MessageCard(msg: Message) {
    Row {
        Image(
            painter = painterResource(id = R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
        )

        Column {
            Text(text = msg.author)
            Text(text = msg.body)
        }
    }
}

2-4 Modifier 사용하기

composable의 크기, 레이아웃, 모양을 변경하거나 clickable하게 만드는 등의 동작을 수행하기 위해 Modifier를 사용할 수 있습니다.

일부 레이아웃 개선을 위해 Modifier를 사용합니다.

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
        )

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

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

이미지를 원형으로 출력하고, 크기나 spacing에 대해 설정해줍니다.

*xml로 레이아웃을 구성할 때 원형 이미지를 만들기 위해서는 drawable을 만들어 뷰에 지정해주는 등의 추가 작업이 필요했던 반면, compose 사용시 CircleShape을 사용하여 이미지 형태를 쉽게 지정해줄 수 있습니다.

📘 3. Material Design

compose는 Material Design을 지원하도록 빌드되어 있습니다.

3-1 Material Design 사용하기

Material Design을 사용해 MessageCard composable의 디자인을 개선해보겠습니다.

프로젝트 생성시에 지정한 앱 이름에 따라 자동적으로 material theme이 생성됩니다.

해당 theme으로 MessageCard 함수를 wrapping하여 사용합니다.
Preview와 setContent 함수 모두에서 이 작업을 실행해야 composable이 앱 테마에 정의된 스타일을 상속하여 앱 전체에서 일관성이 보장됩니다.

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

// ...

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

3-2 Color 지정하기

MaterialTheme.colors를 사용하여 래핑된 테마의 색으로 스타일을 지정할 수 있습니다.

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

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
            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)
        }
    }
}

3-3 Typography 지정하기

Text composable에 추가하여 스타일을 지정할 수 있다.

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
            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
            )
        }
    }
}

3-4 Shape 지정하기

Shape을 사용해 마지막 작업을 할 수 있습니다.

Surface로 메시지 본문 영역을 wrapping 할 수 있습니다.

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
            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
                )
            }
        }
    }
}

메시지 영역에 elevation이 추가되고, padding이 지정된 것을 확인할 수 있습니다.

3-5 Dark Theme 사용하기

다크 모드 대응시, preview를 통해서도 확인할 수 있습니다.

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

지정한 name 대로 라이트 모드와 다크 모드의 preview를 아래와 같이 확인할 수 있습니다.

각 테마의 색상은 Theme.kt 파일에 정의되어 있습니다.

📙 4. Lists and animations

compose를 통해 list를 손쉽게 만들고, animation을 추가할 수 있습니다.

4-1. List 구현하기

메시지 두 개 이상 포함하는 대화뷰를 구현하기 위해 Conversation 함수를 만들어야 합니다.

LazyColumn, LazyRow는 화면에 표시되는 요소만 렌더링하여 긴 List에 효율적인 composable 입니다.

*Recyclerview가 스크롤 시 화면에 보이는 영역의 뷰만 렌더링된다는 점에서 유사함을 확인할 수 있습니다.

LazyColumn/LazyRow의 하위 요소인 items를 사용해 List 매개변수를 가져와 항목마다 람다식을 호출합니다.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposePracticeTheme {
                Conversation(SampleData.conversationSample)
            }
        }
    }
}

// ...

@Composable
fun Conversation(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageCard(msg = message)
        }
    }
}

@Preview(name = "Light Mode")
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "Dark Mode"
)
@Composable
fun PreviewConversation() {
    ComposePracticeTheme {
        Conversation(SampleData.conversationSample)

    }
}

아래와 같은 화면을 확인할 수 있으며, 수직 스크롤로 모든 데이터를 표시했음을 알 수 있습니다.

4-2 animation 적용하기

긴 메시지는 확장하여 볼 수 있도록 animation을 적용해보도록 하겠습니다.

이를 구현하기 위해서 로컬 UI 상태 변경을 추적하기 위해 remembermutableStateOf 함수를 사용해야 합니다.

composable 함수는 remember를 사용해 메모리에 로컬 상태를 저장하고, mutableStateOf전달된 값의 변경사항을 추적할 수 있습니다.

여기서 값이 업데이트되면 자동으로 다시 그려지는 Recomposition이 발생합니다.

클릭 이벤트를 처리하기 위해 clickable modifier를 사용합니다.

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
        )

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

        var isExpanded by remember { mutableStateOf(false)}

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

아래와 같이 확장 여부에 따라 maxline 값을 변경해줌을 확인할 수 있습니다.

추가로 단순히 Surface의 배경색을 바꾸는 대신, animateColorAsState 함수를 사용하여 MaterialTheme의 색 두 가지를 갖고 값을 점진적으로 수정해 배경색에 animation을 적용할 수 있습니다.

그리고 animateContentSize modifier를 사용해 메시지 container 크기 변화에도 animation을 적용합니다.

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
        )

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

        var isExpanded by remember { mutableStateOf(false) }

        val surfaceColor by animateColorAsState(
            if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface
        )

        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,
                color = surfaceColor,
                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
                )
            }
        }
    }
}

아래와 같이 메시지를 클릭하면 메시지 확장과 색 변경에 animation이 적용되었음을 확인할 수 있습니다.

마무리

지금까지 android jetpack compose 튜토리얼 문서를 바탕으로 compose 기초에 대해 학습하였습니다.

기존 xml로 레이아웃을 작업할 때는 kt 파일과 xml 파일을 이동하는 것도 번거로웠고 더 나은 UI를 위해 복잡한 Drawable을 추가해주어야 했는데 compose는 그러한 면에서 큰 이점을 갖는 것 같습니다.

상태 관리 등의 복잡한 비즈니스 로직과 연동하였을 때 compose의 효율성은 어떨지 더 학습해보아야 할 것 같습니다.😛

profile
🙇‍♀️ Android

0개의 댓글