Week 2-1 Layouts in Jetpack Compose (1)

jihyo·2021년 11월 17일

DevFest 2021

목록 보기
4/8

Modifier (수정자)

Modifier는 Composable을 꾸밀 때 사용하며 일반 Kotlin 객체이다. 변수에 할당하고 재사용할 수 있으며, 여러 Modifier를 차례로 연결하여 사용할 수도 있다.

  • 동작, 모양을 변경
  • 접근성 레이블 같은 정보 추가
  • 사용자 입력을 처리, 클릭/스크롤/드래그/확대/축소 가능과 같은 High-Level 상호작용

이번에는 아래와 같은 프로필 형식을 구현해볼 것이다.
profile

우선 프로필의 이름과 시간을 먼저 만든다.

@Composable
fun PhotographerCard() {
    Column {
        Text(text = "Alfred Sisley", fontWeight = FontWeight.Bold)
        // LocalContentAlpha는 자식의 불투명도 수준을 정의한다.
        CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            Text(text = "3 minutes ago", style = MaterialTheme.typography.body2)
        }
    }
}

@Preview
@Composable
fun PhotographerCardPreview() {
    LayoutComposeCodelabTheme {
        PhotographerCard()
    }
}

profile preview (1)

setContent 내에서 사용되는 앱 테마는 프로젝트명에 따라 다르다. 앱 테마를 확인하기 위해서는 ui/Theme.kt 파일에서 확인할 수 있다.

위 코드에서 CompositionLocalProvider를 사용했는데, 컴포지션 트리를 통해 암시적으로 데이터를 전달할 수 있다. 이 경우 MaterialTheme에 의해 테마 수준에서 정의된 중간 불투명도 수준인 ContentAlpha.medium에 접근한다.

이미지가 로드되는 동안 자리 표시자(placeholder)를 표시할 수 있다. 원 모양과 자리 표시자 색상을 지정하는 Surface를 사용할 수 있다. 크기를 지정하려면 크기 수정자(size)도 사용해준다.

@Composable
fun PhotographerCard() {
    Row {
        Surface(
            modifier = Modifier.size(50.dp),
            shape = CircleShape,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
        ) {
            // Image 들어갈 곳
        }
        Column {
            ~
        }
    }
}

profile image placeholder

그림에서 보이다시피 자리 표시자와 텍스트의 위치가 보기 좋은 위치에 있지 않다. 그래서 조정을 해보려 한다.
1. 자리 표시자와 텍스트 사이에 공간을 추가
2. 텍스트가 수직으로 중앙에 위치

자리 표시자와 텍스트 사이에 공간을 추가
텍스트가 포함된 ColumnModifier.padding을 사용하여 이미지와 텍스트를 구분하기 위해 Composable 시작 부분에 공간을 추가

텍스트가 수직으로 중앙에 위치
일부 레이아웃은 레이아웃 특성과 그 특성에만 적용할 수 있는 modifier를 제공한다. 예를 들어, Row의 Composaable은 weightalign같은 특정 modifier(Row 콘텐츠의 RowScope 수신기로부터)에 접근할 수 있다. 이 scoping(범위 지정)은 type-safety를 제공하므로 다른 레이아웃에서 의미가 없는 modifier를 실수로 사용할 수 없다. 예를 들어, weightBox에서 의미가 없으므로 compile-time error로 막힌다.

Modifier는 View 시스템의 XML 속성과 유사한 역할을 할 수 있지만 완벽히 같지는 않다. scope specific modifier(범위별 수정자)의 유형 안전성은 특정 레이아웃에 사용 가능하고 적용할 수 있는 것을 찾고 이해하는데 도움이 된다.

@Composable
fun PhotographerCard() {
    Row {
        Surface(
            modifier = Modifier.size(50.dp),
            shape = CircleShape,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
        ) {
            // Image 들어갈 곳
        }
        Column(
            modifier = Modifier
                .padding(start = 8.dp)
                .align(Alignment.CenterVertically)
        ) {
            Text(text = "Alfred Sisley", fontWeight = FontWeight.Bold)
            // LocalContentAlpha는 자식의 불투명도 수준을 정의한다.
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text(text = "3 minutes ago", style = MaterialTheme.typography.body2)
            }
        }
    }
}

Profile Image Placeholder edited

대부분의 Composable은 선택적 Modifier 매개변수를 허용하여 더 유연하게 만들어 호출자가 수정할 수 있도록 한다. 자신만의 Composable을 생성하는 경우 Modifier를 매개변수로 사용하는 것을 고려하고 기본적으로 Modifier(즉, 아무 작업도 수행하지 않는 빈 Modifier)로 설정하고 이를 함수의 Root Composable에 적용한다.

