Jetpack Compose 에서 TextField를 이용하여 자동 검색 기능 구현 하기 (기존 xml 에서의 방식과 비교) - 2

이지훈·2023년 6월 30일
0

테스트를 하던 중 Recyclerview 와 LazyColumn 을 통해서 리스트로 뿌리는 화면을 구현했을 때 동작의 차이를 발견할 수 있었는데

검색어를 입력한 후에 어느정도 스크롤을 내린 뒤, 새로운 검색어를 입력할 경우, 기존 Recyclerview 를 통한 방식의 경우 별도의 코드 추가 없이 리스트가 갱신되었을 경우, 스크롤이 최상단으로 이동하는 것을 확인할 수 있었는데, LazyColumn 은 이전의 스크롤 위치를 유지하였다.

똑같은 화면, 기능을 만드는 것이 목표이기에 Compose 쪽에 스크롤 관련 추가 작업을 해주기로 하였다.

LazyColumn 사용할 경우 이 같은 작업을 LazyListState 를 이용하여 animateScrollToItem(0) 함수 호출을 통해 구현할 수 있다.
단, Compose 에서 animateTo~ 와 같이 animate prefix 가 붙은 함수들은 suspend 키워드가 붙어있기 때문에 코루틴 스코프내에서 호출해줘야 하며, 언제 이 함수를 호출할지 그 트리거를 정해야 한다.

첫번째로 들었던 생각은 검색어가 변경되면 API 가 호출될 것이기 때문에 리스트가 갱신될 것이고 그렇다면 리스트를 LaunchedEffect 의 key 로 설정하면 가능할 것이라는 생각이 들어 다음과 같이 코드를 작성해보았다.

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun VideoScreen(
    videos: LazyPagingItems<VideoItem>,
    onClickSeeVideoDetail: (String) -> Unit,
) {
    val listState = rememberLazyListState()

	// key 값을 LazyPagingItems 로 두어 변화가 발생할 경우 block 내의 함수를 실행
    LaunchedEffect(key1 = videos) {
    	// 리스트의 스크롤을 최상단으로 이동
        listState.animateScrollToItem(0)
    }

	// LazyColumn 에 LazyListState 연동
    LazyColumn(state = listState) {
        items(
            count = videos.itemCount,
            key = videos.itemKey(key = { video -> video.url }),
            contentType = videos.itemContentType()
        ) { index ->
            val item = videos[index]
            item?.let {
                VideoCard(videoItem = it, onClick = onClickSeeVideoDetail)
            }
        }
    }
}

위와 같이 작성한 뒤에 제대로 동작하는지 확인을 해봤지만, 스크롤은 그대로 이전의 위치를 유지하였다.
animateScrollToItem(0) 함수 자체가 문제는 아닐 것이므로, 트리거가 동작을 하지 않았다는 것을 예측할 수 있었는데, 그렇다면 videos: LazyPagingItems<VideoItem> 가 변하지 않았다는 것인가?

맞다. 변하지 않았다.

자료들을 통해 조사를 해본 결과, LaunchedEffect는 key 가 인스턴스일 경우, 그 인스턴스가 변하였는지를 판단한다. LazyPagingItems 는 mutable 한 타입이기 때문에 그 값이 변해도 동일한 인스턴스를 유지한다. 따라서 인스턴스가 변하지 않았기 때문에 트리거가 되지 않은 것이다.

만약 key 로 사용된 값이 data class일때, data class 의 필드들의 값이 변하지 않아도, 새로운 인스턴스를 생성했다면 값이 변했다고 판단하여 트리거가 작동하게 된다.

그렇다면?
immutable 타입의 인스턴스, 또는 원시 타입의 key 를 사용해야 원하는 동작을 실행시킬 수 있다.

immutable 타입의 객체는 값이 변할 수 없기 때문에, 객체의 값을 수정하게 될 경우 새로운 인스턴스가 생성된다.

String type은 immutable 하기 때문에 String 타입인 DebouncedSearchQuery 를 key 로 선정하였다.
SearchQuery 는 입력에 따라 즉시 변경되는 값이기 때문에 원하지 않는 타이밍에 스크롤의 변화가 생길 수 있기 때문이다.

변경된 코드는 다음과 같다.

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun VideoScreen(
    debouncedSearchQuery: String,
    videos: LazyPagingItems<VideoItem>,
    onClickSeeVideoDetail: (String) -> Unit,
) {
    val listState = rememberLazyListState()

    // videos 는 값이 변해도 인스턴스가 동일하기 때문에 변하지 않음
    // debouncedSearchQuery 를 key 로 사용해야 함
    // String 은 immutable 타입이기 때문에 한번 생성된 문자열은 변경될 수 없으며, 문자열을 수정하면 새로운 문자열이 생성됨
    LaunchedEffect(key1 = debouncedSearchQuery) {
        listState.animateScrollToItem(0)
    }

    LazyColumn(state = listState) {
        items(
            count = videos.itemCount,
            key = videos.itemKey(key = { video -> video.url }),
            contentType = videos.itemContentType()
        ) { index ->
            val item = videos[index]
            item?.let {
                VideoCard(videoItem = it, onClick = onClickSeeVideoDetail)
            }
        }
    }
}

https://youtube.com/shorts/skF2baTXFwc?feature=share
(gif 파일 용량이 커서 유튜브 링크로 대체)

성공적으로 스크롤이 최상단에 위치되는 것을 확인할 수 있었다.

profile
실력은 고통의 총합이다. Android Developer

0개의 댓글