Compose의 기본 레이아웃

1. 소개

UI 도구 키트인 Compose를 사용하면 앱 디자인을 쉽게 구현할 수 있습니다. 개발자가 UI의 디자인을 기술하면 Compose가 화면에 그리는 작업을 처리합니다. 이 Codelab에서는 Compose UI를 작성하는 방법을 알아봅니다. 이 Codelab은 기본 Codelab에서 배운 개념을 이해했다고 가정하고 진행되니 먼저 기본 Codelab을 완료해야 합니다. 기본 Codelab에서는 Surfaces, Rows, Columns를 사용하여 간단한 레이아웃을 구현하는 방법을 배웠습니다. 또한 padding, fillMaxWidth, size와 같은 수정자를 사용하여 레이아웃을 보강했습니다.

이 Codelab에서는 보다 현실적이고 복잡한 레이아웃을 구현해 보고 여러 즉시 사용 가능한 컴포저블수정자에 관해 알아봅니다. 이 Codelab을 마치면 기본적인 앱의 디자인을 실제로 작동하는 코드로 변환할 수 있게 됩니다.

이 Codelab에서는 앱에 동작을 추가하지 않습니다. 상태 및 상호작용에 관해 알아보려면 Compose에서의 상태 Codelab을 수강하세요.

이 Codelab을 진행하는 동안 추가 지원을 받으려면 다음 코드를 함께 체크아웃하세요.

학습할 내용

이 Codelab에서는 다음에 관해 알아봅니다.

  • 수정자를 사용하여 컴포저블을 보강하는 방법
  • Column 및 LazyRow와 같은 표준 레이아웃 구성요소로 하위 컴포저블을 배치하는 방법
  • 정렬과 배치로 상위 요소 내에서 하위 컴포저블의 위치를 변경하는 방법
  • Scaffold 및 하단 탐색과 같은 Material 컴포저블을 사용하여 포괄적인 레이아웃을 만드는 방법
  • 슬롯 API를 사용하여 유연한 컴포저블을 빌드하는 방법

필요한 항목

빌드할 항목

이 Codelab에서는 디자이너가 제공한 모의 버전을 기반으로 현실적인 앱 디자인을 구현합니다. MySoothe는 몸과 마음을 보살피는 다양한 방법을 소개하는 웰빙 앱입니다. 이 앱에는 즐겨찾는 컬렉션이 나열된 섹션과 신체 운동이 안내된 섹션이 있습니다. 앱의 모습은 다음과 같습니다.

24ff9efa75f22198.png

2. 설정

이 단계에서는 테마 설정과 기본 설정이 포함된 코드를 다운로드합니다.

코드 가져오기

이 Codelab의 코드는 android-compose-codelabs GitHub 저장소에서 찾을 수 있습니다. 클론하려면 다음을 실행합니다.

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

또는 ZIP 파일 두 개를 다운로드해도 됩니다.

코드 체크아웃

다운로드한 코드에는 사용 가능한 모든 Compose Codelab용 코드가 포함되어 있습니다. 이 Codelab을 완료하려면 Android 스튜디오 내에서 BasicLayoutsCodelab 프로젝트를 엽니다.

main 브랜치의 코드로 시작하고 각자의 속도에 맞게 Codelab을 단계별로 따라하는 것이 좋습니다.

3. 계획 세우기

디자인을 자세히 살펴보겠습니다.

c31e78e48cc1f336.png

디자인을 구현하라는 요청을 받았을 때 먼저 디자인의 구조를 명확하게 파악하는 것이 좋습니다. 곧바로 코딩을 시작하는 대신 디자인을 분석해 보세요. 이 UI를 여러 개의 재사용 가능한 부분으로 나누려면 어떻게 해야 할까요?

Codelab에서 사용할 디자인을 살펴보겠습니다. 가장 높은 추상화 수준에서는 이 디자인을 두 부분으로 나눌 수 있습니다.

  • 화면의 콘텐츠
  • 하단 탐색

9a0f4be94a5a206c.png

화면 콘텐츠는 다음과 같은 세 부분으로 구성됩니다.

  • 검색창
  • '신체의 조화' 섹션
  • '즐겨찾는 컬렉션' 섹션

d9bf2ca5a0939959.png

각 섹션 내에서 다음과 같은 재사용되는 하위 수준 구성요소도 볼 수 있습니다.

  • 가로로 스크롤되는 행에 표시된 '신체의 조화' 요소

29bed1f813622dc.png

  • 가로로 스크롤되는 그리드에 표시된 '즐겨찾는 컬렉션' 카드

cf1fe8b2d682bfca.png

