Paging3 With MVVM

HEETAE HEO·2022년 7월 5일
0
post-thumbnail

Paging Library를 이용하여 RecyclerView를 출력하는 프로젝트에 대해 설명해보려고 합니다.

Paging Library란?

페이징 라이브러리는 로컬 데이터베이스 또는 네트워크의 데이터를 페이지 단위로 UI에 쉽게 표현할 수 있도록 도와주는 라이브러리입니다.

페이징 라이브러리를 사용하면 다음과 같은 이점을 얻을 수 있습니다.

  • 페이징된 데이터를 메모리 캐시에 담아 시스템 리소스를 효율적으로 사용가능하도록 해줍니다.

  • 요청 중복 제거 기능이 기본으로 제공되어 앱에서 네트워크 대역폭과 시스템 리소스를 효율적으로 사용할 수 있습니다.

  • 사용자가 로드된 데이터의 끝까지 스크롤할 때 구성 가능한 RecyclerView Adapter가 자동으로 데이터를 요청합니다.

  • Kotlin 코루틴 및 Flow 뿐만 아니라 LiveData 및 RxJava를 지원합니다.

  • 새로고침 및 재시도 기능을 포함하여 오류 처리를 기본으로 지원합니다.

    바로 프로젝트로 들어가보겠습니다.

Gradle

build.gradle(module)

// 버전의 차이는 있을 수 있습니다. 
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
    implementation 'androidx.paging:paging-runtime:3.2.0-alpha01'

Retrofit과 coroutine,paging 등을 implementation해줍니다.

그리고 ViewBinding을 사용할 것이기 때문에 다음과 같이 viewBinding을 true해줍니다.

android{
	  buildFeatures{
        viewBinding = true
    }
}

Retrofit

우선 Retrofit 통신을 통해 받아올 Data 형식에 맞게 Data Class들을 작성해줍니다.

RickAndMortyList.kt

data class RickAndMortyList(val info: Info, val results: List<CharacterData> )
data class CharacterData(val name: String?, val species: String?, val image: String?)
data class Info(val count: Int?, val pages: String?, val next: String?,val prev: String?)

다음은 Retrofit Instance를 생성해줍니다.

