LazyColumn를 직관적으로 사용해보자

akcineg·2025년 2월 15일

안드로이드 공부

목록 보기
11/12

스크롤 가능한 Android UI

요즘 대부분의 앱들은 제공하는 콘텐츠가 굉장히 많다. 제공해야 하는 콘텐츠가 많아지게 되면 당연히 하나의 화면에 제공해야 하는 UI가 늘어나게 된다. 하나의 화면에 많은 UI 컴포넌트를 담아내려면 세로 스크롤이 가능하게 한다

방대한 콘텐츠 -> 하나의 화면 안의 UI가 많아짐 -> 세로 스크롤 가능

그래서 웬만한 앱들의 메인화면에서는 스크롤을 많이 해야 찾고자 하는 콘텐츠를 확인할 수 있다. Compose로 스크롤 가능한 리스트 UI를 제공하기 위해서는 scrollState를 기반으로 modifier를 설정하여 스크롤 가능한 Column 혹은 Row를 만들거나, LazyColumn 혹은 LazyRow를 사용해야 한다.

가로, 세로 스크롤로 모두 콘텐츠를 제공할 수 있지만 일반적으로 세로 스크롤을 통해 콘텐츠를 제공한다.

메인 화면
	콘텐츠 1 타이틀
    	콘텐츠 1 리스트
        	콘텐츠 1 아이템 1
            콘텐츠 1 아이템 2
            콘텐츠 1 아이템 3
    콘텐츠 2 타이틀
    	콘텐츠 2 리스트
        	콘텐츠 2 아이템 1
            콘텐츠 2 아이템 2
            콘텐츠 2 아이템 3

이런 형식으로 화면을 구성하는 것을 많이 본 것 같다.

Compose를 활용한 스크롤 화면 개발

이러한 구성을 Compose를 기반으로 만드려고 한다면

부모 뷰는 Scroll 가능한 Column으로 만들고, 자식 뷰에 각 콘텐츠마다 LazyColumn으로 만들면 되겠네!

라고 생각할 수 있다. 나도 처음에는 그렇게 생각했다. 하지만 결과는?

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    TestScreen()
                }
            }
        }
    }
}


@Composable
fun TestScreen() {
    Surface(modifier = Modifier.fillMaxSize()) {
        val scrollState = rememberScrollState()
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(scrollState)
        ) {
            Text("컨텐츠 1 타이틀")
            LazyColumn {
                items(3) {
                    Text(
                        text = "컨텐츠 1 아이템 $it"
                    )
                }
            }
            Text("컨텐츠 2 타이틀")
            LazyColumn {
                items(3) {
                    Text(
                        text = "컨텐츠 2 아이템 $it"
                    )
                }
            }
        }
    }
}

에러 발생

java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a header before the list of items please add a header as a separate item() before the main items() inside the LazyColumn scope. There are could be other reasons for this to happen: your ComposeView was added into a LinearLayout with some weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a custom layout. Please try to remove the source of infinite constraints in the hierarchy above the scrolling container.
번역 : 수직으로 스크롤 가능한 구성 요소가 허용되지 않는 무한 최대 높이 제약 조건으로 측정되었습니다. 일반적인 이유 중 하나는 LazyColumn 및 Column(Modifier.verticalScroll())과 같은 중첩 레이아웃입니다. 항목 목록 앞에 헤더(앞의 코드로 치면 타이틀)를 추가하려면 LazyColumn 범위 내의 메인 items() 앞에 별도의 item()으로 헤더를 추가하세요. 생략...

이러한 메시지는 Android 공식 문서에도 찾을 수 있다. Android 공식 문서를 확인해보면 아주 친절하게 동일한 방향(세로) 그리고 고정되지 않은 크기의 스크롤의 가능한 UI 컴포넌트를 중첩시키지 말라고 되어 있다. 또한 '대신' 하나의 LazyColumn 안에 item과 items를 적절히 사용하여 컨텐츠를 배치하라는 제안을 주고 있다.

https://developer.android.com/develop/ui/compose/lists?hl=ko#avoid-nesting-scrollable

제안한 방식으로 수정

위의 예시를 참고해서 LazyColumn 하나만을 이용해서 방대한 양의 콘텐츠들을 하나의 화면에 담아낼 수 있을 것 같다. 앞서 작성한 코드를 에러가 발생하지 않게 수정한다면 다음과 같다.

@Composable
fun TestScreen() {
    Surface(modifier = Modifier.fillMaxSize()) {
        LazyColumn {
            item {
                Text("컨텐츠 1 타이틀")
            }
            items(3) {
                Text(
                    text = "컨텐츠 1 아이템 $it"
                )
            }
            item {
                Text("컨텐츠 2 타이틀")
            }
            items(3) {
                Text(
                    text = "컨텐츠 2 아이템 $it"
                )
            }

        }
    }
}

직관적이지 않는 문제 발생

저희 이제 콘텐츠 3,4,5 도 메인 화면에 들어가야 할 것 같아요. 그리고 콘텐츠들 사이에 구분선 ui도 들어가야 할 것 같아요!

라는 요청이 들어오게 되어 LazyColumn 안의 item, items 함수를 둘러싼 Composable 함수를 추가한다면

LazyColumn 코드가 길어지게 되어서 Compose의 장점인 직관성이 사라지는 것 같다.
그렇기 때문에 직관적으로 사용할 수 있도록 수정하는 것이 좋을 것 같다.

@Composable
fun TestScreen() {
    Surface(modifier = Modifier.fillMaxSize()) {
        LazyColumn {
            Content1(contentList1)
            Content2(contentList2)
            Content3(contentList3)
            Content4(contentList4)
            Content5(contentList5)
        }
    }
}

Composable 함수 Content(1~5)를 LazyColumn 안에 item, items를 사용하지 않고 직접 호출하는 것이 굉장히 직관적으로 보인다.

그런데 이렇게 사용 가능할까?

단순하게 코드를 작성하면 컴파일 에러가 발생한다. LazyColumn 함수 시그니쳐를 살펴보자

@Composable
public fun LazyColumn(
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    reverseLayout: Boolean = false,
    verticalArrangement: Arrangement. Vertical = if (!reverseLayout) Arrangement. Top else Arrangement. Bottom,
    horizontalAlignment: Alignment. Horizontal = Alignment.Start,
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
    userScrollEnabled: Boolean = true,
    content: LazyListScope.() -> Unit
): Unit

직관적인 사용

LazyColumn의 content의 타입 LazyListScope의 수신 객체 람다이다. 그렇기 때문에 Composable 함수가 들어가면 Type Mismatch 에러가 발생하는 것이다. 에러를 발생시키지 않으려면 타입을 맞추면 된다.
content 타입이 LazyListScope 수신 객체 타입이기 때문에 Content(1~5)의 함수를
LazyListScope 타입의 확장함수로 선언하면 된다. 그렇게 하면 item, items 함수를 사용할 수 있다.

// Composable 함수가 아니기 때문에 Content1가 아닌 content1로 설정
fun LazyListScope.content1(contentList1: List<Any>) {
    item {
        Content1Header()
    }
    items(contentList1) { item ->
        Content1Item(item)
    }
    item {
        Content1Footer()
    }
}

이렇게 함수를 선언하면 LazyColumn content 람다에 그대로 사용할 수 있게 되어 앞선 코드처럼 직관적으로 사용할 수 있게 된다
(물론, 함수 이름 컨벤션에 맞춰 이름을 ContentX 가 아니라 contentX로 설정)

profile
Android Developer

0개의 댓글