디자인을 분석해 보았으니 이제 UI에서 식별한 모든 부분의 컴포저블 구현을 시작할 수 있습니다. 가장 낮은 수준의 컴포저블부터 시작한 다음 이를 조합하여 복잡한 컴포저블을 구현해 보겠습니다. 이 Codelab을 마치면 앱은 위와 같은 디자인을 갖게 됩니다.

4. 검색창 - 수정자

컴포저블로 변환할 첫 번째 요소는 검색창입니다. 디자인을 다시 한번 살펴보겠습니다.

6b7c2f913d189b9a.png

이 스크린샷만 봐서는 픽셀 단위로 완벽한 디자인을 구현하기가 어려울 수 있습니다. 디자이너는 보통 디자인에 관해 이보다는 많은 정보를 알려줍니다. 개발자에게 디자인 도구를 제공하거나 소위 말하는 '레드라인' 디자인을 공유할 수 있습니다. 우리의 경우에는 디자이너가 레드라인 디자인을 공유해 주었는데요. 여러분은 여기에서 크기 값을 확인할 수 있습니다. 이 디자인은 8dp 그리드 오버레이로 표현되어 있으므로 요소와 요소 사이의 간격과 주변의 간격을 쉽게 확인할 수 있습니다. 여기에 더해 특정 크기를 명확하게 알 수 있도록 명시된 간격도 있습니다.

6c6854661a89e995.png

검색창의 높이는 56dp(밀도 독립형 픽셀)인 것을 볼 수 있습니다. 검색창의 너비는 상위 요소의 전체 너비를 차지합니다.

검색창을 구현하려면 텍스트 필드라는 Material 구성요소를 사용합니다. Compose Material 라이브러리에는 이 Material 구성요소의 구현인 TextField라는 컴포저블이 있습니다.

기본적인 TextField 구현부터 시작하겠습니다. 코드베이스에서 MainActivity.kt를 열고 SearchBar 컴포저블을 검색합니다.

SearchBar라는 컴포저블 내에서 기본적인 TextField 구현을 작성합니다.

import androidx.compose.material.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
   )
}

참고사항:

  • 텍스트 필드 값은 하드코딩했고, onValueChange 콜백은 아무런 작업도 하지 않습니다. 레이아웃을 집중적으로 다루는 Codelab이므로 상태와 관련된 모든 내용은 무시합니다.
  • SearchBar 구성 가능한 함수는 modifier 매개변수를 받아서 TextField에 전달합니다. 이 방식은 Compose 가이드라인에 따른 권장사항입니다. 이 방식을 사용하면 메서드의 호출자가 컴포저블의 디자인과 분위기를 수정할 수 있어 유연성이 높아지고 재사용이 가능하게 됩니다. 이 Codelab의 모든 컴포저블에 권장사항을 적용합니다.

이 컴포저블의 미리보기를 살펴보겠습니다. Android 스튜디오의 Preview 기능을 사용하여 코드를 수정한 후 매번 개별 컴포저블을 빠르게 확인할 수 있습니다. MainActivity.kt에는 이 Codelab에서 빌드할 모든 컴포저블의 미리보기가 들어 있습니다. 여기서는 SearchBarPreview 메서드가 배경색과 패딩을 사용하여 SearchBar 컴포저블을 렌더링합니다. 방금 추가한 구현이 적용된 컴포저블은 다음과 같습니다.

c2e1eec30f36bc72.png

아직 빠진 것들이 있습니다. 우선 수정자를 사용하여 컴포저블의 크기를 수정하겠습니다.

컴포저블을 작성할 때는 수정자를 사용하여 다음을 합니다.

  • 컴포저블의 크기, 레이아웃, 동작, 모양 변경
  • 접근성 라벨과 같은 정보 추가
  • 사용자 입력 처리
  • 요소를 클릭 가능, 스크롤 가능, 드래그 가능 또는 확대/축소 가능하게 만드는 것과 같은 높은 수준의 상호작용 추가

개발자가 호출하는 각 컴포저블에는 modifier 매개변수가 있습니다. 이 매개변수를 설정하여 컴포저블의 디자인, 느낌, 동작을 조정할 수 있습니다. 수정자를 설정할 때는 여러 수정자 메서드를 연결하여 보다 복잡한 조정을 적용할 수 있습니다.