규칙(convention)에 따라 Modifier는 함수의 첫 번째 선택적 매개변수로 지정된다. 이는 모든 매개변수의 이름을 지정할 필요없이 Composable에 Modifier를 지정할 수 있다.

Order of modifiers matter 수정자의 순서가 중요하다

코드에서 팩토리 확장 함수를 사용하여 여러 modifier를 차례로 연결할 수 있는 방법에 주목해야 한다.
ex) Modifier.padding(start = 8.dp).align(Alignment.CenterVertically)

순서가 중요하기 때문에 수식어를 연결할 때 주의해야 한다. 단일 인수(single argument)로 연결되므로 순서가 최종 결과에 영향을 준다.

지금 만드려는 프로필을 클릭 가능하게 만들고 여백을 추가

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(modifier
        .padding(16.dp)
        .clickable(onClick = { /* do nothing */ })
    )

이 코드를 추가하면 클릭이 가능해진다. (onClick 블록 안을 비웠기 때문에 아무것도 하지 않는다.) 단, 현재 상태에서는 clickable 수정자 전에 padding이 적용되었기 때문에 우리가 사용하는 앱들처럼 모든 영역에서 클릭이 되지는 않는다. 이를 개선하기 위해 paddingclickable의 위치를 바꿔주면 된다.

명시적 순서는 다양한 modifier가 상호 작용하는 방식을 추론하는 데 도움이 된다. Box 모델에 대해 알아야 했던 View 시스템과 비교해 보면, margin은 요소 "외부"에 적용되지만 padding "내부"와 background element는 padding, background에 따라 크기가 변한다. modifier 디자인은 이 동작을 명시적이고 예측 가능하게 만들고 원하는 정확한 동작을 달성하기 위해 더 많은 control을 제공한다.

이번에는 콘텐츠가 외부와의 간격을 가지고 Composable의 배경색을 변경하고 Row의 모서리를 둥글게 만들어 보겠다.

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(modifier
        .padding(8.dp)
        .clip(RoundedCornerShape(4.dp))
        .background(MaterialTheme.colors.surface)
        .clickable(onClick = { /* do nothing */ })
        .padding(16.dp)
    )

profile final

Slot APIs

Compose는 UI를 빌드하는 데 사용할 수 잇는 High-Level Material Components를 제공한다. UI 생성을 위한 빌딩 블록이므로 우리는 화면에 표시할 정보를 제공해줘야 한다.

Slot APIs는 사용 가능한 Material Components Composable 위에 사용자 정의 레이어를 가져오기 위해 Compose가 도입한 패턴이다.

Material Button를 예로 들자면, 버튼이 어떻게 보이고 뭘 포함해야 되는지에 대한 가이드라인이 있다. 이를 간단한 API로 변환해 사용할 수 있는데,

Button(text = "Button")

아래처럼 더 커스터마이징하고 싶어 각 개별 요소에 매개변수를 사용해 추가할 수 있지만 빠른 해결책은 아니다.

Button(
    text = "Button",
    icon: Icon? = myIcon,
    textStyle = TextStyle(...),
    spacingBetweenIconAndText = 4.dp,
    ...
)

여러 매개 변수를 추가하는 대신 슬롯을 추가했다.

  • 슬롯은 개발자가 원하는 대로 채울 수 있도록 UI에 빈 공간을 남긴다.

아래처럼 icon과 text가 있는 행을 삽입하려는 사용자가 버튼 내부를 채울 수 있다.

Button {
	Row {
    	MyImage()
        Spacer(4.dp)
        Text("Button")
    }
}

이를 가능하게 하기 위해서 자식 composable 람다(content: @Composable() -> Unit)를 취하는 Button용 API를 제공한다. Button 내에서 방출될 자신만의 Composable을 정의할 수 있다.

@Composable
fun Button(
    modifier: Modifier = Modifier,
    onClick: (() -> Unit)? = null,
    ...
    content: @Composable () -> Unit
)

content라는 이름을 지정한 람다가 마지막 매개변수이다. 이를 통해 후행 람다 구문을 사용하여 콘텐츠를 구조화된 방식으로 Button에 삽입할 수 있다.

Compose는 Top App Bar같은 복잡한 구성요소에서 슬롯을 많이 사용한다.
top app bar
여기서 사용할 수 있는 부분은 아래와 같다.
top app bar slot

TopAppBar(
    title = {
        Text(text = "Page title", maxLines = 2)
    },
    navigationIcon = {
        Icon(myNavIcon)
    }
)

Composable을 구축할 때 Slots API pattern을 사용하면 더 많은 재사용이 가능하다.

0개의 댓글