컴포저블 함수는 Compose의 기본 구성요소로 UI의 일부를 설명하는Unit을 내보내는 함수다. 컴포저블 함수는 여러 UI요소를 내포할 수 있으므로 적절한 정렬 가이드가 없다면 개발자가 원하지 않는 방식으로 요소를 정렬할 수 있다. 따라서 Compose에서 정렬은 매우 중요하며 그만큼 다양한 기본 API를 제공한다.
@Composable
fun ArtistCard() { // 텍스트가 겹쳐서 표시되게 된다.
Text("Alfred Sisley")
Text("3 minutes ago")
}

🍕 Unit은 Kotlin에서의 기본 반환 타입으로 void와 유사한 반환값이 없는 함수를 말한다.
Column: 요소를 세로로 배치
@Composable
fun ArtistCardColumn() {
Column {
Text("Alfred Sisley")
Text("3 minutes ago")
}
}
Row: 요소를 가로로 배치
@Composable
fun ArtistCardRow(artist: Artist) {
Row(verticalAlignment = Alignment.CenterVertically) { // 상하 중앙정렬
Image(bitmap = artist.image, contentDescription = "Artist image")
Column {
Text(artist.name)
Text(artist.lastSeenOnline)
}
}
}
Box: 요소위에 다른 요소를 올림, 포함된 요소의 특정 정렬 구성도 지원
@Composable
fun ArtistAvatar(artist: Artist) {
Box {
Image(bitmap = artist.image, contentDescription = "Artist image")
Icon(Icons.Filled.Check, contentDescription = "Check mark")
}
}

그리고 레이아웃 모델에서 UI트리는 단일 패스로 배치되는데 크기 제약 조건을 트리 아래 하위 요소를 전달한다. 그러면 리프 토드에서 확인된 크기 및 배치안내가 트리위로 다시 전달되게 된다. 즉, 상위 요소가 먼저 측정되더라도 하위 요소 이후에 크기 및 위치가 지정된다.
따라서 아래와 같은 UI 트리가 있을 때
SearchResult
Row
Image
Column
Text
Text
순서는 이렇게 된다.

