읽기 전

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

Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. 레이아웃 Modifier를 사용하여 Modifier 확장함수를 어떻게 정의하는 지 알아봅니다. 레이아웃 컴포저블을 사용하여 Column을 직접 구현해봅니다.. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.

커스텀 레이아웃 만들기

Jetpack Compose는 Column, Row, Box 등의 라이브러리 내장 컴포저블들을 결합하여 커스텀 컴포저블을 구성하여 재사용성을 높인다. 그러나 하위 컴포넌트를 수동으로 측정하고 배치해야 한다면 앱만의 고유한 동작 로직을 정의해야 할 수도 있다. 이 경우 Layout 컴포저블을 사용한다. Column, Row 등의 모든 상위 수준 레이아웃은 Layout 컴포저블을 사용하여 빌드된 것을 확인할 수 있다.

Compose의 레이아웃 원칙

컴포저블 함수 중에는 호출될 때 UI 요소를 리턴하여 화면에 렌더링될 UI 트리에 추가한다. 각 요소에는 상위 요소가 하나 있고 하위 요소가 여러 개(혹은 하나) 존재한다. 추가로 상위 요소 내의 위치(x, y)와 크기(width, height)도 포함된다.

UI 요소는 자체적으로 측정되며 제약조건을 충족해야 한다고 설명한다. 제약 조건은 최소/최대 width, height이다. 만약 하위 UI 요소가 존재한다면 각 하위 요소들을 측정하여 자체 크기를 파악한 뒤 리턴된 결과를 기준으로 자신의 조건에 맞춰 하위 요소를 배치할 수 있다.

가장 유념해야 할 점은 Compose UI는 다중 패스 측정을 허용하지 않는다는 점이다. 레이아웃 요소가 다른 측정 구성을 시도하기 위해 하위 요소를 두 번 이상 측정할 수 없다. 단일 패스 측정은 성능 측면에서 유리하기에 도입된 개념으로 Compose UI 트리의 효율적 처리에 목적을 둔다. 따라서, 추가 정보가 필요한 경우에는 별도의 방법을 사용해야 한다.

레이아웃 Modifier 사용

layout Modifier를 사용하여 요소를 측정하고 배치하는 과정을 수동으로 제어해보자. 일반적인 laout Modifier 구조는 다음과 같다.

fun Modifier.customLayoutModifier(...) = Modifier.layout {
measurable, constraints ->
...
})

layout Modifier 사용 시 람다 매개변수 measurable, constraints가 있다.

  • measurable : 측정하기 배치할 하위 요소
  • constraints : 하위 요소의 너비와 높이 최솟값과 최댓값

화면에 Text 컴포저블이 배치될 때 Text 컴포저블의 상단에서 기준선까지의 거리를 제어해보자. 해당 기능을 구현하기 위해선 layout Modifier를 사용하여 화면에 컴포저블을 수동으로 배치해야 한다. 상단에서 첫 번째 기준선까지의 거리가 24.dp로 설정되도록 작성하자.

Codelab_Jetpack_Compose_layout_023

fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
...
}
)

Modifier의 확장함수로 firstBaselineToTop이라는 함수를 정의하고 파라미터로 Dp 값을 입력받도록 하였다.

이제 컴포저블을 측정해야 한다. 앞서 작성하였듯이 하위 요소는 원칙적으로 한 번만 측정할 수 있음을 기억하자. measurable.measure(constraints)를 호출하여 컴포저블을 측정하였다.

fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
...
}
)

measure(constraints)를 호출하여 컴포저블을 측정할 때 constraints 람다 매개변수로부터 사용할 수 있는 컴포저블의 제약 조건을 전달하거나 직접 만들 수 있다. measure()를 호출한 결과는 placeRelative(x, y)를 호출하여 컴포저블을 배치할 수 있는 Placeable이다. 해당 함수를 사용하여 컴포저블을 배치한다. 우선 위 코드에서 작성한 대로 별도의 측정 제한 없이 주어진 제약 조건만 넘겨준다.

측정한 뒤 baseline을 갖고 있는지 체크하여 변수에 저장한다.

fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then (
layout { measurable, constraint ->
val placeable = measurable.measure(constraint)
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// 최상단이 0이므로 firstBaseline은 baseline까지의 길이를 값으로 가짐 = fontSize
// placeableY는 padding값 - baseline까지의 길이
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
// 전체 출력할 높이는 하위 컴포저블의 높이 + padding값 - baseline까지의 길이
// padding 값이 0이면 baseline과 하단까지의 간격만 출력
val height = placeable.height + placeableY
layout(placeable.width, height) {
placeable.placeRelative(0, placeableY)
}
}
)

