Android

안드로이드 [Kotlin] - Jetpack Compose 코트랩 실습(1)

🤖 Play with Android 🤖 2023. 5. 7. 20:25
728x90


코드랩 실습 (Layout In Jetpack Compose)

시작하기

  • 표준 레이아웃 컴포넌트들을 사용하여 다음과 같은 UI를 만들 수 있다.
image

Modifiers

  • 검색창을 구현하려면 TextField라는 Material 구성 요소를 사용해야 한다.
  • Compose Material 라이브러리에는 이 Material 구성요소의 구현인 TextField 라는 컴포저블이 존재한다.
@Composable
fun SearchBar(
    modifier: Modifier = Modifier
) {
    TextField(
        value = "",
        onValueChange = {},
        modifier = modifier
            .fillMaxWidth()
            .heightIn(min = 56.dp)
    )
}
  • 검색창은 높이가 최소 56dp이고 너비가 상위 요소의 전체 너비를 차지한다.
  • 높이는 heightIn 설정으로 최소 높이를 제한할 수 있다. (단, 사용자가 시스템 글꼴 크기를 확대하면 크기가 커질 수 있다.)
  • 너비로는 fillMaxWidth Modifier 을 사용할 수 있다.
  • 여기서 하나의 Modifier는 너비에 영향을 주고 하나는 높이에 영향을 주기 때문에 순서는 중요하지 않다.

이제 조금 더 업그레이드를 해보자

@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)
   )
}
  • 검색 아이콘을 추가한다.
  • TextField에는 다른 컴포저블을 받는 매개변수 leadingIcon이 있다.
  • 내부에는 Icon을 설정할 수 있다. 여기서는 Search 아이콘을 설정한다. 올바른 Compose Icon import를 사용해야 한다.
  • 텍스트 필드의 배경색을 MaterialTheme의 surface 색으로 설정한다. TextFieldDefaults.textFieldColors를 사용하여 특정 색상을 재정의할 수 있다.
  • 자리표시자 텍스트(EditText의 Hint 역할) 'Search'를 추가한다(문자열 리소스 R.string.placeholder_search).

Alignment

image
  • 텍스트의 기준선과 이미지의 기준선 사이의 간격은 24dp 이다.
  • 텍스트의 기준선이란 문자가 '놓여' 있는 선을 가리킵니다. 디자이너들은 주로 상단이나 하단이 아닌 기준선을 기준으로 텍스트 요소를 정렬한다.

  • 이 컴포저블을 구현하려면 Image 및 Text 컴포저블이 필요하다.
  • 이 두 가지 컴포저블을 세로 방향으로 배치하려면 Column에 포함해야 한다.
@Composable
fun AlignYourBodyElement(
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier
    ) {
        Image(
            painter = painterResource(R.drawable.photo_0),
            contentDescription = null
        )
        Text(
            text = stringResource(R.string.ab1_inversions)
        )
    }
}
  • 이 이미지는 순전히 장식용이므로 contentDescriptionnull 로 설정하였다.

이 이미지는 우리가 원하는대로 사각형이 아니다. 이미지가 너무 크고 원형이 아니다.

  • sizeclip Modifier 와 contentScale 매개변수를 사용하여 Image 컴포저블을 조정할 수 있다.
  • size 수정자는 앞 단계에서 본 fillMaxWidthheightIn 수정자와 마찬가지로 특정 크기에 맞게 컴포저블을 조정한다.
  • clip 수정자는 이와 달리 컴포저블의 모양을 조정한다. 이 수정자를 원하는 Shape으로 설정하면 컴포저블의 콘텐츠가 이 도형에 맞춰 잘른다.

이미지의 크기도 올바르게 조정해야 합니다. Image의 contentScale 매개변수를 사용하면 됩니다. 여러 가지 옵션이 있다.

image
@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)
       )
   }
}
image

Column 의 경우 하위 요소를 가로로 정렬할 방법을 정해야 한다.

  • Start
  • CenterHorizontally
  • End

Row 의 경우 세로 정렬을 설정해야 한다.

  • 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(
           //..
       )
   }
}
  • 이미지와 텍스트를 동적으로 만들고. 컴포저블 함수에 인수로 전달하도록 해보자.
@Composable
fun AlignYourBodyElement(
    @DrawableRes drawable: Int,
    @StringRes text: Int,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier
    ) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = modifier
        ) {
            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
                )
            )
        }
    }
}
  • 리소스를 컴포저블 함수의 인자로 넣기 위해 @DrawableRes, @StringRes 로 설정해준다.

Material Surface

image
  • 이 컨테이너에는 화면의 배경과 다른 배경색이 적용되어 있다.
  • 모서리는 둥글게 처리되었다.
  • 디자이너가 색상을 지정하지 않았으므로 색상이 테마에 의해 정의될 것이라고 가정할 수 있다.

이러한 컨테이너로는 Material의 Surface 컴포저블을 사용한다.

  • Surface는 요소를 감싸는 컨테이너와 같은 역할을 한다.
  • 배경색이나 배경 테두리 등을 설정할 수 있다.

  • Surface는 매개변수와 수정자를 설정하여 필요에 맞게 조정할 수 있다.
  • 여기서는 모서리를 둥글게 처리해야 한다.
  • 이를 위해 shape 매개변수를 사용할 수 있다.