이미지 출처: Android Developer
🍕 단일패스란 한 번의 트리 순회로 크기와 배치를 계산하는 방식이다.
수정자(Modifier)는 체이닝 방식으로 컴포저블 장식, 강화가 가능하다. 레이아웃 맞춤설정에 필수적이다.
@Composable
fun ArtistCardModifiers(
artist: Artist,
onClick: () -> Unit
) {
val padding = 16.dp
Column(
Modifier
.clickable(onClick = onClick) // 사용자 입력 반응 및 물결 효과 표시
.padding(padding) // 요소 주의 공간 배치
.fillMaxWidth() // 해당 컴포저블이 상위 요소로부터 부여받은 최대 너비를 가지도록
) {
Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
Spacer(Modifier.size(padding))
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) { /*...*/ }
}
}
컴포즈에서 스크롤 가능한 레이아웃은 기존 xml에서 사용하던 리사이클러뷰를 가공한 Lazy 시리즈나 modifier를 통해 해당 컴포저블이 스크롤이 가능하도록 할 수 있다. 기존에 ScrollableColumn 및 ScrollableRow가 있었지만 현재 Lazy시리즈로 변경되었으므로 Deprecated 되었다.
1. LazyColumn
세로 방향으로 스크롤 가능한 리스트를 만들 때 사용한다. 일반적인 리스트 또는 목록을 효율적으로 렌더링할 수 있도록 도와준다.
큰 데이터 목록을 처리할 때 유용하며, 항목이 필요할 때만 렌더링되기 때문에 성능이 뛰어나다.
LazyColumn {
items(itemsList) { item ->
Text(text = item)
}
}
2. LazyRow
가로 방향으로 스크롤 가능한 리스트를 만들 때 사용한다. 가로로 스크롤하는 이미지 슬라이더나 아이콘 목록에 적합하다.
LazyRow {
items(itemsList) { item ->
Text(text = item)
}
}
3. LazyVerticalGrid
그리드 형태로 세로 방향 스크롤이 가능한 레이아웃을 만들 수 있다. 여러 열을 갖는 레이아웃을 쉽게 만들 수 있다.
LazyVerticalGrid(columns = GridCells.Fixed(2)) {
items(itemsList) { item ->
Text(text = item)
}
}
4. LazyHorizontalGrid
가로 방향으로 스크롤 가능한 그리드 레이아웃을 만들 때 사용하며 LazyVerticalGrid와 유사하지만 가로로 스크롤한다.
LazyHorizontalGrid(rows = GridCells.Fixed(2)) {
items(itemsList) { item ->
Text(text = item)
}
}
5. Column + Modifier.verticalScroll
Column은 기본적으로 스크롤 기능이 없지만, Modifier.verticalScroll을 사용하면 세로로 스크롤이 가능하게 만들어준다.
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
itemsList.forEach { item ->
Text(text = item)
}
}
6. Row + Modifier.horizontalScroll
Row에 Modifier.horizontalScroll을 적용하여 가로 방향으로 스크롤 가능한 레이아웃을 만들 수 있다.
Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
itemsList.forEach { item ->
Text(text = item)
}
}
앱을 설계할 때는 다양한 화면 크기(예: 스마트폰, 태블릿, 데스크탑)와 화면 방향(가로, 세로)에 맞춰 유연하게 동작하는 반응형 레이아웃을 고려해야 한다. Jetpack Compose에서는 이런 다양한 폼 팩터에 맞춰 레이아웃을 유연하게 설계할 수 있는 몇 가지 메커니즘을 제공한다.
BoxWithConstraints는 화면의 크기에 따라 레이아웃을 동적으로 조정할 수 있는 컨테이너다. 이 컴포저블은 현재 레이아웃의 제약 조건(최대 너비와 높이 등)을 제공하며, 이를 이용해 화면 크기별로 다른 레이아웃을 설정할 수 있다.
예를 들어, 작은 화면과 큰 화면에서 서로 다른 UI 구성을 할 때 유용하다.
@Composable
fun ResponsiveLayout() {
BoxWithConstraints {
if (maxWidth < 600.dp) {
// 작은 화면에서 사용할 UI
Text(text = "작은 화면")
} else {
// 큰 화면에서 사용할 UI
Text(text = "큰 화면")
}
}
}
maxWidth와 maxHeight는 현재 레이아웃의 최대 너비와 높이를 제공하며, 이를 활용해 조건부 레이아웃을 만들 수 있다.
dp(density-independent pixels) 단위를 사용하면, 화면 밀도에 따라 UI 요소의 크기가 조정되어 작은 화면이나 큰 화면에서도 비슷한 크기와 비율을 유지할 수 있다. 예를 들어, 5인치 스마트폰과 10인치 태블릿 모두에서 16dp 크기의 텍스트는 밀도에 따라 다르게 보정되어, 상대적으로 비슷한 크기로 보여지게 된다.
그러나, 같은 스마트폰이라도 해상도와 밀도가 다를 경우 단순히 dp로 크기를 설정하는 것만으로는 일관된 UI를 유지하기 어렵다. 예를 들어, FHD 해상도를 가진 6인치 스마트폰과 QHD 해상도를 가진 동일한 크기의 스마트폰에서는 같은 16dp 크기의 텍스트가 다르게 보일 수 있다. 밀도가 높을수록 텍스트나 요소가 상대적으로 작게 보이는 문제가 발생할 수 있다.
이와 같은 이유로, 단순히 dp를 사용하더라도 다양한 해상도와 밀도에서 UI의 크기를 완벽하게 조정하기 어려운 상황이 발생할 수 있다.
1. 기기별 해상도와 밀도의 차이
스마트폰, 태블릿, 고해상도 기기 등 기기마다 해상도와 밀도가 다르기 때문에 고정된 dp 값이 모든 기기에서 적절하지 않다.
예를들어 16dp의 버튼 크기가 FHD 해상도에서는 적절하게 보일 수 있지만, QHD 해상도나 그 이상의 밀도가 높은 기기에서는 작게 보일 수 있다.
같은 크기의 화면을 가진 스마트폰이라도 해상도나 dpi가 다르면 dp 단위로 설정된 요소가 각각 다르게 보이게 된다.
2. 밀도에 따른 크기 차이
해상도가 높을수록 dp로 설정된 크기가 상대적으로 작게 보이는 문제가 발생한다. 이는 단순히 밀도가 높은 기기에서 더 많은 픽셀이 사용되기 때문에 발생하는 문제다. 예를 들어:
6인치 스마트폰에서 FHD와 QHD 해상도를 비교하면, 같은 16dp 크기의 버튼이 QHD에서는 상대적으로 더 작게 보인다.
따라서, 기기별로 밀도와 해상도를 고려하지 않고 고정된 dp 값만 사용하면 UI 요소가 화면에서 적절한 크기로 보여지지 않게 된다.
3. 다양한 폼 팩터 지원의 필요성
고정된 dp 값은 특정한 기기나 화면 크기에만 최적화된 경우가 많다. 하지만, 다양한 크기와 폼 팩터를 가진 기기를 지원해야 할 때는 유연하게 크기를 조정할 필요가 있다. 예를 들어:
스마트폰에서는 적당한 크기의 버튼이 태블릿에서는 너무 작아 보일 수 있다.
큰 화면에서 고정된 dp 값은 화면 공간을 충분히 활용하지 못하게 되어 비율이 어색해질 수 있다.
따라서 화면의 구성이 복잡한 경우 각 dpi별로 dimens.xml 파일을 사용하여 각기 다른 크기의 dp를 정의해놓고 사용하는 경우가 많다.
또한 화면 구성 시 xml의 경우 Layout Validation으로 각 화면마다 어떻게 화면이 구현되는 지 미리볼 수 있고 compose의 경우 Preview Api에 크기 설정을 할 수도 있다.
@Preview(name = "Phone Preview", device = "spec:width=411dp,height=891dp")
@Composable
fun PhonePreview() {
Codelab_material_themeTheme {
Greeting("Android")
}
}
@Preview(name = "Tablet Preview", device = "spec:width=1280dp,height=800dp")
@Composable
fun TabletPreview() {
Codelab_material_themeTheme {
Greeting("Android")
}
}
WindowSizeClass는 Android에서 다양한 화면 크기와 방향에 맞춰 UI 레이아웃을 유연하게 설계할 수 있도록 돕는 메커니즘이다. 이는 Jetpack Compose와 Material Design 3에서 제공되는 API로, 화면의 너비와 높이에 따라 세 가지 주요 분류(Compact, Medium, Expanded)로 나뉜다. 이를 통해 다양한 디바이스 크기에 맞춰 UI를 최적화할 수 있다.
Compact (600dp 미만): 주로 스마트폰에서 사용되며, 콘텐츠가 한 열로 표시되거나 최소한의 정보만 화면에 보여진다.
Medium (600dp ~ 840dp): 중간 크기의 기기에서 사용되며, 태블릿의 세로 모드나 큰 스마트폰에서 사용될 수 있다. 여기서는 두 열로 나뉘거나 추가적인 콘텐츠가 포함될 수 있다.
Expanded (840dp 이상): 주로 태블릿이나 폴더블 기기의 가로 모드에서 사용되며, 더 넓은 화면 공간을 활용하여 여러 열의 정보를 표시하거나, 분할된 레이아웃을 지원할 수 있다.
@Composable
fun MyApp(windowSize: WindowSizeClass) {
when (windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
// 세로 모드 600 이하, 스마트폰 및 작은 화면에서 사용할 레이아웃
MyAppCompact()
}
WindowWidthSizeClass.Medium -> {
// 중간 사이즈 600-840 사이
MyAppMiddle()
}
WindowWidthSizeClass.Expanded -> {
// 가로 모드 840 이상, 태블릿, 폴더블 기기의 가로 모드에서 사용할 레이아웃
MyAppLaunch()
}
else -> {
// 기본 상태로 처리
MyAppCompact()
}
}
}