RetroInstance.kt

    companion object {
        val baseURL = "https://rickandmortyapi.com/api/"

        fun getRetroInstance(): Retrofit {
            return Retrofit.Builder()
                .baseUrl(baseURL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()

        }
    }

URL을 설정해주고 Retrofit을 통해 Instance를 만들어줍니다. 이때 데이터를 요청하면 API에서 전달해주는 데이터는 Json이기 때문에 GsonConverter를 통해 데이터를 쉽게 분리할 수 있게 해줍니다.

다음은 EndPoint를 지정해줘 데이터를 요청하도록하겠습니다.

RetroService.kt(interface)

 @GET("character")
    suspend fun getDataFromAPI(@Query("page") query: Int):RickAndMortyList

비동기 처리를 할 수 있도록 suspend를 붙여주곻
RickAndMortyList으로 타입을 정해줍니다.

Adapter

RecyclerView에 데이터를 넣기 위해 사용될 Adapter를 선언해줍니다.

RecyclerViewAdapter.kt

class RecyclerViewAdapter : PagingDataAdapter<CharacterData,RecyclerViewAdapter.MyViewHolder>(DiffUtilCallBack()) {

 class DiffUtilCallBack: DiffUtil.ItemCallback<CharacterData>() {
        override fun areItemsTheSame(oldItem: CharacterData, newItem: CharacterData): Boolean {
            return oldItem.name == newItem.name
        }

        override fun areContentsTheSame(oldItem: CharacterData, newItem: CharacterData): Boolean {
            return oldItem.name == newItem.name
                    && oldItem.species == newItem.species
        }

    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(getItem(position)!!)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val binding = RecyclerListBinding.inflate(LayoutInflater.from(parent.context),parent,false)

        return MyViewHolder(binding)
    }

여기서 DiffUtilCallBack이라는 클래스가 존재합니다. DiffUtil은 무엇을 하는 것일까

새로운 데이터의 추가 및 삭제가 되어 RecyclerView를 최신화하는 방법에는 여러가지 방법들이 있습니다. 추가의 경우 위치를 지정해 넣어주는 방법 삭제의 경우에도 삭제를 하고 순서를 수정하는 방법들 많습니다. 하지만 위의 2가지는 복잡하기 때문에 notifyDataSetChanged를 사용합니다. 데이터의 변경을 Adapter에 알려주면 RecyclerView 자체를 새로 재생성하며 변경된 부분이 수정되기 때문입니다.

하지만 위의 notifyDataSetChanged는 리스트 자체를 새로 만들기 때문에 데이터가 많이 변경되는 (예시로는 배달앱의 식당 리스트 정보들 -> 리뷰 수, 배달 시간, 찜 등) 곳에서는 적합하지가 않습니다.

그렇기에 DiffUtil을 사용하여 재생성을 최소한으로 해줍니다. 더 자세한 동작에 대해 궁금하시다면

해당 링크 눌러주세요

https://velog.io/@heetaeheo/ListAdapter

그런다음 MyViewHolder에서 데이터를 bind해줍니다.

MyViewHolder.kt

inner class MyViewHolder(binding: RecyclerListBinding) : RecyclerView.ViewHolder(binding.root){

        val imageView = binding.imageView
        val Name = binding.textTitle
        val Content = binding.textContent


        fun bind(data : CharacterData){
            Name.text = data.name
            Content.text = data.species

            Glide.with(imageView)
                .load(data.image)
                .circleCrop()
                .into(imageView)
        }
    }

PagingSource

 override fun getRefreshKey(state: PagingState<Int, CharacterData>): Int? {
        return state.anchorPosition
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CharacterData> {
        return try {
            val nextPage: Int = params.key ?: FIRST_PAGE_INDEX
            val response = apiService.getDataFromAPI(nextPage)

            var nextPageNumber: Int? = null
            if(response?.info?.next != null) {
                val uri = Uri.parse(response?.info?.next!!)
                val nextPageQuery = uri.getQueryParameter("page")
                nextPageNumber = nextPageQuery?.toInt()
            }
            var prevPageNumber: Int? = null
            if(response?.info?.prev != null) {
                val uri = Uri.parse(response?.info?.prev!!)
                val prevPageQuery = uri.getQueryParameter("page")

                prevPageNumber = prevPageQuery?.toInt()
            }

            LoadResult.Page(data = response.results,
                prevKey = prevPageNumber,
                nextKey = nextPageNumber)
        }
        catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
    companion object {
        private const val FIRST_PAGE_INDEX = 1
    }
  • load(params : LoadParams<Key.>
    -> load 함수는 사용자가 스크롤 할 때마다 데이터를 비동기적으로 가지고 옵니다.

-> LoadParams 객체는 로드 작업과 관련된 정보를 가지고 오는데 params.key에 현재 페이지 인덱스를 관리합니다. 처음 데이터를 로드 할 때는 null이 있습니다.

-> load함수는 LoadResult를 반환합니다.
LoadResult.Page : 로드에 성공한 경우 데이터와 이전 다음 페이지 key가 포함됩니다.

LoadResult.Error : 오류가 발생한 경우

  • getRefreshKey()

    -> 가장 최근에 접근한 인덱스인 anchorPosition으로 주변 데이터를 다시 로드합니다.

MainActivityViewModel

lateinit var retroService: RetroService

   init {
       retroService = RetroInstance.getRetroInstance().create(RetroService::class.java)
   }

   fun getListData(): Flow<PagingData<CharacterData>> {
       return Pager (config = PagingConfig(pageSize = 20, maxSize = 200),
           pagingSourceFactory = {CharacterPagingSource(retroService)}).flow.cachedIn(viewModelScope)
   }
  • PagingConfig
    -> pageSize : 각 페이지에 로드할 데이터의 수
    -> enablePalceholders : 플레이스 홀더 사용 여부
    -> maxSize : 기본적으로 모든 페이지를 메모리에 유지합니다. 메모리 낭비 등을 해결하기 위해 설정합니다.
  • pagingSouceFactory
    -> PagingSource 인스턴스를 생성합니다.

MainActivity

MainActivity의 경우 코드만 올려놓겠습니다.


    private lateinit var binding : ActivityMainBinding
    lateinit var recyclerAdapter: RecyclerViewAdapter


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initRecyclerView()
        initViewModel()
    }


    private fun initRecyclerView(){
        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            val decoration = DividerItemDecoration(applicationContext,DividerItemDecoration.VERTICAL)
            addItemDecoration(decoration)

            recyclerAdapter = RecyclerViewAdapter()
            adapter = recyclerAdapter

        }
    }


    private fun initViewModel(){
        val viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
        lifecycleScope.launchWhenCreated {
            viewModel.getListData().collectLatest {
                recyclerAdapter.submitData(it)
            }
        }
    }

전체코드를 보고 싶으시다면 아래의 깃 허브 주소를 클릭해주세요

https://github.com/heetaeheo/Jetpack-Paging3-with-MVVM.git

reference

https://developer.android.com/topic/libraries/architecture/paging/v3-overview

profile
Android 개발 잘하고 싶어요!!!

0개의 댓글