높이를 측정하는 코드가 있다. 우선 하위 컴포즈의 최상단 y값은 0이고 아래로 내려갈수록 증가하는 값임을 알아야 한다. 따라서 firstBaseline은 baseline까지의 높이로 여기서는 텍스트 사이즈와 같은 값이다. 그리고 firstBaselineToTop은 우리가 컴포저블의 최상단과 baseline 사이의 간격으로 지정할 값이므로 placeableY의 값은 padding 값 - 텍스트 사이즈가 되어 만약 텍스트 크기보다 매개변수 값이 더 작다면 글자가 잘린 채로 출력될 것임을 추측할 수 있다.

height 변수는 전체 컴포저블을 출력할 높이로 설정하였다. placeable.height는 하위 컴포저블의 높이이고 placeableY값을 더하면 결국 하위 컴포저블 높이 - 텍스트 사이즈가 되어 baseline과 컴포저블 하단까지의 빈 공간과 매개변수로 입력한 padding값에 해당한다.

마지막으로 layout 함수를 호출하여 width, height를 매개변수로 받고 placeable.placeRelative 함수를 호출하여 컴포저블을 배치한다. x값이 0이므로 좌측 끝에서부터 x값이 증가하는 방향으로 출력된다. y값은 placeableY기준으로 출력하므로 만약 매개변수가 텍스트 사이즈보다 작아 placeableY의 값이 음수라면 글자가 레이아웃 기준 음수높이에서부터 출력되어 상단부분이 가려져서 출력될 것이다.

@Preview
@Composable
fun FirstBaselineToTopPreview() {
Text("fooooo", Modifier.firstBaselineToTop(24.dp))
}
@Preview
@Composable
fun TopPaddingPreview() {
Text("baaaar", Modifier.padding(top=24.dp))
}

Codelab_Jetpack_Compose_layout_024

확장함수를 사용하여 간격을 부여한 경우와 Padding을 상단에 부여한 경우 조금 다르게 출력됨을 확인하였다.

레이아웃 컴포저블 사용

방금까지 layout Modifier를 사용하여 단일 컴포저블을 측정하고 화면에 배치되는 방식을 제어하는 커스텀 확장함수를 구현하였으니 이번에는 단일 컴포저블이 아니라 하위 요소들을 측정하여 배치하기 위해 컴포저블에서 Layout을 사용해보자. Layout을 사용하는 컴포저블의 일반적인 구조는 다음과 같다.

@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
// custom layout attributes
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}

적어도 커스텀 레이아웃을 정의한다면 스타일을 지정할 modifier 매개변수와 어떤 하위 컴포저블을 담을 것인지 입력받는 content 매개변수는 필수적으로 선언해야 한다. 해당 매개변수들은 내부에 선언된 Layout 컴포저블로 전달된다. Layout 컴포저블 선언 후 뒤따라오는 후행 람다에서 layout Modifier를 사용했을 때와 같은 람다 매개변수가 주어진다. 조금 다른 점은 하위 요소가 여러 개일 수도 있으므로 변수 이름을 복수형으로 변경하였다.

해당 기본 구조를 토대로 Column을 구현해보자. 편의상 레이아웃은 최대한의 공간을 차지한다고 가정하자.(fillMax!)

@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 부여된 제약조건 로직에 따라 하위 요소들을 측정하고 배치한다.
}
}

앞서 layout Modifier에서도 그러하였듯이 하위 요소들을 측정해야 한다.

@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 주어진 제약조건으로만 측정하되 추가적인 제약을 자식 뷰에 적용하지 않는다.
// 측정된 자식들이 담긴 리스트를 받는다.
val placeables = measurables.map { measurable ->
// 각 자식들에 대해 측정 수행
measurable.measure(constraints)
}
}
}

각 하위 요소별로 측정을 수행해야 하므로 람다 변수로 받은 measurables에 대해 map을 호출한 뒤 람다 매개변수로 자식 뷰를 측정할 수 있는 람다 매개변수를 선언하고 제약조건에 대해 자식뷰 측정을 수행한다. 이로써 측정 가능한 모든 content를 확인하였다.

각 요소들을 배치하기 위해 전체 레이아웃의 사이즈를 정의하자.