검색창은 높이가 최소 56dp이고 너비가 상위 요소의 전체 너비를 차지합니다. 이를 위한 올바른 수정자를 찾으려면 수정자 목록에서 크기 섹션을 살펴보세요. 높이로는 heightIn 수정자를 사용할 수 있습니다. 이렇게 하면 컴포저블에 특정 최소 높이가 지정됩니다. 단, 사용자가 시스템 글꼴 크기를 확대하면 크기가 커질 수 있습니다. 너비로는 fillMaxWidth 수정자를 사용할 수 있습니다. 이 수정자는 검색창이 상위 요소의 전체 가로 공간을 차지하도록 합니다.

아래 코드와 일치하도록 수정자를 업데이트합니다.

import androidx.compose.material.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

여기서는 하나의 수정자는 너비에 영향을 주고 다른 수정자는 높이에 영향을 주기 때문에 수정자의 순서는 중요하지 않습니다.

TextField의 몇 가지 매개변수도 설정해야 합니다. 매개변수 값을 설정하여 컴포저블을 가급적 디자인과 똑같이 만들어 보세요. 아래의 디자인을 참고하세요.

6b7c2f913d189b9a.png

구현을 업데이트하려면 다음 단계를 따라야 합니다.

  • 검색 아이콘을 추가합니다. TextField에는 다른 컴포저블을 받는 매개변수 leadingIcon이 있습니다. 내부에는 Icon을 설정할 수 있습니다. 여기서는 Search 아이콘을 설정합니다. 올바른 Compose Icon import를 사용하세요.
  • 텍스트 필드의 배경색을 MaterialTheme의 surface 색으로 설정합니다. TextFieldDefaults.textFieldColors를 사용하여 특정 색상을 재정의할 수 있습니다.
  • 자리표시자 텍스트 'Search'를 추가합니다(문자열 리소스 R.string.placeholder_search).

작업을 마치면 컴포저블이 다음과 같아집니다.

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.ui.res.stringResource
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       leadingIcon = {
           Icon(
               imageVector = Icons.Default.Search,
               contentDescription = null
           )
       },
       colors = TextFieldDefaults.textFieldColors(
           backgroundColor = MaterialTheme.colors.surface
       ),
       placeholder = {
           Text(stringResource(R.string.placeholder_search))
       },
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

짚고 넘어갈 사항:

  • 검색 아이콘을 표시하는 leadingIcon을 추가했습니다. 텍스트 필드의 자리표시자가 이미 텍스트 필드의 의미를 설명하므로 이 아이콘에는 콘텐츠 설명이 필요하지 않습니다. 콘텐츠 설명은 주로 접근성을 위해 사용되어 앱 사용자에게 이미지 또는 아이콘의 텍스트 표현을 제공합니다.
  • 텍스트 필드의 배경색을 조정하기 위해 colors 속성을 설정했습니다. 컴포저블은 색상별로 하나의 매개변수를 사용하는 대신 하나의 결합된 매개변수를 갖습니다. 여기서는 TextFieldDefaults 색상 클래스의 사본을 전달하여 차이가 나는 색상만 업데이트합니다. 차이가 나는 색상은 배경색 하나였습니다.
  • 고정 높이가 아닌 최소 높이를 설정했습니다. 이렇게 하면 사용자가 시스템 설정에서 글꼴 크기를 늘릴 경우 텍스트 필드의 크기가 커질 수 있게 되므로 권장되는 방식입니다.

이 단계에서는 컴포저블 매개변수와 수정자를 사용하여 컴포저블의 디자인과 분위기를 변경하는 방법을 알아보았습니다. 이 방식은 Compose와 라이브러리와 Material 라이브러리에서 제공하는 컴포저블과 개발자가 직접 작성하는 컴포저블에 모두 적용됩니다. 직접 작성하는 컴포저블을 맞춤설정할 매개변수를 어떻게 제공하면 좋을지 생각해야 합니다. 또한 컴포저블의 디자인과 분위기를 외부에서 조정할 수 있도록 modifier 속성을 추가해야 합니다.

5. 신체의 조화 - 정렬

다음으로 구현할 컴포저블은 '신체의 조화' 요소입니다. 전체적인 디자인과 그 옆의 레드라인 디자인을 살펴보겠습니다.

29bed1f813622dc.png 9d11e16a8817686f.png

이 레드라인 디자인에는 기준선을 중심으로 하는 간격도 기재되어 있습니다. 여기서 알 수 있는 정보는 다음과 같습니다.

  • 이미지의 높이는 88dp입니다.
  • 텍스트의 기준선과 이미지의 기준선 사이의 간격은 24dp입니다.
  • 기준선과 요소 하단 사이의 간격은 8dp입니다.
  • 텍스트의 서체 스타일은 H3입니다.

