경험의 기록

Paging이란

로컬 저장소에서나 네트워크를 통해 대규모 데이터 세트의 데이터 페이지를 로드할 때 일정한 덩어리로 쪼개서 로드하는 것

 

인터넷의 페이지를 생각하면 된다.

 

🔴 Paging3 아키텍처

위의 그림은 페이징 라이브러리가 어떻게 동작하는지 보여준다.

 

PagingSource

Repository 레이어의 기본 페이징 라이브러리 구성 요소.

데이터 소스와 이 소스에서 데이터를 검색하는 방법을 정의

네트워크 소스 및 로컬 데이터베이스를 포함한 단일 소스에서 데이터를 로드함.

 

RemoteMediator

Repository 레이어의 또 다른 페이징 라이브러리 구성 요소

로컬 데이터베이스 캐시가 있는 네트워크 데이터 소스와 같은 계층화된 데이터 소스의 페이징을 처리

즉, 네트워크에서 데이터를 불러올 때 캐싱하여 내부 데이터베이스에 저장하고 그 데이터베이스에서 데이터를 불러옴.

 

Pager

PagingSource 및 PagingData를 바탕으로 반응형 스트림에 노출되는 PagingData 인스턴스를 구성하기 위한 공개 API를 제공.

Pager로부터 Flow, Observable, LiveData 형태로 반환.

 

PagingData

ViewModel과 UI를 연결데이터의 스냅샷을 보유하는 컨테이너

 

PagingConfig

PagingSource를 구성하는 방법 정의

 

 

PagingDataAdapter

UI 레이어의 기본 페이징 라이브러리 구성 요소.

DiffUtil 를 사용함.

 

 

이론적인 내용으로 이해하기 어려우니

사용해보면서 더 설명하려고 한다.

 

 

 

 


사용해보기

2021.05.17 - [안드로이드/AAC, MVVM] - [Android] Retrofit 사용하여 서버와 http 통신하기 - (3) 통신결과 리싸이클러뷰에 연결하기

 

[Android] Retrofit 사용하여 서버와 http 통신하기 - (3) 통신결과 리싸이클러뷰에 연결하기

2021.05.17 - [안드로이드/AAC, MVVM] - [Android] Retrofit 사용하여 서버와 http 통신하기 - (2) 동적주소, 쿼리 사용하기 [Android] Retrofit 사용하여 서버와 http 통신하기 - (2) 동적주소, 쿼리 사용하기 20..

hanyeop.tistory.com

여기서 사용한 코드를 사용하였습니다.

 

 

종속성 추가

// Paging 3
    def paging_version = "3.0.0"
    implementation "androidx.paging:paging-runtime-ktx:$paging_version"

페이징 라이브러리를 추가해준다.

 

 

PagingSource 정의하기

private const val STARTING_PAGE_INDEX = 1

/*
simpleApi : 데이터를 제공하는 인스턴스
userId : 쿼리를 위한 값
 */
class MyPagingSource(
    private val simpleApi : SimpleApi,
    private val userId : Int
) : PagingSource<Int,Post>(){

    // 데이터 로드
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Post> {
        // LoadParams : 로드할 키와 항목 수 , LoadResult : 로드 작업의 결과
        return try {
            // 키 값이 없을 경우 기본값을 사용함
            val position = params.key ?: STARTING_PAGE_INDEX

            // 데이터를 제공하는 인스턴스의 메소드 사용
            val response = simpleApi.getCustomPost2(
                userId = userId,
                sort = "id",
                order = "asc"
            )
            val post = response?.body()

            /* 로드에 성공 시 LoadResult.Page 반환
            data : 전송되는 데이터
            prevKey : 이전 값 (위 스크롤 방향)
            nextKey : 다음 값 (아래 스크롤 방향)
             */
            LoadResult.Page(
                data = post!!,
                prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
                nextKey = null
            )

            // 로드에 실패 시 LoadResult.Error 반환
        } catch (exception: IOException) {
            LoadResult.Error(exception)
        } catch (exception: HttpException) {
            LoadResult.Error(exception)
        }
    }

    // 데이터가 새로고침되거나 첫 로드 후 무효화되었을 때 키를 반환하여 load()로 전달
    override fun getRefreshKey(state: PagingState<Int, Post>): Int? {
        TODO("Not yet implemented")
    }
}

코드를 하나씩 살펴보면

 

PagingSource<Key, Value> 를 상속받는 클래스를 선언해준다.

 

Key는 데이터를 로드하는 데 사용되는 식별자(페이지번호) 이며, Value는 데이터 클래스이다.

 

기본값으로 사용할 상수를 선언해주고,

데이터 통신을 하는 API와 쿼리를 위한 값을 파라미터로 설정해준다.

 

 

데이터를 로드하는 메소드인 load를 오버라이드 해준다.

 

여기서 

prevKey nextKey는 각각 위, 아래 방향으로 더 많은 데이터를 로드할 수 있을때 정의해주고,

아닌 경우 null을 사용한다.

 

이 그림은 load() 메소드가 어떻게 그 이후의 키를 로드하는지의 흐름을 보여준다.

 

데이터가 새로고침되거나 첫 로드 후 무효화되었을 때 키를 반환하여 load()로 전달해주는 메소드로 여기선 사용하지 않았다.

 

PagingData 스트림 설정

이제 받아온 데이터를 PagingData로 변환해야 한다.

뷰 모델에서 한번에 처리해도 되지만 별도의 repository를 만드는 것이 좋으므로

따로 만들었다.

 

Repository

class MyPagingRepository {
    