@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 자식 요소 측정 - 앞서 작성했기에 생략
...
// layout의 크기를 최대 사이즈로 설정(fillMax!)
layout(constraints.maxWidth, constraints.maxHeight) {
// 자식 요소 배치
}
}
}

하위 요소 배치를 수행하기 전 Column의 크기를 계산한다. 상위 요소만큼 크게 만들기로 했으므로 상위 요소로부터 전달된 제약조건 사이즈를 입력한다. 하위 요소 배치에 사용되는 Layout의 람다 함수가 제공하는 layout(width, height) 함수를 호출하여 자체적으로 Column의 크기를 지정한다.

Column 레이아웃의 사이즈를 지정했으니 하위 요소들을 어떻게 배치할 지 작성하자.

@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// 이전 자식 요소가 어느 y좌표에 배치되었는지 추적하기 위한 변수
var yPosition = 0
layout(constraints.maxWidth, constraints.maxHeight) {
// 자식들을 부모 레이아웃에 배치
placeables.forEach { placeable ->
// 화면에서의 아이템 위치를 지정하여 배치
placeable.placeRelative(x = 0, y = yPosition)
// 하위요소를 배치한 y위치를 추적하여 이후 자식 배치에 활용
yPosition += placeable.height
}
}
}
}

Column은 상단으로부터 수직적으로 자식 요소를 배치하는 레이아웃이다. layout Modifier에서 다뤘듯이 (0, 0)은 우측 최상단임을 확인했었다. 따라서 최초 자식이 배치될 y좌표는 0좌표임을 알 수 있고 각 자식 요소가 배치될 때마다 이전 자식뷰의 하단에 배치되어야 하므로 자식 요소가 배치될 y좌표를 기록해야 한다. 우선 0값을 저장하는 변수를 선언한 뒤 palceable.placeRelative(x, y) 함수를 호출하여 자식 요소들을 배치한다. 자식 요소의 위치를 지정한 뒤 이후 자식 요소 배치를 위해 yPosition 변수에 배치한 자식 요소의 높이를 더해준다. 해당 로직을 통해 이후 자식 요소는 이전 자식 요소 하단에 배치될 수 있다.

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
MyOwnColumn(modifier.padding(8.dp)) {
Text("MyOwnColumn")
Text("places items")
Text("vertically.")
Text("We've done it by hand!")
}
}

작성한 코드대로 상하좌우 8dp씩 padding이 부여되었고 하위 컴포저블 요소로 부여한 4개의 Text 컴포저블이 차례대로 수직으로 배치되었다.

Codelab_Jetpack_Compose_layout_025

참고자료

  1. Google Codelab - Jetpack Compose Layout #7 맞춤 레이아웃 만들기

읽기 전

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

Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. Compose에서 목록형 레이아웃에 아이템들을 담아 출력하는 LazyList 관련된 컴포저블에 대해 실습합니다. 이미지를 네트워크에 요청하여 렌더링해본 뒤 코루틴을 사용하여 스크롤 제어까지 다룹니다. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.

목록 사용

아이템 목록 출력은 앱 구성 시 많은 곳에서 사용된다. 아이템을 Row로 구성하고 Column에 쌓아서 리스팅을 구현할 수 있을 것이라 간단하게 예상할 수 있다. Jetpack Compose에서는 리스트 출력에 최적화된 Lazy List를 제공한다.

Column 컴포저블을 사용하여 100개의 목록을 출력하는 컴포저블 함수는 다음과 같이 작성할 수 있다.

@Composable
fun SimpleList() {
Column {
repeat(100) {
Text("Item #$it")
}
}
}

다만, Column 컴포저블은 기본적으로 스크롤 속성이 부여되지 않았기에 화면 밖 아이템을 표시할 수 없다. 스크롤이 가능하도록 verticalScroll Modifier를 추가하자.

@Composable
fun SimpleList() {
// 해당 state로 스크롤 위치를 저장할 수 있으며 코드로 리스트를 스크롤할 수 있게끔 해준다.
val scrollState = rememberScrollState()
Column(Modifier.verticalScroll(scrollState)) {
repeat(100) {
Text("Item #$it")
}
}
}

Lazy List