이 컴포저블을 구현하려면 ImageText 컴포저블이 필요합니다. 이 두 가지 컴포저블을 세로 방향으로 배치하려면 Column에 포함해야 합니다.

코드에서 AlignYourBodyElement 컴포저블을 찾아서 다음과 같은 기본 구현으로 콘텐츠를 업데이트합니다.

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.res.painterResource

@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null
       )
       Text(
           text = stringResource(R.string.ab1_inversions)
       )
   }
}

짚고 넘어갈 사항:

  • 이 이미지는 순전히 장식용이므로 contentDescription을 null로 설정했습니다. 이미지 아래의 텍스트만으로 의미가 충분히 전달되기 때문에 이미지에 추가 설명이 필요하지 않습니다.
  • 하드 코딩된 이미지와 텍스트를 사용했습니다. 다음 단계에서는 컴포저블을 동적으로 만들기 위해 AlightYourBodyElement 컴포저블에 제공된 매개변수를 사용하도록 이동해 봅니다.

이 컴포저블의 미리보기를 살펴보세요.

b9686f83eb73c542.png

몇 가지 개선할 점이 있습니다. 무엇보다도, 이미지가 너무 크고 원형이 아닙니다. sizeclip 수정자와 contentScale 매개변수를 사용하여 Image 컴포저블을 조정할 수 있습니다.

size 수정자는 앞 단계에서 본 fillMaxWidthheightIn 수정자와 마찬가지로 특정 크기에 맞게 컴포저블을 조정합니다. clip 수정자는 이와 달리 컴포저블의 모양을 조정합니다. 이 수정자를 원하는 Shape으로 설정하면 컴포저블의 콘텐츠가 이 도형에 맞춰 잘립니다.

이미지의 크기도 올바르게 조정해야 합니다. ImagecontentScale 매개변수를 사용하면 됩니다. 여러 가지 옵션이 있는데, 그 중에서 다음과 같은 옵션을 살펴보세요.

5f17f07fcd0f1dc.png

여기서는 Crop 유형을 사용하는 것이 가장 적합합니다. 수정자와 매개변수를 적용한 코드는 다음과 같습니다.

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(R.string.ab1_inversions)
       )
   }
}

디자인은 다음과 같습니다.

6576ed1e8b1cde30.png

다음으로, Column의 정렬을 설정하여 텍스트를 가로로 정렬합니다.

일반적으로, 상위 컨테이너 내부에서 컴포저블을 정렬하려면 상위 컨테이너의 alignment를 설정합니다. 즉, 하위 요소에 상위 요소 배부에 배치되도록 지시하는 대신 상위 요소에 하위 요소를 정렬하는 방법을 지시합니다.

Column의 경우 하위 요소를 가로로 정렬할 방법을 정해야 합니다. 옵션은 다음과 같습니다.

  • Start
  • CenterHorizontally
  • End

Row의 경우 세로 정렬을 설정해야 합니다. 옵션은 Column의 옵션과 유사합니다.

  • Top
  • CenterVertically
  • Bottom

Box의 경우 가로 및 세로 정렬을 결합하여 사용합니다. 옵션은 다음과 같습니다.

  • TopStart
  • TopCenter
  • TopEnd
  • CenterStart
  • Center
  • CenterEnd
  • BottomStart
  • BottomCenter
  • BottomEnd

컨테이너의 모든 하위 요소가 동일한 정렬 패턴을 따릅니다. align 수정자를 추가하여 단일 하위 요소의 동작을 재정의할 수 있습니다.

이 디자인에서는 텍스트가 가로로 가운데 정렬되어야 합니다. 이렇게 하려면 ColumnhorizontalAlignment가 가로로 가운데 정렬되도록 설정합니다.

import androidx.compose.ui.Alignment
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       horizontalAlignment = Alignment.CenterHorizontally,
       modifier = modifier
   ) {
       Image(
           //..
       )
       Text(
           //..
       )
   }
}

여기까지 구현했다면 이제 컴포저블을 디자인과 동일하게 만들기 위해 몇 가지 변경사항만 적용하면 됩니다. 직접 구현해 보세요. 문제가 발생하면 최종 코드를 참고하세요. 다음과 같은 단계를 참고해 보세요.

  • 이미지와 텍스트를 동적으로 만듭니다. 구성 가능한 함수에 인수로 전달합니다. 상응하는 미리보기를 업데이트하고 하드 코딩된 데이터를 전달하는 것도 잊지 마세요.
  • 올바른 서체 스타일을 사용하도록 텍스트를 업데이트합니다.
  • 텍스트 요소의 기준선 간격을 업데이트합니다.