@Composable
fun FavoriteCollectionCard(
    @DrawableRes drawable: Int,
    @StringRes text: Int,
    modifier: Modifier = Modifier
) {
    Surface(
        shape = MaterialTheme.shapes.medium,
        modifier = modifier
    ) {
        Row(
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.width(192.dp)
        ) {
            Image(
                painter = painterResource(drawable),
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier.size(56.dp)
                    .clip(CircleShape)
            )
            Text(
                text = stringResource(text),
                style = MaterialTheme.typography.body2,
                modifier = Modifier.padding(horizontal = 16.dp)
            )
        }
    }
}
image

Arrangements

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

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}
image
  • 디자인에서 보았던 간격은 적용되어 있지 않다.
  • 간격을 구현하려면 배치에 대해 알아야 한다.

Row
image


Column
image


  • 이러한 배치 외에도Arrangement.spacedBy() 메서드를 사용하여 각 하위 컴포저블 사이에 고정된 공간을 추가할 수 있다.
@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp), // 사진들 사이에 8dp의 공간을 주었다.
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}
  • 이제 디자인은 다음과 같다.
image

Lazy 그리드

image
  • 이 컴포저블에는 단일 행이 아닌 그리드가 필요하다.
  • 그리드 요소 매핑을 더 효과적으로 지원하는 LazyHorizontalGrid를 사용해보자.
@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       modifier = modifier
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text)
       }
   }
}
  • 앞 단계의 LazyRow를 LazyHorizontalGrid로 바꾸기만 한 것이다.
image
  • 우리가 원하는 결과가 나오지 않는다.
  • 그리드가 상위 요소의 전체 공간을 자치한다.
  • 즉, 즐겨찾는 컬렉션 카드가 세로 방향으로 지나치게 늘어져 있는 것을 확인할 수 있다.

그리드 셀이 올바른 크기와 셀 간 간격을 갖도록 컴포저블을 조정해야 한다.

@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)
           )
       }
   }
}

Slot API

image
  • 각 섹션에는 제목과 슬롯이 존재한다.
  • 섹션은 섹션에 따라 달라지는 콘텐츠로 동적으로 채울 수 있다.

이렇게 유연한 섹션 컨테이너를 구현하려면 슬롯 API를 사용해야 한다.

슬롯 기반 레이아웃은 개발자가 원하는 대로 채울 수 있도록 UI에 빈 공간을 남겨 둔다. 슬롯 기반 레이아웃을 사용하면 보다 유연한 레이아웃을 만들 수 있다.

@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 라는 이름을 지정한 이 람다표현식이 마지막 매개변수로 있는데, 후행 람다 구문을 사용하여 구조화된 방식으로 Column에 콘텐츠를 삽입할 수 있다.
  • 자신만의 컴포저블을 만들 때, 더욱 재사용 가능하게 만들기 위해 Slots API 패턴을 사용할 수 있다.

스크롤

  • 대부분의 기기는 전체 UI를 나타내기에 부족하다.
  • 따라서 스크롤 동작을 추가해야한다.

  • verticalScroll 혹은 horizontalScroll 수정자를 사용한다.
  • 또한 스크롤 상태를 저장하는데애 rememberScrollState를 사용한다.
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(
       modifier
           .verticalScroll(rememberScrollState()) // 수직 스크롤을 설정하면서 rememberScrllState 설정
           .padding(vertical = 16.dp)
   ) {
       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))
   }
}

Bottom Navigation

image
  • 이 컴포저블은 처음부터 구현하지 않아도 된다.
  • Compose Material 라이브러리의 일부인 BottomNavigation 컴포저블을 사용하면 된다.
  • BottomNavigation 컴포저블 내에서 하나 이상의 BottomNavigationItem 요소를 추가하면 Material 라이브러리에 의해 자동으로 스타일이 지정되게 된다.
@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 = {}
       )
   }
}
image
  • 먼저 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 = {}
       )
   }
}

Scaffold

  • Scaffold는 앱을 위한 구성 가능한 최상위 수준의 컴포저블이다.
  • TopAppBar, BottomAppBar, FloatingActionButtonDrawer 와 같은 일반적인 최상위 머테리얼 컴포넌트에 대한 Slot을 제공한다.
  • Scaffold를 사용하면 이러한 컴포넌트들을 배치하고 올바르게 동작하도록 한다.

컴포즈에는 앱을 만드는데 필요한 개발자가 사용할 수 있는 머테리얼 컴포넌트 컴포저블이 딸려있다. 가장 높은 수준의 컴포저블은 Scaffold다.

@Composable
fun MySootheApp() {
   MySootheTheme {
       Scaffold(
           bottomBar = { SootheBottomNavigation() }
       ) { padding ->
           HomeScreen(Modifier.padding(padding))
       }
   }
}
  • Scaffold안에 위에서 만들었던 바텀바와 메인 홈 화면이 들어가는 것을 확인할 수 있다.