내일배움캠프 앱 개발 심화 팀 프로젝트
서론
대부분의 API들은 수많은 검색결과를 불러와야 할 때, 한 번에 불러오는 게 아닌, page query 파라미터를 추가로 이용해서 조금씩 데이터를 불러오도록 처리한다.
이런 형식으로 데이터를 불러오는 이유는 많은 데이터를 여러 페이지로 나누어서 로딩 속도를 개선하고 사용성을 좋게 하기 위해서이다.
이는 기본적인 내용이긴 하지만 그냥 써봤다.
page 파라미터로 나뉘어 있는 검색결과들을 Android에 무한 스크롤을 적용해서 끝없는 데이터를 계속 불러올 수 있도록 만들어 보자.
무한 스크롤
무한 스크롤은 우리가 자주 사용하는 instagram, youtube처럼 스크롤을 계속 내려도 데이터가 계속 나오는 것을 뜻한다.
무한 스크롤을 구현하기 위해서는 첫 데이터가 recyclerView에 뜨고 나서 스크롤을 최하단으로 내렸을 때, 다음 페이지의 데이터를 호출해서 recyclerView에 띄워 주는 것을 반복해 주면 무한 스크롤이 완성된다.
위 사진이 무한 스크롤의 예이다.
무한 스크롤을 구현해서 수많은 데이터를 불러오고 사용성을 개선시켜 보자.
무한 스크롤 구현
무한 스크롤을 구현하기 위해서는 데이터가 page마다 구분되어 있는 데이터들이 있어야 한다. page가 숫자 형식이어도, string 형식이어도 상관없다.
xml에 무한 스크롤을 구현할 recyclerView 아래에 circular loading indicator를 생성한다.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_result_rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingHorizontal="16dp" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/search_more_loading_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
android:layout_marginVertical="12dp"
app:indicatorColor="@color/button_unselected"/>
추가 로딩(페이지 전환)이 필요할 때 이 circular progress indicator의 visibility를 변경해 가면서 이용할 계획이다.
Adapter 쪽은 그냥 특별한 작업 없이 Adapter 구현하듯이 구현해 주면 된다. 일단 나는 ListAdapter를 이용해 Adapter를 구현했다. 코드는 생략한다.
UI 코드에서는 state에 따른 변화를 이용해 xml의 circular progress indicator의 visibility를 변화시킬 것이다.
private fun observeSearchState() = with(binding) {
viewLifecycleOwner.lifecycleScope.launch {
searchViewModel.uiState.collectLatest {
when (it) {
is SearchUiState.Success -> {
searchCategoryRvNestedScrollHost.visibility = View.VISIBLE
searchingLoadingIndicator.visibility = View.GONE
searchResultStatusText.visibility = View.GONE
searchResultRv.visibility = View.VISIBLE
searchMoreLoadingIndicator.visibility = View.GONE
nextPage = it.searchResultModel.nextPageToken.toString()
}
is SearchUiState.LoadingMore -> {
searchMoreLoadingIndicator.visibility = View.VISIBLE
searchResultRv.smoothScrollToPosition(searchResultAdapter.itemCount - 1)
}
. . .
}
}
}
}
LoadingMore state일 때는 visibility를 visible로 설정하고 scrollPosition을 마지막 item으로 옮겨 준다.
smoothScrollToPosition을 쓰는 이유가 loading indicator가 visible 되면서 순간적으로 recyclerView의 일부가 가려지는데 이를 해결하기 위해 자동으로 스크롤이 마지막으로 이동할 수 있도록 해주는 조치이다.
제일 중요한 내용인데, 무한 스크롤을 발동시키기 위해서는 recyclerView의 scroll이 맨 마지막이어야 한다.
binding.searchResultRv.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) {
if (!isMoreLoading && nextPage != null) {
val query = binding.searchingEt.text.toString()
searchViewModel.searchResultMore(query, nextPage!!, selectedCategory)
}
}
}
})
addonScrollListener로 scroll 할 때를 감지한다.
onScrollStateChanged 메서드를 이용한다.
recyclerView.canScrollVertically(1)은 스크롤이 최하단에 위치해 있는지 확인한다. 두 번째 조건은 newState가 RecyclerView.SCROLL_STATE_IDLE일 때인데 이는 중복된 호출을 방지해 준다.
구현 중 발생한 문제 상황
무한 스크롤 구현 중 state 쪽에서 애를 많이 먹었다.
한 번에 여러 번 recyclerView가 갱신되고, scroll state가 초기화되는 문제가 있었는데, 이 문제는 state에 recyclerView 적용 코드를 넣어놔서 state가 변할 때마다 계속 adapter가 새로 구성돼서 발생하는 문제였다.
기존에는 xml에 따로 indicator를 넣는 게 아닌, recyclerView item으로 넣고자 했지만, 무슨 이유에서인지 listAdapter의 submitList가 먹히지 않았다. 그런데 검색 결과는 또 잘 작동해서 적용되더라.
원인을 찾지 못해서 다른 방안을 찾던 중 recyclerView 영역 바로 아래에 추가하는 방법을 찾게 되었다.
마지막으로 이제 구현한 화면을 확인해 보자.
정리
무한 스크롤을 구현해 봤는데 compose에서 구현할 때보다 고려해야 하는 것들이 많았다.
state 관리에 더 신경을 써야 된다는 느낌이었다.
또한 따로 loading indicator를 만들어서 넣었기 때문에 무한 스크롤은 잘 동작하지만 뭔가(?) 부자연스러웠다.
비동기로 viewModel의 로직을 수행하기 때문에 viewModel 로직이 여러 번 호출되게 되면 상당한 낭비가 있을 것이다.
원인을 찾아서 잘 해결해야 한다. 이 부분도 해결하는데 시간이 좀 걸렸다.
'⛏️ | 개발 기록 > 🎬 | 부트캠프 심화 팀 프로젝트' 카테고리의 다른 글
[Android] ViewModel과 UiState를 이용해 상황에 맞는 화면 띄우기 (1) (0) | 2024.08.16 |
---|