위 단계의 구현을 마치면 코드가 다음과 같아집니다.

import androidx.compose.foundation.layout.paddingFromBaseline

@Composable
fun AlignYourBodyElement(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Image(
           painter = painterResource(drawable),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(text),
           style = MaterialTheme.typography.h3,
           modifier = Modifier.paddingFromBaseline(
               top = 24.dp, bottom = 8.dp
           )
       )
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun AlignYourBodyElementPreview() {
   MySootheTheme {
       AlignYourBodyElement(
           text = R.string.ab1_inversions,
           drawable = R.drawable.ab1_inversions,
           modifier = Modifier.padding(8.dp)
       )
   }
}

6. 즐겨찾는 컬렉션 카드 - Material Surface

다음으로 구현할 컴포저블은 '신체의 조화' 요소와 비슷합니다. 전체적인 디자인과 그 아래의 레드라인 디자인을 살펴보겠습니다.

71fcfc487ef8c02a.png

f2f4fb696389ba4f.png

컴포저블의 전체 크기가 나와 있습니다. 텍스트의 서체는 이번에도 H3입니다.

이 컨테이너에는 화면의 배경과 다른 배경색이 적용되어 있습니다. 모서리는 둥글게 처리되었습니다. 디자이너가 색상을 지정하지 않았으므로 색상이 테마에 의해 정의될 것이라고 가정할 수 있습니다. 이러한 컨테이너로는 Material의 Surface 컴포저블을 사용합니다.

Surface는 매개변수와 수정자를 설정하여 필요에 맞게 조정할 수 있습니다. 여기서는 모서리를 둥글게 처리해야 합니다. 이를 위해 shape 매개변수를 사용할 수 있습니다. 앞 단계의 이미지처럼 모양을 Shape로 설정하는 대신, Material 테마의 값을 사용합니다.

다음 코드를 살펴보세요.

import androidx.compose.foundation.layout.Row
import androidx.compose.material.Surface

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.small,
       modifier = modifier
   ) {
       Row {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null
           )
           Text(
               text = stringResource(R.string.fc2_nature_meditations)
           )
       }
   }
}

이번에는 이 구현의 미리보기를 살펴보세요.

5584e459f9838f8b.png

다음으로, 앞 단계에서 배운 내용을 적용합니다. 이미지 크기를 설정하고 컨테이너 내부에서 자릅니다. Row의 너비를 설정하고 하위 요소를 세로로 정렬합니다. 솔루션 코드를 보기 전에 변경사항을 직접 구현해 보세요.

이제 코드가 다음과 같아집니다.

import androidx.compose.foundation.layout.width

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.small,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(192.dp)
       ) {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(56.dp)
           )
           Text(
               text = stringResource(R.string.fc2_nature_meditations)
           )
       }
   }
}

미리보기는 다음과 같습니다.

e0afeb1658a6d82a.png

이 컴포저블을 완성하려면 다음 단계를 구현합니다.

  • 이미지와 텍스트를 동적으로 만듭니다. 구성 가능한 함수에 인수로 전달합니다.
  • 올바른 서체 스타일을 사용하도록 텍스트를 업데이트합니다.
  • 이미지와 텍스트 사이의 간격을 업데이트합니다.

최종 결과는 다음과 같습니다.

@Composable
fun FavoriteCollectionCard(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.small,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(192.dp)
       ) {
           Image(
               painter = painterResource(drawable),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(56.dp)
           )
           Text(
               text = stringResource(text),
               style = MaterialTheme.typography.h3,
               modifier = Modifier.padding(horizontal = 16.dp)
           )
       }
   }
}

//..

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun FavoriteCollectionCardPreview() {
   MySootheTheme {
       FavoriteCollectionCard(
           text = R.string.fc2_nature_meditations,
           drawable = R.drawable.fc2_nature_meditations,
           modifier = Modifier.padding(8.dp)
       )
   }
}

7. 본문 행 정렬 - 배치

화면에 표시되는 기본 컴포저블을 만들었으니 이제 화면의 여러 섹션을 만들 차례입니다.

스크롤 가능한 '신체의 조화' '행부터 시작하겠습니다.

25089e1f3e5eab4e.gif

이 구성요소의 레드라인 디자인은 다음과 같습니다.

9d943fabcb5ae632.png

그리드의 한 블록은 8dp입니다. 따라서 이 디자인에는 첫 번째 항목 앞과 마지막 항목 뒤에 16dp의 간격이 있습니다. 각 항목 사이에 간격은 8dp입니다.