    fun getPost(userId : Int) =
        Pager(
           config = PagingConfig(
               pageSize = 5,
               maxSize = 20,
               enablePlaceholders = false
           ),
            // 사용할 메소드 선언
           pagingSourceFactory = { MyPagingSource(RetrofitInstance.api,userId)}
        ).liveData
}

Pager를 사용하여 데이터를 변환해준다.

PagingConfig 로 PagingSource 구성 방법을 정의해주어야 하는데

 

pageSize 는 미리 로드할 데이터 개수 값으로, 

만약 5로 설정했다면 PagingData는 미리 5개의 항목을 로드하려고 할 것이다.

일반적으로 보이는 항목의 여러배로 설정해야 한다.

 

maxSize 는 페이지를 삭제하기 전에 PagingData 에 로드 할 수있는 최대 항목 수를 정의한다.

페이지를 삭제하여 메모리에 보관되는 항목 수를 제한하는 데 사용할 수 있다.

최소 pageSize + (2 * prefetchDistance) 보다 높게 설정해야 한다.

 

ViewModel

// 데이터를 처리함
class MainViewModel(private val repository : MyPagingRepository) : ViewModel() {

    private val myCustomPosts2 : MutableLiveData<Int> = MutableLiveData()

    // 라이브 데이터 변경 시 다른 라이브 데이터 발행
    val result = myCustomPosts2.switchMap { queryString ->
        repository.getPost(queryString).cachedIn(viewModelScope)
    }

    // 라이브 데이터 변경
    fun searchPost(userId: Int) {
        myCustomPosts2.value = userId
    }
}

쿼리값을 변경하는 메소드를 만들어주고,

switchMap으로 새로운 라이브데이터를 cachedIn 을 사용하여 호출한다.

 

ViewModelFactory

// 뷰모델에 인자를 넘겨주기 위한 팩토리 메서드
class MainViewModelFactory(
    private val repository : MyPagingRepository
    ) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return MainViewModel(repository) as T
    }
}

팩토리메서드로

activity ktx나 DI를 사용하면 만들지 않아도 된다.

 

PagingDataAdapter 정의

class MyAdapter
    : PagingDataAdapter<Post, MyAdapter.MyViewHolder>(IMAGE_COMPARATOR) {

    class MyViewHolder(private val binding : ItemLayoutBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(post : Post){
            Log.d("tst5", "bind: ${post.id} 바인드됨")
            binding.userIdText.text = post.myUserId.toString()
            binding.idText.text = post.id.toString()
            binding.titleText.text = post.title
            binding.bodyText.text = post.body
        }
    }

    // 어떤 xml 으로 뷰 홀더를 생성할지 지정
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val binding = ItemLayoutBinding.inflate(LayoutInflater.from(parent.context),parent,false)
        return MyViewHolder(binding)
    }

    // 뷰 홀더에 데이터 바인딩
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val currentItem = getItem(position)

        if (currentItem != null) {
            holder.bind(currentItem)
        }
    }

    companion object {
        private val IMAGE_COMPARATOR = object : DiffUtil.ItemCallback<Post>() {
            override fun areItemsTheSame(oldItem: Post, newItem: Post) =
                oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: Post, newItem: Post) =
                oldItem == newItem
        }
    }
}

리사이클러뷰와 동일하게 사용하나

PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder> 를 상속받고, 

DiffUtil.ItemCallback 을 정의해주어야만 한다.

 

areItemsTheSame 는 두 개체가 동일한 항목(ID)을 나타내는 지 확인하기 위해 호출 되고,

areContentsTheSame 는 두 항목에 동일한 데이터가 있는지 확인하기 위해 호출 된다.

 

areItemsTheSame 에서 동일한 ID 였다면, areContentsTheSame 에서도 비교하게 되어 컨텐츠가 다를 경우만 리사이클러뷰가 갱신되어 다시 보여지기 때문에 효율적으로 뷰가 갱신된다.

 

메인액티비티

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel : MainViewModel
    private lateinit var binding : ActivityMainBinding
    private val myAdapter by lazy { MyAdapter() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 뷰바인딩
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 어댑터 연결
        binding.recyclerView.adapter = myAdapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL,false)

        val repository = MyPagingRepository()
        val viewModelFactory = MainViewModelFactory(repository)
        viewModel = ViewModelProvider(this,viewModelFactory).get(MainViewModel::class.java)

        // 받아온 값을 리싸이클러뷰에 보여줌
        binding.button.setOnClickListener {
            viewModel.searchPost(Integer.parseInt(binding.editTextView.text.toString()))
            Log.d("tst5", "클릭됐음.")

            // 포커스 없애기
            val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            inputMethodManager.hideSoftInputFromWindow(binding.editTextView.windowToken, 0)
        }

        // 관찰하여 submitData 메소드로 넘겨줌
        viewModel.result.observe(this, Observer {
            myAdapter.submitData(this.lifecycle,it)
            Log.d("tst5", "호출됐음.")
        })
    }
}

이제 라이브데이터를 관찰하여

submitData 메소드를 이용하여 어댑터에 넘겨준다.

 

 

 

잘 실행되는 것을 확인할 수 있다.

 

 

https://github.com/HanYeop/AndroidStudio-Practice2/tree/master/PagingEx

 

HanYeop/AndroidStudio-Practice2

(2021.05.20~) 안드로이드 학습 내용 저장소. Contribute to HanYeop/AndroidStudio-Practice2 development by creating an account on GitHub.

github.com

 

 

 

 

참고

https://developer.android.com/codelabs/android-paging#0

https://codechacha.com/ko/android-jetpack-sample/

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

https://two22.tistory.com/5

 

 

 

 

 

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading