본문 바로가기
[Android]

Paging3 + Room 이틀동안 삽질 후 성공 정리

by Hevton 2023. 6. 24.
반응형

 

 

채팅 관련 앱을 개발하고 있었다

필요한 기능은 오프라인 페이징 기능이었다.

 

Room에서 데이터를 받아오는데, 모든 데이터를 한번에 받아오는게 아니라

필요한 데이터만 받아서 관리하는 페이징 기능이 필요했다.

 

예전에 커뮤니티성 앱을 만든 적이 있어서, 그 때를 참고하려 했었다.

그 때에는 10개의 실질적인 아이템이 있다면, getItemCount()는 11개를 리턴하도록 하여

가장 마지막 아이템 다음의 뷰는 ProgressBar 로 채웠고,

이 progressBar가 보여지기 시작하면 다음 10개의 아이템을 로딩하게 하는 페이징을 구현했었다.

그림으로 본다면 이런느낌!

 

 

하지만 이번에는 Android Jetpack의 Paging을 이용해서 구현하고 싶었다.

근데 이게 생각보다 여간 쉬운 일이 아니었다. 내가 원하는 방향으로 구현하기 위해서 이틀정도를 소비했다.

 

일반적인 페이징은 그냥 바로 이용하면 금방 할 수도 있곘지만, 내 앱은 채팅류 앱이라

채팅 데이터들이 시간 기준으로 순차적으로 보여지는 것이 아닌, 가장 최근의 데이터들이 보여져야 한다.

또한 스크롤을 아래가 아닌 위로 진행하면 그 이전의 채팅들이 또다시 보여져야 한다.

 

이게 생각했을 땐 간단하지만, 나름 복잡했다. 채팅방에 들어가는 순간 가장 최근 시간의 채팅들이 '순차적'으로 보여져야 하고

스크롤을 위로 올리면 그 이전의 채팅들이 또한 '순차적'으로 보여져야 하기 때문에, 정렬에 있어서 매우 까다로웠다.

 

 


 

우선 Paging3 라이브러리는 오프라인 데이터 기준으로 크게 세가지로 분류될 수 있다.

 

PagingSource : 데이터를 어디서 가져올지, 어느 방법으로 어떻게 가져올지를 정의해줌

Pager : PagingSource를 이용해서 PageData를 Flow형식으로 제공해서 페이징 데이터를 만들어줌

PagingDataAdapter : 페이징 전용 어댑터. 스크롤 정보에 따라 알아서 다음과 이전 데이터를 반영해줌

 

 

만약 Retrofit2 같은 온라인 데이터를 이용한다면 PagingSource 외에 RemoteMediator 같은 것을 이용할 수도 있다

https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=ko 

 

페이징 라이브러리 개요  |  Android 개발자  |  Android Developers

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 페이징 라이브러리 개요   Android Jetpack의 구성요소 페이징 라이브러리를 사용하면 로컬 저장소에서나 네트

developer.android.com

 

 

 

 

 

 

처음에는 room-paging 라이브러리를 이용해서 구현했다.

결론부터 말하자면 이는 내가 원하는 방법으로 구현되긴 어려웠다.

 

room-paging 라이브러리는, room 단에서 리턴 타입으로 PagingSource를 제공해 주는 방식인데

@Query("SELECT * from Chat WHERE chatGroup = :chatGroup")
fun getChats(chatGroup: String): PagingSource<Int, Chat>

PagingSource를 구현해야하는 어려움과 달리 간편해서 좋다.

 

하지만 단점은, Room 내의 어떤 데이터의 변경이 있을지라도, 저 getChats에 해당하는 쿼리 데이터의 변경이 없을지라도,

다시 호출이 되기 때문에,, 매우 불편하다. 물론 실시간 데이터 반영이 필요한 분들에겐 아주 큰 장점이 될 수도 있지만

나는 실시간 데이터 반영이 필요하지 않았을 뿐더러, 실시간 반영을 위해 불필요한 과정이 계속 반복되는 것을 선택하고 싶진 않았다.

 

이로 인에 distinctUtilChanged() 같은 '중복 호출 방지' 를 아무리 써봐도, PagingData를 커스텀하여 동등 여부를 판단할 수 없기 때문에 아주 불가능하다고 판단되었다. 이곳에서 정말 긴 삽질의 시간을 보냈다.

 


 

 

결국엔 PagingSource를 직접 구현하는 것으로 해결할 수 있었다. 

처음부터 모두 코드를 천천히 정리!!

 

 

1. build.gradle에 dependency를 추가해준다

def paging_version = "3.1.1"
implementation("androidx.paging:paging-runtime:$paging_version")

 

2. Dao를 만들어준다 (suspend로 해줘도 되겠다)

