안드로이드 개발을 하다보면, RecyclerView를 사용할 때가 많다.
그런데 네트워크 통신을 통해 List 형태의 데이터를 받아올 때
너무 많은 데이터를 한 번에 받아오도록 구현할 경우에는
로딩이 오래 걸리고, 사용자로 하여금 데이터를 많이 사용하도록 하게 된다.
이를 방지하기 위해 Server의 API에서 페이지 번호를 나누어서 내려주게 되는데,
안드로이드에서는 이것을 받기 위해 예전에는 RecyclerView의 맨 아래에 도달했을 때
다음 Page의 데이터를 요청하는 방식으로 구현했다.
그런데 페이징 3.0 라이브러리를 사용하면 굳이 화면의 끝에 도달했는지 Check하지 않고 구현할 수 있다.
페이징 라이브러리 이점
- 페이징 라이브러리를 사용할 경우, 자동으로 메모리 내의 캐싱을 해줍니다.
따라서 페이징 데이터로 작업할 때 시스템 리소스를 효율적으로 사용할 수 있습니다. - 중복이 존재할 경우, 자동으로 제거됩니다. (DiffUtil을 기본적으로 사용합니다.)
- 사용자가 리스트의 끝까지 스크롤 했을 때 자동으로 데이터를 요청합니다.
- Coroutine, Flow, LiveData, RxJava를 지원합니다.
- 새로고침, 재시도를 포함해서 오류 처리를 기본으로 지원합니다.
페이징 라이브러리 아키텍처
페이징 라이브러리는 세 가지 레이어에서 작동한다.
- Repository Layer (저장소)
1. PagingSource : 페이징 라이브러리의 기본. 각 PagingSource 객체는 데이터를 검색하는 방법을 정의하게 됨.
PagingSource에서 데이터를 로드할 수 있다.
2. RemoteMediator : 로컬 DB의 캐시를 페이지로 처리할 목적으로 사용함. (ex. 네트워크 소스 캐시) - ViewModel Layer (뷰모델)
1. Pager : PagingSource, PagingConfig(PagingData를 가공할 때 설정하는 객체)를 바탕으로 PagingData 객체를 반응형 스트림에 노출되는 (ex. livedata / flow ...) 인스턴스로 구성하기 위한 API를 제공.
2. PagingData : PagingSource로 불러온 데이터를 담아두는 컨테이너. - UI Layer (화면)
- PagingDataAdapter : 리사이클러뷰의 RecyclerViewAdapter라고 생각하면 됨.
layout에서 recyclerview의 어댑터로 사용할 수 있음.
이제 코드를 살펴보면서 각 레이어 내부의 클래스들이 무슨 역할을 하는지 살펴보겠다.
의존성 추가
예시 프로젝트는 코틀린 기반이므로 -ktx를 붙인 의존성을 추가해야 한다.
코드 살펴보기
먼저 오늘의 결과물의 화면을 살펴보겠다.
화면 구성은 단순하게 텍스트뷰와 리사이클러뷰만 존재한다.
데이터를 불러오는 것은 네트워크를 통한 것이 아니라,
임의의 데이터를 불러와서 넣는 방식으로 구현해 보겠다.
API 클래스 & 데이터 JSON 형식
package com.example.paging3exampleproject.model
import com.example.paging3exampleproject.data.MyModel
interface MyService {
suspend fun getMyModel(pageId: Long): List<MyModel>
}
package com.example.paging3exampleproject.data
data class MyModel (val emoji: String, val idx: String)
PagingSource 클래스
package com.example.paging3exampleproject.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.example.paging3exampleproject.model.MyService
import kotlinx.coroutines.delay
import java.lang.Exception
class MyModelPagingSource(private val myService: MyService, private val pageSize: Int): PagingSource<Long, MyModel>() {
override fun getRefreshKey(state: PagingState<Long, MyModel>): Long? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, MyModel> {
return try {
delay(1000) // 1초의 딜레이
// 서버와 연동 없이 사용할때
val pageId = params.key?: 1
val response = myService.getMyModel(pageId)
LoadResult.Page(
data = response,
prevKey = null,
nextKey = pageId + 1
)
/* 서버와 연동할 때, */
// val pageId = params.key?: 1
// val response = myService.getMyModel(pageId)
// val myModelList = response.body()?: listOf()
//
// if(response.isSuccessful && myModelList.isNotEmpty()) {
// LoadResult.Page(
// data = myModelList,
// prevKey = null,
// nextKey = pageId + 1
// )
// } else {
// LoadResult.Page(
// data = myModelList,
// prevKey = null,
// nextKey = null
// )
// }
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
PagingSource 클래스는 데이터를 로드하는 방법을 정의하게 된다.
예외처리를 통해 return을 하게 되는데, gerRefreshKey 메서드와 load 메서드를 오버라이드 하게 된다.
- getRefreshKey : refresh() 메서드가 호출됐을 때 Key를 어떻게 return할 지를 정의하게 된다.
현재 PagingState에 따라 {이전 키 + 1}?: {다음 키 - 1}을 호출하게 된다. - load : 데이터를 로드하는 방법을 정의한다.
보통 Retrofit2를 통해 response에 정의되어 있는 List를 받아온 후 LoadResult.Page 형태로
리턴한다. LoadResult.Page는
LoadResult.Page({리스트 형태}, {이전 키}, {다음 키})의 형태이다.
만일 리스트가 비어있거나 response가 성공적이지 않다면 빈 리스트를 넘겨주고
이전 키, 다음 키를 null로 넘겨주면 된다.
이 프로젝트에서는 실제 서버와 통신하는 부분은 없으므로
실제 서버와 통신하는 코드는 주석 처리를 했다.
Repository 클래스
package com.example.paging3exampleproject.repo
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.paging3exampleproject.data.MyModel
import com.example.paging3exampleproject.data.MyModelPagingSource
import com.example.paging3exampleproject.model.MyService
import kotlinx.coroutines.flow.Flow
class MyRepository {
private val myService = object: MyService {
var lastIdx = 1
val emojiList = "👶 👧 🧒 👦 👩 🧑 👨 👩🦱 🧑🦱 👨🦱 👩🦰 🧑🦰 👨🦰 👱♀️ 👱 👱♂️ 👩🦳 🧑🦳 👨🦳 👩🦲 🧑🦲 👨🦲 🧔 👵 🧓 👴 👲 👳♀️ 👳 👳♂️ 🧕 👮♀️ 👮 👮♂️ 👷♀️ 👷 👷♂️ 💂♀️ 💂 💂♂️ 🕵️♀️ 🕵️ 🕵️♂️ 👩⚕️ 🧑⚕️ 👨⚕️ 👩🌾 🧑🌾 👨🌾 👩🍳 🧑🍳 👨🍳 👩🎓 🧑🎓 👨🎓 👩🎤 🧑🎤 👨🎤 👩🏫 🧑🏫 👨🏫 👩🏭 🧑🏭 👨🏭 👩💻 🧑💻 👨💻 👩💼 🧑💼 👨💼 👩🔧 🧑🔧 👨🔧 👩🔬 🧑🔬 👨🔬 👩🎨 🧑🎨 👨🎨 👩🚒 🧑🚒 👨🚒 👩✈️ 🧑✈️ 👨✈️ 👩🚀 🧑🚀 👨🚀 👩⚖️ 🧑⚖️ 👨⚖️ 👰♀️ 👰 👰♂️ 🤵♀️ 🤵 🤵♂️ 👸 🤴 🥷 🦸♀️ 🦸 🦸♂️ 🦹♀️ 🦹 🦹♂️ 🤶 🧑🎄 🎅 🧙♀️ 🧙 🧙♂️ 🧝♀️ 🧝 🧝♂️ 🧛♀️ 🧛 🧛♂️ 🧟♀️ 🧟 🧟♂️ 🧞♀️ 🧞 🧞♂️ 🧜♀️ 🧜 🧜♂️ 🧚♀️ 🧚 🧚♂️ 👼 🤰 🤱 👩🍼 🧑🍼 👨🍼 🙇♀️ 🙇 🙇♂️ 💁♀️ 💁 💁♂️ 🙅♀️ 🙅 🙅♂️ 🙆♀️ 🙆 🙆♂️ 🙋♀️ 🙋 🙋♂️ 🧏♀️ 🧏 🧏♂️ 🤦♀️ 🤦 🤦♂️ 🤷♀️ 🤷 🤷♂️ 🙎♀️ 🙎 🙎♂️ 🙍♀️ 🙍 🙍♂️ 💇♀️ 💇 💇♂️ 💆♀️ 💆 💆♂️ 🧖♀️ 🧖 🧖♂️ 💅 🤳 💃 🕺 👯♀️ 👯 👯♂️ 🕴 👩🦽 🧑🦽 👨🦽 👩🦼 🧑🦼 👨🦼 🚶♀️ 🚶 🚶♂️ 👩🦯 🧑🦯 👨🦯 🧎♀️ 🧎 🧎♂️ 🏃♀️ 🏃 🏃♂️ 🧍♀️ 🧍 🧍♂️ 👭 🧑🤝🧑 👬 👫 👩❤️👩 💑 👨❤️👨 👩❤️👨 👩❤️💋👩 💏 👨❤️💋👨 👩❤️💋👨 👪 👨👩👦 👨👩👧 👨👩👧👦 👨👩👦👦 👨👩👧👧 👨👨👦 👨👨👧 👨👨👧👦 👨👨👦👦 👨👨👧👧 👩👩👦 👩👩👧 👩👩👧👦 👩👩👦👦 👩👩👧👧 👨👦 👨👦👦 👨👧 👨👧👦 👨👧👧 👩👦 👩👦👦 👩👧 👩👧👦 👩👧👧 🗣 👤 👥 🫂"
.split(' ').toList()
override suspend fun getMyModel(pageId: Long): List<MyModel> {
val list = mutableListOf<MyModel>()
for(i in 0 until 12) {
val randIdx = (Math.random() * emojiList.size).toInt()
list.add(MyModel(emojiList[randIdx], (lastIdx++).toString()))
}
return list
}
}
fun getMyModelList(): Flow<PagingData<MyModel>> {
return Pager(PagingConfig(pageSize = 10, prefetchDistance = 3, enablePlaceholders = true)) {
MyModelPagingSource(myService, 10)
}.flow
}
}
myService 객체는 실제로는 Retrofit2 객체라고 생각하면 된다.
(보통 한 줄이면 정의가 된다. 여기서는 실제 서버와 연동하는 게 아니므로 코드가 길어졌다.)
getMyModelList() 메서드는 Coroutine Flow 형태로 데이터를 반환한다.
LiveData가 아니라 Flow 형태로 반환하는 이유는 다음과 같다.
- LiveData는 수명주기를 인식하기 위해 나온 컴포넌트이다.
따라서 Main Thread(UI Thread in Android)위에서 작동하는게 옳지만
데이터를 받아오는 Repository는 IO Thread 위에서 작동하므로 Flow로 반환하는 것이 맞다. - Flow는 비동기 프로그래밍 / 반응형 프로그래밍을 처리하기 위해 나온 컴포넌트이다.
LiveData는 반응형 프로그래밍을 처리하기 위해 나온 컴포넌트이다.
따라서 네트워크 같은 비동기 작업에서는 Flow로 반환하는 것이 맞다.
이외에도 Flow는 RxJava처럼 여러가지 기능을 제공한다고 한다. (RxJava와 유사하다.)
Pager형태의 인스턴스를 .flow를 통해 Flow의 형태로 반환하도록 한다.
Pager는 liveData로도, flow로도 반환될 수 있는 API를 제공한다.
PagingData는 불러온 데이터를 담아두는 컨테이너이다.
ViewModel & ViewModelFactory 클래스
package com.example.paging3exampleproject.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.paging3exampleproject.repo.MyRepository
class MainViewModelFactory(private val myRepository: MyRepository): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor(MyRepository::class.java).newInstance(myRepository)
}
}
package com.example.paging3exampleproject.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asLiveData
import com.example.paging3exampleproject.repo.MyRepository
class MainViewModel(private val myRepository: MyRepository): ViewModel() {
val myModelPagingLiveData = myRepository.getMyModelList().asLiveData()
}
레포지토리를 통해서 Flow 형태의 리스트를 받아온 후 asLiveData() 메서드를 통해서 LiveData로 변환한다.
Flow로 그대로 사용할 수도 있지만 View에서 Observe해서 사용하게 될 경우, 수명주기를 인식하는 것이
좋으므로 LiveData로 사용했다.
페이징 리스트 어댑터
package com.example.paging3exampleproject.view.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.example.paging3exampleproject.data.MyModel
import com.example.paging3exampleproject.databinding.ViewMyModelHolderBinding
class MyModelListPagingAdapter: PagingDataAdapter<MyModel, MyModelListPagingAdapter.MyModelViewHolder>(
object: DiffUtil.ItemCallback<MyModel>() {
override fun areItemsTheSame(oldItem: MyModel, newItem: MyModel): Boolean {
// 같은 객체인지 check
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: MyModel, newItem: MyModel): Boolean {
// 같은 내용물인지 check
return oldItem.idx == newItem.idx
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyModelViewHolder {
val viewBinding = ViewMyModelHolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyModelViewHolder(viewBinding)
}
override fun onBindViewHolder(holder: MyModelViewHolder, position: Int) {
getItem(position)?.let { item ->
holder.bind(item)
}
}
inner class MyModelViewHolder(private val viewMyModelHolderBinding: ViewMyModelHolderBinding): RecyclerView.ViewHolder(viewMyModelHolderBinding.root) {
fun bind(item: MyModel) {
viewMyModelHolderBinding.myModel = item
}
}
}
리사이클러뷰의 어댑터라고 생각하면 된다.
기본 형태는 모두 같지만, List가 필요가 없고, DiffUtil.ItemCallback을 매개변수로 필요로 한다.
왜 List가 필요없지? 라고 생각할 수 있는데, 잠시 후에 액티비티 코드를 보면 알 수 있겠지만
데이터를 불러올 때마다 어댑터에 데이터를 전송하면 어댑터가 데이터를 받아온 후 UI에 반영하는 방식이다.
메인 액티비티 레이아웃 xml & 소스 코드
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
<TextView
android:id="@+id/title_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/paging_example"
android:textSize="21sp"
android:textColor="@color/black"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
package com.example.paging3exampleproject.view
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.example.paging3exampleproject.databinding.ActivityMainBinding
import com.example.paging3exampleproject.repo.MyRepository
import com.example.paging3exampleproject.view.adapter.MyModelListPagingAdapter
import com.example.paging3exampleproject.viewmodel.MainViewModel
import com.example.paging3exampleproject.viewmodel.MainViewModelFactory
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
private val mainViewModel: MainViewModel by lazy {
ViewModelProvider(this, MainViewModelFactory(MyRepository())).get(MainViewModel::class.java)
}
private val myModelListPagingAdapter: MyModelListPagingAdapter
by lazy { MyModelListPagingAdapter() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpRecyclerView()
setUpObserver()
}
private fun setUpRecyclerView() {
binding.recyclerView.apply {
layoutManager = GridLayoutManager(baseContext, 3)
adapter = myModelListPagingAdapter
}
}
private fun setUpObserver() {
mainViewModel.myModelPagingLiveData.observe(this) { pagingData ->
lifecycleScope.launch {
myModelListPagingAdapter.submitData(pagingData)
}
}
}
}
setUpRecyclerView()를 통해 리사이클러뷰의 레이아웃 매니저, 어댑터를 지정해준다.
그리고 뷰모델의 Paging List LiveData를 Observe하게 되는데, PagingData가 들어오면
리사이클러뷰 어댑터에 submitData를 호출해서 데이터를 넘겨주면 된다.
이 때, submitData 메서드는 suspend 메서드이므로 비동기로 처리될 수 있도록 한다.
위와 같은 형태로 만들면, 미리보기에 나왔던 형태로 Paging 3.0을 적용시킬 수 있다.
그런데 만일 다른 앱들처럼 아래로 스크롤했을 때 로딩하는 것도 보여주고 싶으면 어떻게 해야할까?
이는 LoadStateAdapter를 구현해주면 된다.
LoadStateAdapter는 이름처럼 Load State를 나타내는 역할을 해주며 PagingDataAdapter에서 사용 가능하다.
LoadStateAdapter도 에러 처리를 기본적으로 지원해준다.
리스트의 레이아웃 매니저가 LinearLayoutManager의 경우에는 그냥 LoadStateAdapter를 구현하면 되지만,
GridLayoutManager라면, 아래 사진과 같은 문제가 발생하게 된다.
이는 LoadState 역시 하나의 Item으로 보고 PagingData에서 위치에 맞게 생성해주기 때문이다.
어떻게 하면 가운데에 표시할 수 있을까?
- RecyclerView.Adapter에는 getItemViewType()이라는 메서드가 있다.
이를 Override해서 로딩 상태인지, 아닌지를 구분해서 viewType을 넘겨주면 된다. - 액티비티 / 프래그먼트에서 리사이클러뷰의 LayoutManager를 설정할 때
spanSizeLookup 객체의 getSpanSize() 메서드를 Override해서
1에서 정의한 어댑터의 ViewType이 로딩 상태라면 spanSize를 1로,
아니라면 n(본 예시에서는 3)으로 설정해준다.
수정된 코드는 PagingDataAdapter 부분과 MainActivity 부분이고,
추가된 코드는 LoadStateAdapter이다.
LoadState View layout.xml & LoadStateAdapter 소스코드
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
style="?android:attr/progressBarStyle"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/error_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/error_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/error_occur"
android:textSize="12sp"
android:textColor="@color/black"
android:textAlignment="center"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="@+id/retry_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/retry"
android:textSize="11sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/error_text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
프로그래스 바를 통해 로딩 중이라는 상태를 알려주고,
error layout을 통해 에러가 발생(네트워크 통신 에러 등)하면 해당 레이아웃을 통해 알려준다.
package com.example.paging3exampleproject.view.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.paging3exampleproject.databinding.ViewMyModelLoadStateBinding
class MyModelListPagingLoadStateAdapter(private val retryCallback: () -> Unit):
LoadStateAdapter<MyModelListPagingLoadStateAdapter.MyModelLoadStateViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
loadState: LoadState
): MyModelLoadStateViewHolder {
val binding = ViewMyModelLoadStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyModelLoadStateViewHolder(binding)
}
override fun onBindViewHolder(holder: MyModelLoadStateViewHolder, loadState: LoadState) {
holder.bind(loadState)
}
inner class MyModelLoadStateViewHolder(private val viewMyModelLoadStateBinding: ViewMyModelLoadStateBinding):
RecyclerView.ViewHolder(viewMyModelLoadStateBinding.root) {
fun bind(loadState: LoadState) {
viewMyModelLoadStateBinding.retryBtn.setOnClickListener { retryCallback() }
viewMyModelLoadStateBinding.apply {
progressBar.isVisible = loadState is LoadState.Loading
errorLayout.isVisible = loadState is LoadState.Error
}
}
}
}
크게 어려운 건 없다. 매개변수로 retryCallback 메서드를 받게되는데,
에러가 발생하면 View에서 버튼을 누르면 어댑터의 retry() 메서드를 호출하도록 만들었다.
loadState에 따라 View에서 무엇이 보여질 지를 정해주면 된다.
수정된 PagingDataAdapter
package com.example.paging3exampleproject.view.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.example.paging3exampleproject.data.MyModel
import com.example.paging3exampleproject.databinding.ViewMyModelHolderBinding
class MyModelListPagingAdapter: PagingDataAdapter<MyModel, MyModelListPagingAdapter.MyModelViewHolder>(
object: DiffUtil.ItemCallback<MyModel>() {
override fun areItemsTheSame(oldItem: MyModel, newItem: MyModel): Boolean {
// 같은 객체인지 check
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: MyModel, newItem: MyModel): Boolean {
// 같은 내용물인지 check
return oldItem.idx == newItem.idx
}
}
) {
val contentsType = 1
val loadStateType = 2
override fun getItemViewType(position: Int): Int {
return if (position == itemCount) {
contentsType
} else {
loadStateType
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyModelViewHolder {
val viewBinding = ViewMyModelHolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyModelViewHolder(viewBinding)
}
override fun onBindViewHolder(holder: MyModelViewHolder, position: Int) {
getItem(position)?.let { item ->
holder.bind(item)
}
}
inner class MyModelViewHolder(private val viewMyModelHolderBinding: ViewMyModelHolderBinding): RecyclerView.ViewHolder(viewMyModelHolderBinding.root) {
fun bind(item: MyModel) {
viewMyModelHolderBinding.myModel = item
}
}
}
수정된 부분은 많지않다. getItemViewType() 메서드를 Override했다.
전역변수로 contentsType / loadStateType을 생성해서,
만일 현재 item의 position이 PagingData의 크기와 같다면 ContentsType을,
그게 아니라면(사이즈를 넘었다면) loadStateType을 넘겨준다.
MainActivity 소스코드 수정된 부분
private fun setUpRecyclerView() {
binding.recyclerView.apply {
layoutManager = GridLayoutManager(baseContext, 3).apply {
spanSizeLookup = object: GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (myModelListPagingAdapter.getItemViewType(position)) {
myModelListPagingAdapter.contentsType -> {
3
}
myModelListPagingAdapter.loadStateType -> {
1
}
else -> {
0
}
}
}
}
}
adapter = myModelListPagingAdapter.withLoadStateFooter(
MyModelListPagingLoadStateAdapter { myModelListPagingAdapter.retry() }
)
}
}
위에서 말했듯, spanSizeLookup 객체를 새로 정의한 후에 getSpanSize() 메서드를 오버라이딩 한다.
그리고 LoadStateAdapter를 적용하기 위해 adapter를 적용하는 부분에 withLoadStateFooter 메서드로
LoadStateAdapter를 적용해준다. 콜백으로는 PagingDataAdapter의 retry()를 호출하도록 한다.
로딩이 적용된 페이징 3.0
위처럼 그리드 레이아웃에도 로딩 상태가 가운데에 잘 나타나는 것을 확인할 수 있다.
이전에는 무한 스크롤을 구현할 때 화면의 최하단에 왔는지를 확인한 후에 데이터를 요청하는 방식으로
구현했다.
페이징 3.0은 이러한 방식이 아닐 뿐더러 추가적인 기능들과 에러 처리들도 많이 제공해준다.
프로젝트의 전체 코드는 위 링크에서 확인이 가능하다.