[Compose] 재사용 가능한 Composable 함수 작성하기

Choi Sang Rok·2022년 7월 6일
5

compose

목록 보기
2/5

Compose로 UI를 작성하면서 "Modifier' can't be called in this context by implicit receiver. Use the explicit one if necessary" 라는 이슈를 만나게 되었고, 그런 김에.. 재사용 가능한 Composable을 올바르게 작성하는 방법을 공유하려고 합니다.


1. 문제 파악하기


예시로 쉽게 파악할 수 있게, 토이프로젝트의 일부 화면을 가져왔습니다.
위 화면은 Compose로 작성된 UI입니다. 저는 여기서 반복적으로 사용되는 코드를 줄이고 이를 하나로 묶기 위해 Composable 함수를 작성하려고 합니다.

그러면 여기서 반복적으로 사용되는 컴포넌트는 무엇일까요?

흰색 배경에 Round Corner을 가지고 있는, 이 Box가 공통적으로 사용되고 있습니다.

Box(
    modifier = modifier
    		.fillMaxWidth()
            .height(68.dp)
            .clip(RoundedCornerShape(20.dp))
            .background(White),
        contentAlignment = alignment
    ) {
         Text(
                modifier = Modifier.padding(start = 16.dp),
                text = buildAnnotatedString {
                            withStyle(SpanStyle(fontSize = 14.sp, fontWeight = FontWeight.Bold)) {
                                append("지금은 땅이 쉬는중이에요\n")
                            }
                            append("커밋해서 잔디 씨앗을 뿌려주세요")
                        },
                color = Black,
                fontSize = 12.sp,
                fontFamily = suit,
                fontWeight = FontWeight.Normal
         )
    }
}

기본적으로 위 화면은 이러한 코드들로 이루어 지고 있습니다. 하나의 컴포저블을 작성하는데에는 이렇게 처리해도 그렇게 큰 문제는 없습니다.

그러나, 이러한 컴포저블이 많아졌을 때 round corner와 background를 컴포저블을 작성할 때 마다 설정해 주는것은 너무 비효율적입니다.
그래서 우리는 이를 재사용이 가능하도록 컴포저블 함수로 분리시키겠습니다.



2. Composable 함수 만들기

이런 함수를 하나 만들어 줍니다. 유동적으로 변화해야 하는 부분은 parameter로, 공통적으로 적용되는 부분은 블록 내에 작성될 것입니다.

먼저 공통점을 찾아야 겠죠?

1.  RoundCornerShape(20.dp)
2.  background(white)

이렇게 두개 정도가 될 것입니다.
하지만 크기나 정렬, 클릭 가능 여부 등, Modifier 의 속성같은 경우는 유동적으로 변경될 수 있어야 합니다.

예를 들면 아래 사진과 같이, 일정한 비율로 한 행에 존재할 수 있고, 들어갈 내용물도 달라야 합니다.

그러면 위의 것들을 종합하여 컴포저블 함수를 작성해 보겠습니다.

@Composable
internal fun RoundCornerBox(
    modifier: Modifier,
    alignment: Alignment = Alignment.CenterStart,
    content: @Composable BoxScope.() -> Unit
) {
    Box(
        modifier = modifier
            .clip(RoundedCornerShape(20.dp))
            .background(White),
        contentAlignment = alignment
    ) {
        content()
    }
}

함수의 인자로 modifier, alignment, content 를 받고 있습니다.

modifier 같은 경우 클릭 가능 여부나 크기 등이 바뀔 수 있으므로, 상태를 외부에서 전달 받을 수 있도록 합니다.

alignment 같은 경우, 기본적으로 CenterStart를 사용하는데 잔디 이미지같은 경우 아래에 위치하므로 필요 시 다른 것을 전달하면 됩니다.

content 를 통해서 다른 위젯들을 전달받을 수 있습니다.
그러나 여기서 중요한 점이 하나 있습니다.

content 들은 전달받아 Box 내부에서 사용됩니다. 그런데 여기서 수신 객체를 BoxScope 로 지정해 주지 않으면 다음 오류가 발생합니다.

왜냐하면 Box같은 경우, BoxScope 내에 지정된 자식들이 align 될 수 있으므로, 전달받은 인자들이 BoxScope에 포함된다는 의미로 수신객체를 지정해줘야 합니다.

@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
) 


3. 사용하기

RoundCornerBox(
        modifier = Modifier
               .fillMaxWidth()
               .height(106.dp),
       alignment = Alignment.BottomCenter
){
	Image(
         modifier = Modifier
                  painter = painterResource(id = R.drawable.refresh),
                  contentDescription = "refresh"
    )
    ...
}

사용할 때는 이런식으로 사용하면 됩니다. 결과적으로 background와 round corner을 계속 지정해 주지 않아도 유동적으로 사용할 수 있게 되었습니다!

profile
android_developer

3개의 댓글

comment-user-thumbnail
2022년 7월 6일

BoxScope, ColumScope, 리스코프 스코프가문이 문제네요

답글 달기
comment-user-thumbnail
2022년 7월 7일

저 오늘 잔디 심었습니다

답글 달기
comment-user-thumbnail
2022년 7월 10일

잔디에 금 칠하면 뭔지 아시나요?

답글 달기