읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. Slot API를 사용하여 버튼을 커스텀하고 상단 앱 바를 정의해봅니다. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.

Slot(슬롯) API

코드랩에서 해당 섹션은 이론적인 내용이라며 실습 코드를 조금 비틀어놓았다. 예시와 동작하도록 실제로 사용할 수 있는 코드로 재작성하였다.

구글이 설명하기를 Slot API는 구글이 개발자들의 UI 컴포넌트 커스텀 요구 속도를 따라잡을 수 없어 "슬롯"이라는 개념을 도입하여 내부에 개발자가 원하는 요소로 채울 수 있게끔 공간을 제공한다. 다음 그림의 빈 공간이 슬롯 API가 개발자로 하여금 작성할 수 있도록 비워둔 공간을 의미한다.

Slot API 예시 - 버튼

Codelab_Jetpack_Compose_layout_011

기존 뷰를 사용하여 기본 버튼을 생성한다면 다음 그림 모양의 버튼이 생성되었을 것이다.

Codelab_Jetpack_Compose_layout_012

위 그림대로 버튼을 컴포즈로 구현하면 다음과 같다.

@Composable
fun SlotButton() {
    Button(onClick = {}) {
        Text(text = "Button")
    }
}

이전까지 뷰 기반 UI 컴포넌트에서는 버튼을 작성하고 텍스트를 변경하는 정도가 대부분의 사용례였다. 컴포즈를 사용한다면 좀 더 동적으로 버튼을 가공할 수 있다.

Codelab_Jetpack_Compose_layout_013

위의 버튼을 컴포즈로 구현해보자.

@Composable
fun SlotButton(modifier: Modifier = Modifier) {
    Button(onClick = {} ) {
            Image(Icons.Filled.Favorite, contentDescription = null)
            Spacer(Modifier.width(4.dp))
            Text(text = "Button")
        }
}

코드랩의 이미지와는 달리 하트 아이콘의 색상이 다르게 표기되는 경우가 있지만 이는 프로젝트 색상 설정의 차이이므로 적당히 color를 부여하면 되겠다. 중요한 점은 개발자가 의도하는 대로 버튼에 컴포즈를 부여할 수 있음이다. 개발자가 버튼 컴포저블에 하위 컴포저블을 입력할 수 있도록 어떻게 구현했는지 버튼의 내부 동작 코드를 열어봤다.

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    elevation: ButtonElevation? = ButtonDefaults.elevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) {
    val contentColor by colors.contentColor(enabled)
    Surface(
        modifier = modifier,
        shape = shape,
        color = colors.backgroundColor(enabled).value,
        contentColor = contentColor.copy(alpha = 1f),
        border = border,
        elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp,
        onClick = onClick,
        enabled = enabled,
        role = Role.Button,
        interactionSource = interactionSource,
        indication = rememberRipple()
    ) {
        CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
            ProvideTextStyle(
                value = MaterialTheme.typography.button
            ) {
                Row(
                    Modifier
                        .defaultMinSize(
                            minWidth = ButtonDefaults.MinWidth,
                            minHeight = ButtonDefaults.MinHeight
                        )
                        .padding(contentPadding),
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically,
                    content = content
                )
            }
        }
    }
}

버튼 컴포저블의 content 파라미터를 통해 컴포저블을 입력할 수 있도록 하위 컴포저블 람다를 사용한다. 이를 통해 버튼 내에서 출력되도록 자체 컴포저블들을 정의할 수 있다. content 파라미터는 내부 Row 컴포저블의 content로 사용된다. verticalAlignment에 이미 속성이 부여되어 있어 별도의 조치를 하지 않고도 작성된 하위 컴포저블들이 중앙 정렬된 채로 출력된다.

만약 하위 컴포저블을 Row 컴포저블로 감싸서 입력하면 어떨까.

@Composable
fun SlotButton(modifier: Modifier = Modifier) {
    Button(onClick = {} ) {
        Row {
            Image(Icons.Filled.Favorite, contentDescription = null)
            Spacer(Modifier.width(4.dp))
            Text(text = "Button")
        }
    }
}

위 코드대로 작성하면 Text 컴포저블이 상단에 붙은 채로 출력이 된다. 그 이유는 Button 컴포저블의 하위 컴포저블은 Row 컴포저블이고 해당 컴포저블에 대해 중앙정렬할 뿐, 나머지에 대해서는 중앙정렬하지 않기 때문이다. 따라서, Row로 감싸고도 중앙정렬 상태로르 유지하려면 하위 컴포저블에 대해서 중앙정렬 속성을 다음과 같이 부여해야 한다.

@Composable
fun SlotButton(modifier: Modifier = Modifier) {
    Button(onClick = {} ) {
        Row {
            Image(Icons.Filled.Favorite, contentDescription = null)
            Spacer(Modifier.width(4.dp))
            Text(text = "Button", Modifier.align(Alignment.CenterVertically))
        }
    }
}

그러나 속성을 한 번 더 작성해야 하는 수고로움을 따져봤을 때 이미 Button 컴포저블 내부에 Row 컴포저블이 선언되어있으므로 굳이 Row 컴포저블로 감쌀 필요는 없어 보인다.

Slot API 예시 - 상단 앱 바

슬롯 API을 사용하는 가장 대표적인 사용례로 상단 앱 바를 들 수 있다.

Codelab_Jetpack_Compose_layout_014

위 그림대로 상단 앱 바를 구성함이 대부분의 경우인데, 뷰 기반 UI 컴포넌트와 마찬가지로 각각 구분해서 다르게 정의할 수 있다.

Codelab_Jetpack_Compose_layout_015

@Composable
fun SlotTopAppBar() {
    TopAppBar(
        title = {Text(text = "Page title", maxLines = 2)},
        navigationIcon = {Icon(Icons.Filled.Menu, contentDescription = null)}
    )
}

예시로 든 상단 앱 바를 간단히 구현하면 위 코드처럼 작성할 수 있다. 그러나 title, navigationIcon에 국한되지 않고 하위 컴포저블을 구현하려 한다면 content를 위한 별도의 람다 컴포저블 함수를 정의하여 Box, Row 등을 사용하여 정의할 수 있겠다.

참고자료

  1. Google Codelab - Jetpack Compose Layout #4 슬롯 API

+ Recent posts