앞서 작성했듯이 Column 컴포저블을 사용하여 리스트를 출력하면 화면에 표시되지 않는 아이템들도 모두 한번에 렌더링하여 성능 이슈가 발생한다. 뷰에서의 RecyclerView처럼 화면에 등장할 때만 재사용하여 렌더링하는 컴포저블에서는 LazyColumn에 해당한다. LazyColumn은 Column과는 달리 scroll Modifier없이 즉시 사용할 수 있다.

@Composable
fun LazyList() {
val scrollState = rememberLazyListState()
LazyColumn(state = scrollState) {
items(100) {
Text("Item #$it")
}
}
}

Codelab_Jetpack_Compose_layout_019

이미지 출력

원격으로 이미지를 받아서 리스트에 출력해보자. Image 컴포저블을 사용하연 비트맵이나 벡터 이미지를 출력할 수 있다. Coil 라이브러리를 사용하여 Image에 이미지 리소스를 입력하게끔 활용하였다. Coil 라이브러리 사용을 위해 사전 작업이 필요하다.

Build.gradle에 라이브러리를 import 한다.

// build.gradle
implementation 'io.coil-kt:coil-compose:1.4.0'

네트워크를 이용하여 이미지 리소스를 요청할 예정으로 AndroidManifest에 권한을 추가한다.

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />

Row 컴포저블을 사용하여 출력할 아이템을 구성한다. 차례로 이미지, 구분 간격, 텍스트를 정의하였다.

@Composable
fun ImageListItem(index: Int) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberImagePainter(
data = "https://developer.android.com/images/brand/Android_Robot.png"
),
contentDescription = "Android Logo",
modifier = Modifier.size(50.dp)
)
Spacer(Modifier.width(10.dp))
Text("Item #$index", style = MaterialTheme.typography.subtitle1)
}
}

LazyColumn 컴포저블의 하위 컴포저블로 ImageListItem 컴포저블을 입력한다.

@Composable
fun ImageList() {
val scrollState = rememberLazyListState()
LazyColumn(state = scrollState) {
items(100) {
ImageListItem(it)
}
}
}

Codelab_Jetpack_Compose_layout_020

스크롤 제어

앞서 state를 사용하여 스크롤 위치를 기억하게끔 만들었다. 이걸 직접 코드를 작성하여 수동으로 제어할 수도 있다. 목록 상단과 하단으로 스크롤할 수 있도록 버튼 두 개를 작성한다. 스크롤이 수행되는 동안 중간의 리스트 렌더링 방지를 위해 스크롤 API는 suspend 함수로 구현되어 있다. 따라서, suspend 함수 호출을 위해 코루틴을 생성해야 한다. 컴포즈에서 사용할 수 있도록 rememberCoroutineScope 함수를 사용하여 CorotineScope를 생성한 뒤 해당 scope에서 동작을 수행하면 되겠다.

@Composable
fun ImageList() {
val listSize = 100 // 지정된 위치로 스크롤할 수 있도록 전체 아이템 개수 fix
// 스크롤 위치를 기억하는 state
val scrollState = rememberLazyListState()
// 스크롤링 액션이 실행될 수 있도록 coroutine scope 저장
val coroutineScope = rememberCoroutineScope()
Column {
Row {
Button(onClick = {
coroutineScope.launch {
// 0번째 원소가 첫번째 아이템
scrollState.animateScrollToItem(0)
}
}) {
Text("Scroll to the top")
}
Button(onClick = {
coroutineScope.launch {
// 0좌표 기준에 따라 listSize - 1는 마지막
scrollState.animateScrollToItem(listSize - 1)
}
}) {
Text("Scroll to the bottom")
}
}
LazyColumn(state = scrollState) {
items(listSize) {
ImageListItem(it)
}
}
}
}

Codelab_Jetpack_Compose_layout_021

Floating Button 사용

앞서 작성한 예제처럼 버튼을 고정적으로 작성하여 스크롤 제어를 구현하는 경우는 많지 않다. 대부분 Floating Button을 작성하여 클릭 시 top position으로 이동하게끔 작성한다. Scaffold를 사용하여 Floating Button을 작성한 뒤 스크롤 이벤트를 정의하였다.

@Composable
fun ImageList() {
val listSize = 100
// 스크롤 위치를 기억하는 state
val scrollState = rememberLazyListState()
// 스크롤링 액션이 실행될 수 있도록 coroutine scope 저장
val coroutineScope = rememberCoroutineScope()
Scaffold(
floatingActionButton = {
ExtendedFloatingActionButton(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
text = { Text("TOP") },
onClick = {
coroutineScope.launch {
// 0좌표는 첫번째 아이템 좌표
scrollState.animateScrollToItem(0)
}
}
)
}
) {
LazyColumn(state = scrollState) {
items(listSize) {
ImageListItem(it)
}
}
}
}

