젯팩 컴포즈로 개발하는 안드로이드 UI을 공부하며 정리한 내용입니다.
저작권에 문제가 될 시, 글을 모두 내리겠습니다.
제가 공부한 내용이 더 많은 분들에게도 도움이 되었으면 좋겠습니다. 부족한 부분은 댓글을 통해서 피드백을 주신다면 언제나 반영하겠습니다. 감사합니다.
책에 대한 링크는 가장 아래에 있습니다.
미리 정의된 레이아웃 사용
Jetpack Compose에서는 UI 요소를 어떤 축(방향)으로 배치할지 결정하는 기본 레이아웃 컴포저블을 제공합니다. 주요 축은 다음과 같습니다:
- 수평: Row()
- 수직: Column()
- 스택: Box() / BoxWithConstraints()
이런 축 기반 컴포저블을 조합하면 복잡한 UI도 쉽게 구성할 수 있습니다.
기본 구성 요소 조합
CheckboxWithLabel
체크박스와 텍스트를 가로로 배치하고, 텍스트를 클릭해도 체크 상태가 바뀌도록 구현합니다.
@Composable
fun CheckboxWithLabel(label: String, state: MutableState<Boolean>) {
Row(
modifier = Modifier.clickable { state.value = !state.value },
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = state.value,
onCheckedChange = { state.value = it }
)
Text(
text = label,
modifier = Modifier.padding(start = 8.dp)
)
}
}
- Row()로 수평 배치
- verticalAlignment로 수직 중앙 정렬
- Modifier.clickable로 텍스트 클릭에도 상태 변경 가능
PredefinedLayoutsDemo
체크박스 상태에 따라 빨간색, 초록색, 파란색 박스를 조건부로 렌더링합니다.
@Composable
@Preview
fun PredefinedLayoutsDemo() {
val red = remember { mutableStateOf(true) }
val green = remember { mutableStateOf(true) }
val blue = remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
CheckboxWithLabel("Red", red)
CheckboxWithLabel("Green", green)
CheckboxWithLabel("Blue", blue)
if (red.value) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red)
)
}
if (green.value) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp)
.background(Color.Green)
)
}
if (blue.value) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(64.dp)
.background(Color.Blue)
)
}
}
}
- Column()으로 체크박스 수직 배치
- Box()로 배경색 박스를 겹쳐 배치
- 서로 다른 padding()으로 쌓인 박스들이 모두 보이도록 조정
제약 조건을 기반으로 하는 레이아웃 생성
복잡한 레이아웃에서는 ConstraintLayout을 사용해 중첩 없이 명확한 위치 제약으로 UI를 구성할 수 있습니다.
CheckboxWithLabel – ConstraintLayout 기반
@ExperimentalComposeUiApi
@Composable
fun CheckboxWithLabel(
label: String,
state: MutableState<Boolean>,
modifier: Modifier = Modifier
) {
ConstraintLayout(modifier = modifier.clickable {
state.value = !state.value
}) {
val (checkbox, text) = createRefs()
Checkbox(
checked = state.value,
onCheckedChange = { state.value = it },
modifier = Modifier.constrainAs(checkbox) { }
)
Text(
text = label,
modifier = Modifier.constrainAs(text) {
start.linkTo(checkbox.end, margin = 8.dp)
top.linkTo(checkbox.top)
bottom.linkTo(checkbox.bottom)
}
)
}
}
- createRefs()로 각 요소의 참조 생성
- constrainAs()로 각 요소의 제약 설정
- 텍스트는 체크박스 오른쪽에 위치하며 수직 정렬
ConstraintLayoutDemo
ConstraintLayout 기반으로 PredefinedLayoutsDemo를 재구성한 예시입니다.
@ExperimentalComposeUiApi
@Composable
@Preview
fun ConstraintLayoutDemo() {
val red = remember { mutableStateOf(true) }
val green = remember { mutableStateOf(true) }
val blue = remember { mutableStateOf(true) }
ConstraintLayout(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
val (cbRed, cbGreen, cbBlue, boxRed, boxGreen, boxBlue) = createRefs()
CheckboxWithLabel(
label = "Red", state = red,
modifier = Modifier.constrainAs(cbRed) {
top.linkTo(parent.top)
}
)
CheckboxWithLabel(
label = "Green", state = green,
modifier = Modifier.constrainAs(cbGreen) {
top.linkTo(cbRed.bottom)
}
)
CheckboxWithLabel(
label = "Blue", state = blue,
modifier = Modifier.constrainAs(cbBlue) {
top.linkTo(cbGreen.bottom)
}
)
if (red.value) {
Box(
modifier = Modifier
.background(Color.Red)
.constrainAs(boxRed) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(cbBlue.bottom, margin = 16.dp)
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
)
}
if (green.value) {
Box(
modifier = Modifier
.background(Color.Green)
.constrainAs(boxGreen) {
start.linkTo(parent.start, margin = 32.dp)
end.linkTo(parent.end, margin = 32.dp)
top.linkTo(cbBlue.bottom, margin = 48.dp)
bottom.linkTo(parent.bottom, margin = 32.dp)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
)
}
if (blue.value) {
Box(
modifier = Modifier
.background(Color.Blue)
.constrainAs(boxBlue) {
start.linkTo(parent.start, margin = 64.dp)
end.linkTo(parent.end, margin = 64.dp)
top.linkTo(cbBlue.bottom, margin = 80.dp)
bottom.linkTo(parent.bottom, margin = 64.dp)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
)
}
}
}
포인트 요약
- 각 CheckboxWithLabel은 바로 위 컴포저블의 bottom과 연결되어 수직 정렬됨
- 각 색 박스는 parent와 양쪽 linkTo()로 가운데 정렬되며, top, bottom은 마진을 통해 위치 조절
- Dimension.fillToConstraints를 통해 박스의 크기를 제약 조건에 맞춰 자동 조정
- Row, Column, Box는 단순 UI 배치에 적합하며 직관적
- ConstraintLayout은 복잡한 UI나 중첩을 줄이고 싶을 때 유용
- ConstraintLayout을 사용할 때는 createRefs()와 constrainAs() 패턴을 익혀야 함
Compose의 강력한 레이아웃 시스템을 잘 활용하면 더욱 유연하고 깔끔한 UI를 만들 수 있다.
단일 측정 단계의 이해
Jetpack Compose에서 UI 요소를 배치한다는 것은 크기를 측정하고 위치를 지정하는 것입니다. 기본적으로 레이아웃은 자식의 콘텐츠 크기를 측정한 뒤 배치합니다.
예제: Column 내 Text 구성
@Composable
@Preview
fun ColumnWithTexts() {
Column {
Text(
text = "Android UI development with Jetpack Compose",
style = MaterialTheme.typography.h3
)
Text(
text = "Hello Compose",
style = MaterialTheme.typography.h5.merge(TextStyle(color = Color.Red))
)
}
}
- 첫 번째 텍스트는 길이가 길어 세로로 공간을 더 차지
- 두 번째 텍스트는 한 줄로 표시됨
- 이런 차이는 부모 Column이 전달한 제약 조건(Constraints) 때문
이 제약 조건은 ConstraintLayout에서 말하는 것과는 다른, 크기 제약 조건을 의미합니다.
Column 내부 구조
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
- Layout() 함수로 내부 콘텐츠를 렌더링
- measurePolicy가 핵심이며, 실제 측정과 배치를 담당
측정 정책 정의
columnMeasurePolicy()는 내부적으로 rowColumnMeasurePolicy()를 호출하여 MeasurePolicy를 반환합니다.
MeasurePolicy란?
androidx.compose.ui.layout.MeasurePolicy는 레이아웃 측정 및 배치 방식 정의 인터페이스입니다.
MeasurePolicy { measurables, constraints ->
// 측정 및 배치 로직
}
- measurables: 자식 요소 목록
- constraints: 상위로부터 전달된 제약 조건
- measure(): 각 요소를 측정하여 Placeable을 반환
- MeasureResult: 레이아웃의 크기와 자식 배치 방식 반환
Compose에서는 자식은 오직 한 번만 측정 가능하며, 다시 측정 시 예외가 발생합니다.
Intrinsic 측정
레이아웃은 실제 측정 없이 콘텐츠에 따라 적절한 최소/최대 너비·높이를 질의할 수 있습니다.
fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int {
val mapped = measurables.fastMap {
DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Min, IntrinsicWidthHeight.Width)
}
val constraints = Constraints(maxHeight = height)
val layoutReceiver = IntrinsicsMeasureScope(this, this.layoutDirection)
val layoutResult = layoutReceiver.measure(mapped, constraints)
return layoutResult.width
}
- minIntrinsicWidth: 주어진 높이에서 최소 너비
- Constraints: minWidth, maxWidth, minHeight, maxHeight 등을 포함
- Infinity: 제약이 없음을 나타내는 상수
커스텀 레이아웃 작성
예: 박스를 가로로 나열하다 줄 바꿈하는 FlexBox 레이아웃
ColoredBox 정의
@Composable
fun ColoredBox() {
Box(
modifier = Modifier
.border(2.dp, Color.Black)
.background(randomColor())
.width((40 * randomInt123()).dp)
.height((10 * randomInt123()).dp)
)
}
private fun randomInt123() = Random.nextInt(1, 4)
private fun randomColor() = when (randomInt123()) {
1 -> Color.Red
2 -> Color.Green
else -> Color.Blue
}
CustomLayoutDemo 예제
@Composable
@Preview
fun CustomLayoutDemo() {
SimpleFlexBox {
for (i in 0..42) {
ColoredBox()
}
}
}
SimpleFlexBox 정의
@Composable
fun SimpleFlexBox(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content,
measurePolicy = simpleFlexboxMeasurePolicy()
)
}
커스텀 측정 정책
private fun simpleFlexboxMeasurePolicy(): MeasurePolicy =
MeasurePolicy { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
var yPos = 0
var xPos = 0
var maxY = 0
placeables.forEach { placeable ->
if (xPos + placeable.width > constraints.maxWidth) {
xPos = 0
yPos += maxY
maxY = 0
}
placeable.placeRelative(x = xPos, y = yPos)
xPos += placeable.width
if (placeable.height > maxY) maxY = placeable.height
}
}
}
측정 정책 요약:
- 자식 요소를 한 줄에 배치하다 너비를 초과하면 줄 바꿈
- xPos, yPos, maxY를 계산하며 위치 지정
- placeRelative()로 배치 수행
도서 링크 바로가기
https://www.yes24.com/Product/Goods/116413696
젯팩 컴포즈로 개발하는 안드로이드 UI - 예스24
젯팩 컴포즈는 안드로이드 UI 개발의 새로운 패러다임이다. 이 책은 젯팩 컴포즈를 통해 안드로이드 애플리케이션을 개발할 수 있도록 도와줄 것이다. 젯팩 컴포즈로 안드로이드를 처음 개발해
www.yes24.com
'Android > Study' 카테고리의 다른 글
| [Android] Orbit Multiplatform 개요와 Core 탐구하기 (0) | 2025.07.03 |
|---|---|
| [Android] Android 앱 아키텍처 가이드 정리 (2) | 2025.07.03 |
| [Jetpack Compose로 개발하는 안드로이드 UI] 3장. 컴포즈 핵심 원칙 자세히 알아보기 (0) | 2025.05.07 |
| [Jetpack Compose로 개발하는 안드로이드 UI] 2장. 선언적 패러다임 이해 (0) | 2025.04.18 |
| [Jetpack Compose로 개발하는 안드로이드 UI] 1장. Jetpack Compose 기본 요소 정리하기 (0) | 2025.04.15 |