Compose에서는 LazyRow 컴포저블을 사용하여 다음과 같이 스크롤 가능한 행을 구현할 수 있습니다. 목록 문서에서 LazyRowLazyColumn과 같은 Lazy 목록에 관한 훨씬 자세한 정보를 확인할 수 있습니다. 이 Codelab에서는 LazyRow는 모든 요소를 동시에 렌더링하는 대신 화면에 표시되는 요소만 렌더링하여 앱의 성능을 유지한다는 사실만 알면 됩니다.

LazyRow의 기본 구현부터 시작하겠습니다.

import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

LazyRow의 하위 요소는 컴포저블이 아닙니다. 대신 컴포저블을 목록 항목으로 내보내는 itemitems와 같은 메서드를 제공하는 Lazy 목록 DSL을 사용합니다. 제공된 alignYourBodyData의 각 항목에서, 앞에서 구현한 AlignYourBodyElement 컴포저블을 내보냅니다.

디자인은 다음과 같습니다.

b88f30efe9067f53.png

레드라인 디자인에서 보았던 간격은 적용되어 있지 않습니다. 간격을 구현하려면 배치에 대해 알아야 합니다.

앞 단계에서는 컨테이너의 하위 요소를 교차 축 위에서 정렬하는 데 사용되는 정렬에 대해 알아보았습니다. Column의 경우 교차축은 가로축이고, Row의 경우 교차축은 세로축입니다.

컨테이너의 기본 축(Row의 경우 가로축, Column의 경우 세로축) 위에 하위 컴포저블을 배치할 방식도 정할 수 있습니다.

Row의 경우 다음과 같은 배치를 선택할 수 있습니다.

c1e6c40e30136af2.gif

Column의 경우 다음과 같은 배치를 선택할 수 있습니다.

df69881d07b064d0.gif

이러한 배치 외에도 Arrangement.spacedBy() 메서드를 사용하여 각 하위 컴포저블 사이에 고정된 공간을 추가할 수 있습니다.

이 예에서는 LazyRow의 각 항목 사이에 8dp의 간격을 추가해야 하므로 spacedBy 메서드를 사용합니다.

import androidx.compose.foundation.layout.Arrangement

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

이제 디자인은 다음과 같습니다.

c29a8ee73f218868.png

LazyRow의 측면에도 패딩을 추가해야 합니다. 여기서는 간단한 패딩 수정자를 추가하는 것만으로는 원하는 결과를 얻을 수 없습니다. LazyRow에 패딩을 추가하고 결과가 어떻게 나오는지 살펴보세요.

6b3f390040e2b7fd.gif

스크롤하면 처음 표시되는 항목과 마지막으로 표시되는 항목이 화면 양쪽에서 잘리는 것을 볼 수 있습니다.

동일한 패딩을 유지하되 상위 목록의 경계 내에서 콘텐츠를 자르지 않고 스크롤할 수 있도록, 모든 목록에서 contentPadding이라는 매개변수를 제공합니다.

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       contentPadding = PaddingValues(horizontal = 16.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

8. 즐겨찾는 컬렉션 그리드 - Lazy 그리드

다음으로 구현할 섹션은 화면의 '즐겨찾는 컬렉션' 부분입니다. 이 컴포저블에는 단일 행이 아닌 그리드가 필요합니다.

4378867d758590ae.gif

이 섹션은 LazyRow를 만들고 각 항목이 두 개의 FavoriteCollectionCard 인스턴스를 갖는 Column을 갖도록 하여 앞 섹션과 비슷하게 구현할 수 있습니다. 단, 여기서는 항목-그리드 요소 매핑을 더 효과적으로 지원하는 LazyHorizontalGrid를 사용하겠습니다.

두 개의 고정 행이 있는 그리드를 간단하게 구현하는 것으로 시작해 보겠습니다.

import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       modifier = modifier
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text)
       }
   }
}

앞 단계의 LazyRowLazyHorizontalGrid로 바꾸기만 했습니다.

하지만 이렇게 해도 아직 올바른 결과가 나오지 않습니다.

e51beb5c00457902.png

그리드가 상위 요소의 전체 공간을 자치합니다. 즉, 즐겨찾는 컬렉션 카드가 세로 방향으로 지나치게 늘어져 있습니다. 그리드 셀이 올바른 크기와 셀 간 간격을 갖도록 컴포저블을 조정합니다.

결과는 다음과 같습니다.

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       contentPadding = PaddingValues(horizontal = 16.dp),
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       verticalArrangement = Arrangement.spacedBy(8.dp),
       modifier = modifier.height(120.dp)
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(
               drawable = item.drawable,
               text = item.text,
               modifier = Modifier.height(56.dp)
           )
       }
   }
}