Codelab_Jetpack_Compose_layout_022

좀 더 고도화를 한다면 top position이 화면에 렌더링된 경우는 Floating Button을 숨겼다가 scroll이 하단으로 이동하여 top position이 화면에서 사라진 경우 Floating Button을 등장시키는 방법도 있겠다.

참고자료

  1. Google Codelab - Jetpack Compose Layout #6 목록 사용

읽기 전

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

Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. 앱의 디자인을 위해 Material Design Component 도입을 위한 Scaffold 레이아웃과 기본적인 사용법에 대해 다룹니다. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.

Material Component(머티리얼 구성요소)

앱을 구현하면서 디자인 측면을 넘어서 UI 컴포넌트까지 Material 라이브러리는 광범위하게 영향을 끼친다. 컴포즈에서는 특히 더욱 그러한데 그 중에서도 Scaffold는 가장 높은 수준의 컴포저블이다.

Scaffold

Scaffold를 사용하면 기본 머티리얼 디자인 레이아웃 구조로 UI를 구현할 수 있으며 페이지 구성 시 자주 쓰이는 TopAppBar, BottomAppBar, FloatingActionButton 등 최상위 머티리얼 컴포넌트를 위한 슬롯(공간)을 제공한다.

Scaffold 본문에 Text 컴포저블을 배치해보자.

@Composable
fun LayoutsCodelab() {
Scaffold { innerPadding ->
Text(text = "Hi there!", modifier = Modifier.padding(innerPadding))
}
}

Scaffold 컴포저블의 파라미터는 @Composable (InnerPadding) -> Unit의 본문 content를 제외하고 선택적으로 부여할 수 있다. 하위 람다 함수를 사용하여 본문 컴포저블을 정의할 수 있으며 매개변수로 패딩을 받는다. 해당 패딩은 화면의 항목을 적절히 제한하도록 컨텐츠 루트 컴포저블에 적용한다고 한다.

화면의 기본 컨텐츠를 Vertical하게 배치한다고 가정하고 Column 컴포저블 내부에 쌓아보자.

@Composable
fun LayoutsCodelab() {
Scaffold { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the Layouts codelab")
}
}
}

가급적이면 코드 재사용과 테스팅 편의를 위해 작은 단위로 코드를 구조화하면 좋다. 본문에 들어갈 컴포저블을 따로 컴포저블 함수로 구성하자.

@Composable
fun LayoutsCodelab() {
Scaffold { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the Layouts codelab")
}
}

TopAppBar

많은 앱은 상단 AppBar를 갖는다. Scaffold도 이를 위해 @Composable () -> Unit 유형의 topBar라는 파라미터를 사용하여 상단 AppBar 슬롯에 컴포저블을 배치할 수 있다. h3 스타일의 텍스트를 상단 앱바로 사용하려면 다음과 같이 작성하면 된다.

@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
Text(
text = "LayoutsCodelab",
style = MaterialTheme.typography.h3
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}

Codelab_Jetpack_Compose_layout_016

컴포저블 함수 작성 결과는 위 그림과 같다.

단순 텍스트로 상단 앱 바를 구성하기보다 title, 탐색 아이콘, 작업용 슬롯, navigation icon이 있는 TopAppBar 컴포저블을 사용할 수 있다. 기존 텍스트로만 구성한 상단 앱 바를 TopAppBar로 교체하고 title 슬롯에 Text 컴포저블을 정의해보자.

@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
}
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}

Codelab_Jetpack_Compose_layout_017

프로젝트에 적용된 테마에 맞춰서 상단 앱 바가 구성되었음을 확인할 수 있다. TopAppBar를 사용했으니 타이틀 말고도 다른 파라미터를 사용할 수 있다. 상단 앱 바에서 자주 보던 좌측 navigationIcon이나 우측 작업 버튼 등을 정의할 수 있다. 우측 작업 버튼을 배치해보자.

@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}

Codelab_Jetpack_Compose_layout_018

Preview로 컴포저블 함수를 출력한 결과 작업버튼이 잘 배치되었음을 확인할 수 있다.

추가 Modifier 배치