@Query("SELECT * FROM Chat WHERE chatGroup = :chatGroup ORDER BY date DESC LIMIT :loadSize OFFSET :index * :loadSize")
fun getChats(chatGroup: String, index : Int, loadSize : Int): List<Chat>

index : 페이징이다. 0 1 2 이런식으로

loadSize : 몇 개씩 로드할지를 정한다

그래서 SQL문의 OFFSET을 통해 이를 활용한다.

 

3. PagingSource를 정의해준다

class ChatPagingSource @Inject constructor(
    private val repository: Repository,
    private val chatGroup: String
) : PagingSource<Int, Chat>() {

    private companion object {
        const val INIT_PAGE_INDEX = 0
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Chat> {
        val pageNumber = params.key ?: INIT_PAGE_INDEX

        return try {

            val chat = repository.getChats(chatGroup, pageNumber, params.loadSize)


            val prevKey = if (pageNumber == INIT_PAGE_INDEX) null else pageNumber -1
            val nextKey = if (chat.isNullOrEmpty()) null else pageNumber + 1


            LoadResult.Page(
                data = chat,
                prevKey = prevKey,
                nextKey = nextKey
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Chat>): Int? {
        return null
    }
}

 

 

load 작동 원리는 다음과 같다.

https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data?hl=ko 

 

페이징된 데이터 로드 및 표시  |  Android 개발자  |  Android Developers

페이징된 데이터 로드 및 표시 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Paging 라이브러리는 대규모 데이터 세트에서 페이징된 데이터를 로드하고 표

developer.android.com

 

key는 페이지 넘버라고 생각하면 된다. 또는 인덱스라고 생각해도 된다.

prevKey는 이전 페이지가 있을지에 대한 처리

nextKey는 다음 페이지가 있을지에 대한 처리이다.

 

 

4. ViewModel단에서 Pager 처리

fun getChats(chatGroup: String) : Flow<PagingData<Chat>> {
    return Pager(config = PagingConfig(pageSize = 20, enablePlaceholders = false, initialLoadSize = 20, ), pagingSourceFactory = {
        ChatPagingSource(repository, chatGroup)
    }).flow
}

PagingConfig를 이용해서, 페이징 크기와 첫 로딩 사이즈를 정해놓는다. 이는 ViewModel단에서 정의해줘야한다.

 

5. Ui 단에서 수집 처리

viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
    viewModel.getChats(chatGroup).collectLatest {pagingData ->
        chatAdapter.submitData(pagingData) // 데이터 가져오며 Paging
    }
}

 

6. ChatAdapter 정의

class ChatAdapter() : PagingDataAdapter<Chat, ChatHolder>(CHAT_DIFF_CALLBACK) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatHolder =
        ChatHolder(
            ChatItemBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )

    override fun onBindViewHolder(holder: ChatHolder, position: Int) {
        val item = getItem(position)
        if(item != null) {
            holder.setData(item)
        }
    }
    

    companion object {
        private val CHAT_DIFF_CALLBACK = object : DiffUtil.ItemCallback<Chat>() {
            override fun areItemsTheSame(oldItem: Chat, newItem: Chat): Boolean {
                // Id is unique.
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Chat, newItem: Chat): Boolean {
                return oldItem == newItem
            }
        }
    }

}

class ChatHolder(private val binding: ChatItemBinding) :
    RecyclerView.ViewHolder(binding.root) {

    fun setData(chat: Chat) {
    
	// mapping
    
    }
}

 

7. RecyclerView 속성 reverlayout = true

app:reverseLayout="true"

 

Paging3가 재밌었던 점은, 스크롤을 올렸다가 내려도 새로 재요청이 될 수 있다는 것이다.

즉 정말 필요한 메모리만을 이용한다는 점이었다. 마치 RecyclerView와 메커니즘이 아예 똑같다는 느낌이다.

 

 

정말 오랜 시간 삽질하고 찾아보고 정리했습니다..

긴 시간 고생했습니다. 도움이 되셨다면 하트 부탁드립니다!

 

 

참고 (매우 도움이 됩니다)

https://velog.io/@hs0204/Paging-library%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

 

Paging library를 알아보자!

사용자에게 정보를 표시하는 일반적인 방법 중 하나는 list다. 하지만 list는 전체 콘텐츠를 보기 위한 일부의 작은 창과 같다. 사용자는 list에서 제공되는 정보를 스크롤할 때 더 많은 데이터가

velog.io

https://thinkerodeng.tistory.com/310

 

[Kotlin/Android] Paging 3 + Room DB

많은 데이터를 화면에 보여주기 위해, Paging 은 정말 중요하다. 모든 데이터를 그리게되면, 어플리케이션 성능이 저하되어, 사용자 이탈이 생길 뿐만 아니라 어플리케이션이 중단되는 현상까지

thinkerodeng.tistory.com

 

반응형