9. 홈 섹션 - 슬롯 API

MySoothe 홈 화면에는 동일한 패턴을 따르는 여러 개의 섹션이 있습니다. 각각 제목이 있으며 섹션에 따라 다른 콘텐츠가 표시됩니다. 우리가 구현하려는 디자인은 다음과 같습니다.

2c0bc456d50bb078.png

각 섹션에는 제목슬롯이 있습니다. 제목에는 간격 및 스타일 정보가 있습니다. 섹션은 섹션에 따라 달라지는 콘텐츠로 동적으로 채울 수 있습니다.

이 유연한 섹션 컨테이너를 구현하려면 슬롯 API를 사용합니다. 컨테이너를 구현하기 전에 문서 페이지에서 슬롯 기반 레이아웃에 관한 섹션을 읽어 보세요. 슬롯 기반 레이아웃이 무엇이고 슬롯 API를 사용하여 이러한 레이아웃을 빌드하는 방법을 알아볼 수 있습니다.

제목과 슬롯 콘텐츠를 받도록 HomeSection 컴포저블을 조정합니다. 이 HomeSection을 '신체의 조화' 제목 및 콘텐츠와 함께 호출하도록 연결된 미리보기도 조정해야 합니다.

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(stringResource(title))
       content()
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun HomeSectionPreview() {
   MySootheTheme {
       HomeSection(R.string.align_your_body) {
           AlignYourBodyRow()
       }
   }
}

컴포저블의 슬롯으로 content 매개변수를 사용할 수 있습니다. 이렇게 하면 HomeSection 컴포저블을 사용할 때 후행 람다를 사용하여 콘텐츠 슬롯을 채울 수 있습니다. 컴포저블이 채울 수 있는 슬롯을 여러 개 제공한다면 더 큰 컴포저블 컨테이너에서 각각의 기능을 나타내는 의미 있는 이름을 지정하면 됩니다. 예를 들어 Material의 TopAppBartitle, navigationIcon, actions 슬롯을 제공합니다.

이 구현을 적용하면 섹션이 어떻게 바뀌는지 살펴보겠습니다.

d824b60e650deeb.png

Text 컴포저블이 디자인과 똑같이 되려면 추가 정보가 필요합니다. 다음과 같이 업데이트하세요.

  • 모두 대문자로 표시합니다(힌트: Stringuppercase() 메서드를 사용할 수 있습니다).
  • H2 서체를 사용합니다.
  • 레드라인 디자인과 같은 패딩을 갖습니다.

최종 솔루션은 다음과 같습니다.

import java.util.*

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(
           text = stringResource(title).uppercase(Locale.getDefault()),
           style = MaterialTheme.typography.h2,
           modifier = Modifier
               .paddingFromBaseline(top = 40.dp, bottom = 8.dp)
               .padding(horizontal = 16.dp)
       )
       content()
   }
}

10. 홈 화면 - 스크롤

모든 개별 구성요소를 만들었으니 이제 이들을 전체 화면 구현으로 결합할 차례입니다.

구현할 디자인은 다음과 같습니다.

a535e10437e9df31.png

검색창 아래에 하나의 섹션을, 그 아래에 또 하나의 섹션을 배치하면 됩니다. 디자인과 동일하게 만들려면 간격을 추가해야 합니다. 이제까지 사용하지 않은 Spacer를 사용하면 Column 내부에서 더 많은 공간을 확보할 수 있습니다. Spacer를 사용하는 대신 Column의 패딩을 설정하면 즐겨찾는 컬렉션 그리드에서 본 것과 동일한 잘라내기 동작이 적용됩니다.

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(modifier) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

이 디자인은 대부분의 기기 크기에서 제대로 표현되긴 하나, 기기의 높이가 작을 경우(예: 가로 모드)에 대비하여 세로 방향으로 스크롤할 수 있어야 합니다. 이렇게 하려면 스크롤 동작을 추가해야 합니다.

앞에서 보았듯이 LazyRowLazyHorizontalGrid와 같은 Lazy 레이아웃은 자동으로 스크롤 동작을 추가합니다. 하지만 항상 Lazy 레이아웃이 필요한 것은 아닙니다. 일반적으로, 목록에 포함된 요소가 많거나 로드해야 할 데이터 세트가 많아서 모든 항목을 동시에 내보내면 성능이 저하되고 앱이 느려지게 되는 경우에 Lazy 레이아웃을 사용합니다. 목록에 포함된 요소의 개수가 많지 않은 경우에는 간단한 Column 또는 Row를 사용하고 스크롤 동작을 수동으로 추가하면 됩니다. 이렇게 하려면 verticalScroll 또는 horizontalScroll 수정자를 사용합니다. 이를 위해서는 스크롤의 현재 상태를 포함하며 외부에서 스크롤 상태를 수정하는 데 사용되는 ScrollState가 필요합니다. 여기서는 스크롤 상태를 수정할 필요가 없으므로 rememberScrollState를 사용하여 영구 ScrollState 인스턴스를 만들면 됩니다.