Modifier는 컴포저블의 padding, background 등 기본적인 모양새를 결정한다. 따라서, 새로운 컴포저블 생성 시 기본값이 Modifier로 지정된 modifier 매개변수를 정의해두면 컴포저블 함수의 재사용성이 좋아진다. 앞서 작성했던 BodyContent 컴포저블 함수에 파라미터로 Modifier를 정의하여 하위 요소들을 감싸는 Column 컴포저블의 modifier 파라미터로 입력했었다. 패딩을 Column 컴포저블에 더 추가하고 싶다면 두 가지로 정리할 수 있다.

  1. BodyContent 컴포저블 함수가 호출될 때마다 추가 패딩이 적용되도록 modifier padding을 하위 요소에 적용한다.
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(8.dp)) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the Layouts codelab")
}
}

Modifier가 컴포저블의 고유한 속성이라면 1번 방법처럼 함수 내부에 선언한다.

  1. 추가 padding이 필요할 때만 컴포저블 함수 선언 시 Modifier 적용
@Composable
fun LayoutsCodelab() {
Scaffold(...) { innerPadding ->
BodyContent(Modifier.padding(innerPadding).padding(8.dp))
}
}

Modifier가 컴포저블의 고유한 속성이 아니라 주입해야 한다면 외부로부터 입력받도록 작성한다.

참고자료

  1. Google Codelab - Jetpack Compose Layout #5 머티리얼 구성요소

읽기 전

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

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

읽기 전

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

Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. 기본적인 Modifier 적용과 순서에 따른 동작 차이점에 대해 다룹니다. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.

프로젝트 시작

실습을 위해 새로운 안드로이드 프로젝트를 생성한 뒤 Empty Compose Activity를 선택하여 생성한다. Compose API의 최소 호환 버전을 맞추기 위해 minimumSdkVersion을 API Level 21 이상으로 설정해야 한다.

Codelab_Jetpack_Compose_layout_001

Codelab_Jetpack_Compose_layout_002

Modifier(수정자)

Codelab_Jetpack_Compose_layout_003

Modifier 섹션에서는 위의 그림대로 컴포저블을 구성해본다.

Modifier를 사용하여 컴포저블을 가공할 수 있다. 라벨 추가, 클릭 및 스크롤 부여, 동작 및 모양 등을 정의할 수 있다. 또한, Modifier는 코틀린 객체로 변수에 할당 및 재사용이 가능하며 여러 수정자를 체이닝하여 구성할 수 있으며 확장함수를 정의할 수도 있다.

