print("와챠의 개발 기록장")
[Compose] 1-2. Compose의 기본 레이아웃/element/list/NavigationBar/window size 본문
[Compose] 1-2. Compose의 기본 레이아웃/element/list/NavigationBar/window size
minWachya 2025. 4. 1. 16:011탄에서는 Surfaces, Rows, Columns를 사용해 레이아웃을 구성하고,
Modifier의 padding, fillMaxWidth, size 등으로 레이아웃을 꾸며보았다.
이번엔 아래와 같은 좀 더 복잡한 레이아웃을 만들어보려고 한다.
이 링크에서 실습할 수 있다~
위에서부터 위젯을 작게 나누어서 개발해보자!
<목차>
- SearchBar
- Align Your Body: element
- Favorite Collections: element
- Align Your Body: list
- Favorite Collections: list
- Align Your Body & Favorite Collections
- NavigationBar
- Done!
- window size에 맞게 화면 재구성
1. SearchBar
@Composable
fun SearchBar(
modifier: Modifier = Modifier
) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
.fillMaxWidth()
.heightIn(min = 56.dp),
// 검색 아이콘
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "검색 아이콘"
)
},
// 배경색
colors = TextFieldDefaults.colors(
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
focusedContainerColor = MaterialTheme.colorScheme.surface
),
// hint
placeholder = {
Text(stringResource(R.string.placeholder_search))
}
)
}
2. Align Your Body: element
@Composable
fun AlignYourBodyElement(
modifier: Modifier = Modifier,
@DrawableRes drawable: Int,
@StringRes text: Int
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(drawable),
contentDescription = stringResource(text),
contentScale = ContentScale.Crop,
modifier = Modifier
.size(88.dp)
.clip(CircleShape)
)
Text(
text = stringResource(text),
modifier = modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
- @DrawableRes, @StringRes 이렇게 어노테이션 쓰는 것... 보기 편하고 좋네요
- 나머진 플러터에서 UI 구성하는 거랑 비슷
3. Favorite Collections: element
@Composable
fun FavoriteCollectionCard(
modifier: Modifier = Modifier,
@DrawableRes drawable: Int,
@StringRes text: Int
) {
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surfaceVariant
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.width(255.dp)
) {
Image(
painter = painterResource(drawable),
contentDescription = stringResource(text),
contentScale = ContentScale.Crop,
modifier = Modifier.size(80.dp)
)
Text(
text = stringResource(text),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
}
- 구현하라고 했을 때 modifier.size...이런 식으로 해서 UI가 원하는대로 안 나옴... 당연함. 전달받는 modifier에 다른 설정이 들어가있었기 때문..;; Modifier로 새로 만들어줘야 원하던 UI를 구현할 수 있었다!!
4. Align Your Body: list
// 어떤 거 import 하는지 확인!
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
@Composable
fun AlignYourBodyRow(
modifier: Modifier = Modifier
) {
LazyRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 16.dp)
) {
items(alignYourBodyData) { item ->
AlignYourBodyElement(drawable = item.drawable, text = item.text)
}
}
}
- 아이템 배열을 어떻게 활용해야할지...몰랐는데 items() {}를 활용하는구나,,, 알려주고 진행해주지🥲 근데 완전 간편하네... compose가 기존 UI 구성법보다 kotlin을 더 잘 활용하는 느낌이라 좋은 거 같다. 기존엔 넘 JAVA 스러웠음. 글고 adapter, viewholder도 만들어야 해서 코드 개 길어짐,,,ㄱ- 컴포즈가 천사같다
- LazyRow, LazyColumn: 필요한 요소만 렌더링하고 필요 없어진 요소는 삭제함으로서 메모리를 효율적으로 관리한다. 전에도 다뤘지만 RecyclerView 비슷한데 RecyclerView는 요소가 화면에 안 보인다고 해서 바로 삭제하진 않는 차이가 있는 듯하다.
관련 링크1 : Jetpack Compose LazyColumn vs View System RecyclerView
관련 링크2: 리사이클러뷰의 생명주기 분석 및 메모릭의 원인
5. Favorite Collections: list
@Composable
fun FavoriteCollectionsGrid(
modifier: Modifier = Modifier
) {
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
modifier = modifier.height(168.dp),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(favoriteCollectionsData) { item ->
FavoriteCollectionCard(modifier = Modifier.height(80.dp), drawable = item.drawable, text = item.text)
}
}
}
- 그리드 활용 방식과 아이템 사이 간격 패딩을 넣는 방법~~!
- 플러터가 자꾸 생각나네... 블럭을 쌓아가는 방식같아서 재밌다.
6. Align Your Body & Favorite Collections
@Composable
fun HomeSection(
@StringRes title: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Column(modifier) {
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.paddingFromBaseline(top = 40.dp, bottom = 16.dp)
.padding(horizontal = 16.dp)
)
content()
}
}
- title 밑에 list인 view 양식은 똑같으니 이 양식을 Compose로 또 만든다.
- 이때 content()처럼 UI의 빈 공간을 채우는 것, 이렇게 개발하는 것을 slot api라고 하는듯
Slot API pattern in Compose is a common pattern in Compose that offers “slots”— generic lambdas that accept composable content.
슬롯 API 패턴은 Compose의 흔한 패턴으로, composable content를 수용하는 일반적인 람다인 "slot"을 제공한다.
https://www.valueof.io/blog/compose-slot-api-example-composable-content-lambda
- 이걸 아래처럼 활용하면 됨
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
Column(
modifier.verticalScroll(rememberScrollState())
) {
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))
}
}
스크롤할 요소가 많다면 Lazy~를 쓰면 되지만, 위 화면처럼 스크롤할 요소가 많지 않으면 Column, Row에 스크롤 동작을 추가하면 된다.
세로로 스크롤 할 거니까 verticalScroll을 사용하고, ScrollState는 기본 매개변수를 사용해 상태를 자동으로 기억하도록 rememberScrollState를 사용한다.
State of the scroll. Allows the developer to change the scroll position or get current state by calling methods on this object.
To be hosted and passed to Modifier.verticalScroll or Modifier.horizontalScroll
To create and automatically remember ScrollState with default parameters use rememberScrollState.
스크롤 상태. 개발자가 이 객체에서 메서드를 호출하여 스크롤 위치를 변경하거나 현재 상태를 얻을 수 있다.
호스팅하여 Modifier.verticalScroll 또는 Modifier.horizonScroll로 전달한다.
기본 매개변수를 사용하여 ScrollState를 생성하고 자동으로 기억하려면 rememberScrollState를 사용합니다.
ScrollState 공식문서
7. NavigationBar
@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
NavigationBar(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
modifier = modifier
) {
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_home))
},
selected = true,
onClick = {}
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_profile))
},
selected = false,
onClick = {}
)
}
}
- 지금은 클릭해도 동작 변화 없는 기본 모습... 걍 컴포즈만 공부한다구 생각하면 댐. 와 근데 이것도 엄청 편해졌네... 나때는,,,궁시렁
8. Done!
import androidx.compose.material3.Scaffold
@Composable
fun MySootheAppPortrait() {
MySootheTheme {
Scaffold(
bottomBar = { SootheBottomNavigation() }
) { padding ->
HomeScreen(Modifier.padding(padding))
}
}
}
- 이렇게 Scaffold에 네비바랑 홈 화면 넣어주면 원했던 화면 UI 만들어보기 끝!!
9. window size에 맞게 화면 재구성
- 세로 모드로 하거나 탭과 같이 가로가 긴 화면을 위해서는 위와 같이 UI를 변경해주는 것이 보기에 편하다.
- 이를 위해 먼저 세로 형태의 NaviBar를 만들어보자.
- 크게 다를 건 없고... NavigationRail를 사용하고 가운데정렬을 해주면 된다!
@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
NavigationRail(
modifier = modifier.padding(start = 8.dp, end = 8.dp),
containerColor = MaterialTheme.colorScheme.background,
) {
Column(
modifier = modifier.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// 위와 동일해서 생략
NavigationRailItem(~)
Spacer(modifier = Modifier.height(8.dp))
NavigationRailItem(~)
}
}
}
- 완성된 네비 바를 MySootheApp 함수처럼 windowSize에 맞게 어떤 UI 보여줄지 선택하고,
- MainActivity에서 화면 크기 계산하여 넘겨주면 된다!!
@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
when(windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> MySootheAppPortrait()
WindowWidthSizeClass.Expanded -> MySootheAppLandscape()
}
}
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val windowSizeClass = calculateWindowSizeClass(this)
MySootheApp(windowSizeClass)
}
}
}
느낀점
잭팟 컴포즈 걍 완전 플러터같다...
전에 iOS할 때도 그랬는데 전부 컴포넌트 활용하는 방식으로 개발하는듯
근데 확실히 코드도 더 간결해지고, 파일도 줄고... 언어 특성을 잘 살려서 개발하는 거 같아서 재밋다...
전엔 어케 안드 코딩했는지... 지금에서야 그때를 돌아보니까 그때 되게 노가다한 거 같이 느껴지네...
암튼 하나하나 배워가는 거 재밌고, 플러터에서 이미 비슷한 내용을 공부해봤으니까 이해도 더 쉽게 되는 느낌이다.ㅎㅎ
강의에서 이렇게 개발하라~하고 처음 배우는 요소 쓰게 하는데, 커서 대고있거나 .<누르면 어떤 거 사용 가능한지 쉽게 알 수 있으니까 금방 배우고 적용할 수 있는듯. 굿!!
화면 사이즈 고려하는 거나 Lazy~레이아웃이 너무 간단해서 놀랐다...
'코딩 일기장 > Android(Kotlin)' 카테고리의 다른 글
[Compose] 2-1. Material Design 3으로 앱 테마 지정 / Material Theme Builder / Color, Type, Theme, Shape 설정 (0) | 2025.04.16 |
---|---|
[Compose] 1-3. Compose State/Stateful/Stateless/ViewModel/remember* (0) | 2025.04.04 |
[Compose] 1-1. Jetpack Compose Basics (0) | 2025.03.26 |
[Kotlin] 알아두면 편한 Kotlin 기능 3개 소개 (0) | 2025.03.09 |
[Android] Google Login API 사용해보기 (0) | 2023.05.08 |