최종 결과는 다음과 같습니다.

import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(
       modifier
           .verticalScroll(rememberScrollState())
           .padding(vertical = 16.dp)
   ) {
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
   }
}

컴포저블의 스크롤 동작을 확인하려면 미리보기 높이를 제한한 상태에서 대화형 미리보기로 실행해 보세요.

@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2, heightDp = 180)
@Composable
fun ScreenContentPreview() {
   MySootheTheme { HomeScreen() }
}

11. 하단 탐색 - Material

화면의 콘텐츠를 구현했으니 이제 창 장식을 추가할 차례입니다. MySoothe에는 사용자가 여러 화면 간에 전환할 수 있는 하단 탐색이 있습니다.

먼저 이 하단 탐색 컴포저블을 구현한 다음 앱에 포함합니다.

디자인을 살펴보겠습니다.

2f42ad2b882885f8.png

다행히 이 컴포저블은 처음부터 구현하지 않아도 됩니다. Compose Material 라이브러리의 일부인 BottomNavigation 컴포저블을 사용하면 됩니다. BottomNavigation 컴포저블 내에서 하나 이상의 BottomNavigationItem 요소를 추가하면 Material 라이브러리에 의해 자동으로 스타일이 지정됩니다.

하단 탐색의 기본 구현으로 시작하겠습니다.

import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   BottomNavigation(modifier) {
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

이 기본 구현의 디자인은 다음과 같습니다.

5bdb7729d75e1a72.png

스타일 조정이 필요한 것을 알 수 있습니다. 먼저 backgroundColor 매개변수를 설정하여 하단 탐색의 배경색을 업데이트합니다. Material 테마의 배경색을 사용하면 됩니다. 배경색을 설정하면 아이콘과 텍스트의 색상이 테마의 onBackground 색상으로 자동 조정됩니다. 최종 솔루션은 다음과 같습니다.

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   BottomNavigation(
       backgroundColor = MaterialTheme.colors.background,
       modifier = modifier
   ) {
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       BottomNavigationItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

12. MySoothe 앱 - Scaffold

마지막 단계에서는 하단 탐색을 포함하는 전체 화면 구현을 만듭니다. Material의 Scaffold 컴포저블을 사용하세요. Scaffold는 Material Design을 구현하는 앱을 위한 구성 가능한 최상위 수준 컴포저블을 제공합니다. 여기에는 다양한 Material 개념의 슬롯이 포함되어 있는데, 그중 하나가 하단 메뉴입니다. 하단 메뉴 안에 앞 단계에서 만든 하단 탐색 컴포저블을 배치할 수 있습니다.

MySootheApp 컴포저블을 구현합니다. 이것은 앱의 최상위 수준 컴포저블이므로 다음을 해야 합니다.

  • MySootheTheme Material 테마 적용
  • Scaffold 추가
  • 하단 막대가 SootheBottomNavigation 컴포저블이 되도록 설정
  • 콘텐츠가 HomeScreen 컴포저블이 되도록 설정

최종 결과는 다음과 같습니다.

import androidx.compose.material.Scaffold

@Composable
fun MySootheApp() {
   MySootheTheme {
       Scaffold(
           bottomBar = { SootheBottomNavigation() }
       ) { padding ->
           HomeScreen(Modifier.padding(padding))
       }
   }
}

이렇게 해서 구현이 완료되었습니다. 내 버전이 픽셀 단위까지 정확하게 구현되었는지 확인하려면 다음 이미지를 다운로드하여 내 버전의 미리보기 구현과 비교해 보세요.

24ff9efa75f22198.png

13. 축하합니다

축하합니다. 이 Codelab을 완료하여 Compose의 레이아웃에 관해 자세히 배웠습니다. 실제 디자인을 구현해 보면서 수정자, 정렬, 배치, Lazy 레이아웃, 슬롯 API, 스크롤, Material 구성요소에 관해 알아보았습니다.

Compose 개발자 과정의 다른 Codelab도 살펴보세요. 코드 샘플도 확인해 보세요.

문서

이러한 주제에 관한 자세한 내용 및 안내는 다음 문서를 참고하세요.