@Composable
fun PhotographerCard() {
Column {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
// LocalContentAlpha는 자신의 scope 하단의 자식 컴포즈의 opacity level을 정의한다.
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
@Preview
@Composable
fun PhotographerCardPreview() {
LayoutsCodelabTheme {
PhotographerCard()
}
}

Column 컴포저블을 사용하여 수직 레이아웃을 선언한 뒤 상단에 Text 컴포저블을 선언하고 하단에 CompositionLocalProvider를 사용하여 내부 scope에 Text 컴포저블을 선언한 뒤 스타일을 부여하였다. Preview Annotation을 선언한 Preview용 컴포저블 함수를 선언하여 정의한 레이아웃이 어떤 모습을 갖는지 확인할 수 있다.

Codelab_Jetpack_Compose_layout_004

텍스트 구성은 마무리되었다. 이제 사진사 사진을 로드하기 전까지의 공간을 표시하기 위해 placeholder를 정의해야 한다.

@Composable
fun PhotographerCard() {
Row {
Surface(
modifier = Modifier.size(50.dp),
shape = CircleShape,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
) {
// Image goes here
}
Column {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
}

사진사의 사진과 텍스트는 서로 가로로 쌓이는 모양새이므로 바깥에 Row를 선언한 뒤 사진사 사진을 담을 Surface 컴포저블을 선언한다. 이후 두 텍스트 컴포저블이 담긴 Column 컴포저블이 선언되었다.

Codelab_Jetpack_Compose_layout_005

필자의 안드로이드 스튜디오의 모드가 다크모드라 좌측 placeholder의 색상이 가려졌다. 라이트모드에서 보면 확실히 회색 원형 컴포저블이 생성되었음을 확인할 수 있다.

Codelab_Jetpack_Compose_layout_006

섹션 처음에 생성하려는 컴포저블과 비교하여 두 가지 개선할 점이 보인다.

  1. placeholder와 텍스트 사이의 간격이 필요하다.

우측 두 개의 텍스트 컴포저블을 담은 Column 컴포저블과 placeholder 컴포저블 간 간격을 띄워야 하므로 Column 컴포저블의 파라미터에 Modifier를 부여할 수 있다. Modifier.padding(start = 8.dp)를 부여하자.

  1. 사진사의 설명이 적힌 텍스트 컴포저블이 세로로 가운데 위치해야 한다.

우측 Column 컴포저블에 속한 텍스트 컴포저블들이 수직 중앙 정렬되었으면 한다. 그렇다면 Column 컴포저블의 scope에 속한 컴포저블에 대해 정렬 속성을 부여하자. 역시나 Modifier 파라미터에 속성을 부여해야 한다. .align(Alignment.CenterVertically)를 부여하자.

@Composable
fun PhotographerCard() {
Row {
Surface(
modifier = Modifier.size(50.dp),
shape = CircleShape,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
) {
// Image goes here
}
Column(
modifier = Modifier
.padding(start = 8.dp)
.align(Alignment.CenterVertically)
) {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
}

Codelab_Jetpack_Compose_layout_007

좌측 placeholder와 텍스트 간의 간격이 생겼고 텍스트 또한 수직 중앙정렬된 모습이다. 이렇듯 Modifier를 사용하여 컴포저블을 다양하게 조정할 수 있다. 따라서, 커스텀 컴포저블을 만들 때 파라미터로 Modifier를 받게끔 하여 외부에서 Modifier를 주입할 수 있도록 작성하자.

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier) { ... }
}

주의할 점으로는 위 코드대로 파라미터에 Modifier를 작성하여 Row 컴포저블을 조정할 수 있는 옵션을 부여하였는데 내부에 선언할 컴포저블에 대해서도 스타일을 조정할 필요가 있는지 염두에 둬야 한다. 굳이 Modifier를 부여할 필요가 없다면 스타일 파라미터를 작성하여 개별적으로 부여할 수 있을 것이다.

@Composable
fun PhotographerCard(
modifier: Modifier = Modifier,
fontColor: Color = Color.Black,
content: String = "") {
Row(modifier) {
Text(text = content, color = fontColor)
}
}

Modifier 순서의 중요성

위에서 작성한 Modifier.padding(start = 8.dp).align(Alignment.CenterVertically처럼 Modifier를 적용할 때는 확장함수들을 적용하여 체이닝한다. 단일 객체로 연결하여 사용하기에 순서에 따라 최종 결과가 달라진다. 사진가 프로필에 클릭 속성을 부여하면서 패딩도 적용해보자.

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier
.padding(16.dp)
.clickable(onClick = { /* Ignoring onClick */ })
) {
...
}
}

전체 상하좌우 패딩을 16.dp씩 부여하고 클릭 속성을 부여하였다. 이에 대해 클릭을 수행하면 아래와 같이 동작한다.

Codelab_Jetpack_Compose_layout_008

그림을 보면 모든 영역에 클릭이 적용되지 않는다. 그 이유는 padding이 clickable보다 앞에 적용되었기 때문이다. 만약 순서를 바꿔서 padding을 clickable modifier 뒤에 적용하면 패딩 영역도 클릭할 수 있다.

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier
.clickable(onClick = { /* Ignoring onClick */ })
.padding(16.dp)
) {
...
}
}

Codelab_Jetpack_Compose_layout_009

Modifier를 사용하여 좀 더 그럴듯하게 꾸며보자. 전체 영역에 대해 paddingdmf 8.dp를 부여하고 4.dp만큼 round corner를 부여한다. 그리고 배경은 MaterialTheme에 정의된 기본 surface 색상이고 클릭 속성을 부여하며 내부 컨텐츠에 대해 16.dp padding을 또 부여한다.

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.surface)
.clickable(onClick = { /* Ignoring onClick */ })
.padding(16.dp)
) {
...
}
}

Codelab_Jetpack_Compose_layout_010

전체 영역에 대해 바깥 8.dp는 클릭속성이 부여되어 있지 않아 클릭할 수 없다. 역시 round corner를 clickable보다 먼저 부여했기에 클릭 시 음영 처리되는 영역의 모서리도 둥그렇게 처리되었다. 클릭 속성 부여 이후 padding을 16.dp만큼 부여하였고 해당 영역은 음영처리됨을 확인할 수 있다.

참고자료

  1. Google Codelab - Jetpack Compose Layout #2 수정자

+ Recent posts