본문 바로가기
Development/Android

Android Compose - LazyColumn의 최하단 스크롤 이벤트 감지하기

by du.it.ddu 2023. 6. 10.
반응형

안드로이드 앱을 개발하면서 스크롤이 있는 레이아웃 요소는 흔하게 사용된다.
예를 들자면 ScrollView, RecyclerView와 같은 것들이다.

스크롤이 있는 레이아웃 요소를 사용할 때 스크롤에 대한 이벤트 처리, 특히 최하단에 도달했는지 여부를 확인해야하는 경우가 종종 있다.
이를테면 데이터가 페이징되어 있을 때다. 스크롤이 최하단에 도달했을 때 API 등을 호출하여 추가적인 데이터를 로드하고 스크롤 레이아웃에 아이템을 추가하는 것과 같은 것이다.

Android Compose는 이와 유사한 것으로 LazyColumn, LazyRow를 사용하곤 하는데, 둘은 스크롤 방향만 다르고 LazyColumn이 주로 사용될 것이다.

기존의 XML 레이아웃 요소와 다르게 LazyCoulmn은 어떻게 스크롤에 대한 이벤트를 감지하고, 최하단 스크롤 이벤트를 감지할 수 있는지 알아보자.


LazyListState

Android Compose는 State가 핵심 개념이다.
LazyColumn, LazyRow는 LazyListState라는 것으로 리스트의 상태를 알 수 있다.

@Composable
fun LazyColumn(
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    ...
) { ... }

@Composable
fun LazyRow(
    modifier: Modifier = Modifier,
    state: LazyListState = rememberLazyListState(),
    ...
) { ... }

class LazyListState constructor(
    firstVisibleItemIndex: Int = 0,
    firstVisibleItemScrollOffset: Int = 0
) : ScrollableState { ... }

LazyListState는 RecyclerView에서 스크롤 처리에 대해 사용했던 여러가지 속성이 내부에 있는 것을 확인할 수 있다.
자세한 내용은 직접 코드를 확인하는 것이 좋겠다.

중요한 것은 LazyListState를 통해 LazyColumn의 스크롤 상태의 변경을 알 수 있다는 것이다.


LazyListState로 스크롤 상태 알기

    val scrollState = rememberLazyListState()
    
    LazyColumn(
        ...
        state = scrollState,
    ) { ... }

참 친절하게도, 이미 rememberLazyListState() 가 구현되어 있기 때문에 손쉽게 LazyListState 를 생성할 수 있다.
생성한 LazyListState를 LazyColumn에 전달하면, LazyColumn 상태가 변경될 때 마다 LazyListState를 통해 스크롤 상태를 알 수 있다.


LazyListState로 최하단 스크롤 감지

val visibleItemsInfo = layoutInfo.visibleItemsInfo
val isAtBottom = if (layoutInfo.totalItemsCount == 0) {
    false
} else {
    val lastVisibleItem = visibleItemsInfo.last()
    val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset

    (lastVisibleItem.index == layoutInfo.totalItemsCount - 1) &&
    (lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)
}

스크롤이 끝에 도달했는지 여부는 위의 코드로 알 수 있다.

만약 아이템의 개수가 0이라면, 스크롤 자체가 존재하지 않으므로 최하단 도달 여부는 false이다.
아이템이 존재한다면, 현재 보여지는 마지막 아이템을 가져온 뒤 해당 아이템의 index가 마지막 index인지, 뷰포트의 높이와 마지막 아이템의 좌표를 비교하여 끝에 도달한지 여부를 체크한다.

LazyListState로 최하단 스크롤 이벤트 콜백

val isAtBottom = ...

LaunchedEffect(isAtBottom) {
    if(isAtBottom) {
        // Do something
    }
}

최하단 스크롤 여부는 LazyColumn이 스크롤될 때 마다 변경될 것이다.
따라서 LaunchedEffect에 해당 플래그값을 key로 주어 변경될 때 마다 이벤트가 발생할 수 있도록 한다.
그리고 LaunchedEffect 내부에서 플래그값이 true인 경우에 어떤 처리를 할 수 있도록 구현할 수 있다.

이렇게 하면 LazyColumn의 최하단 스크롤 여부에 대한 콜백 처리를 할 수 있다.
이를 활용하면 LazyRow에서도 동일하게 처리할 수 있을 것이다.

편하게 사용하기 위한 추가 구현

@Composable
fun LazyListState.isAtBottom(): Boolean {
    
    return remember(this) {
        derivedStateOf {
            val visibleItemsInfo = layoutInfo.visibleItemsInfo
            if (layoutInfo.totalItemsCount == 0) {
                false
            } else {
                val lastVisibleItem = visibleItemsInfo.lastOrNull() ?: return@derivedStateOf false
                val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset

                (lastVisibleItem.index == layoutInfo.totalItemsCount - 1) &&
                        (lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)
            }
        }
    }.value
}

먼저 LazyListState에서 최하단 스크롤 여부 플래그값을 얻는 extension 함수를 생성한다.
A 상태가 변경될 때, 다른 어떤 B 상태로 변경할 수 있도록 하는 derivedStateOf를 활용한다.


@Composable
fun ScrollToEndCallback(scrollState: LazyListState, callback: () -> Unit) {
    val isAtBottom = scrollState.isAtBottom()
    LaunchedEffect(isAtBottom) {
        if (isAtBottom) callback.invoke()
    }
}

스크롤 최하단 여부 플래그를 생성하고 LaunchedEffect 블록을 생성하여 이벤트를 호출하는 부분을 Composable 함수로 구현한다.
이렇게 함으로써 어느 Composable에서든 LazyListState와 callback만을 작성함으로써 콜백을 받아 처리할 수 있다.


@Composable
fun MyScreen(
    ...
) {
    val scrollState = rememberLazyListState()
    
    ScrollToEndCallback(scrollState) {
        // DO something
    }

    LazyColumn(
        state = scrollState,
    ) {
        ...
    }
}

사용법은 위와 같다.
LazyColumn에 LazyListScrollState를 전달하고, 그와 동일한 LazyListScrollState를 ScrollToEndCallback에 콜백함수와 함께 전달한다.

아주 간단하게 LazyColumn의 최하단 스크롤 이벤트 감지 완료!

반응형