안드로이드 연구소

[MVVM만들기] Paging3 본문

안드로이드 연구소/MVVM+AAC

[MVVM만들기] Paging3

안드로이드 연구원 2023. 5. 15. 15:56

지난 강의에서 안드로이드 아키텍처 컴포넌트의 라이브러리 중 하나인 Lifecycle을 배워보았습니다.

엑티비티에서 비즈니스 로직을 분리시켜 테스트에 상당한 도움을 줄 수 있는 라이브러리였는데요.

오늘도 다른 안드로이드 아키텍처 컴포넌트를 배워볼까합니다.

 

오늘은 Paging3이라는 라이브러리입니다.

Paging은 어떨때 쓰는 기술이까요?

왜 Paging1도 아니고 2도아니고 3일까요?

어떻게 Paging3을 사용하는걸까요? 

이 모든 답이 궁금하시다면 시작해보겠습니다.

 

Q1. ChatGPT, Paging이 나오게된 배경에 대해서 설명해줘

Paging 기술이 도입되기 전에는
안드로이드 개발자들은 일반적으로 모든 데이터를 한 번에 메모리에 로드하므로
저사양 장치에서 성능 문제 및 충돌이 발생할 수 있습니다.

그래서 2017년에 안드로이드 아키텍처 컴포넌트에 포함되어 등장하여
Paging으로 개발자는 필요에 따라 더 작은 단위의 데이터를 점진적으로 불러올 수 있습니다.
이 접근 방식은 메모리 사용량을 줄이고 앱 성능을 향상시키는 데 도움이 됩니다.

Paging이전에는 1000개의 데이터 리스트가 있으면 1000개 모두 불러와서

필요한 데이터를 선별하여 출력해주고 있었기 때문에

불러오는 과정에서 시간도 많이 들고 성능상 많은 문제가 많이 발생했을것 같네요.

 

이러한 문제를 해결하기위해 Paging은 데이터를 전체로 불러오지 않고

작은 단위로 데이터를 불러올 수 있게 하였나보네요.

 

 

Q2. Paging 라이브러리을 사용하면 어떤 장점들이 있니?

API 또는 데이터베이스로부터 데이터를 로드하는 데이터를
개발자는 더 작고 점진적인 크기 단위로 데이터를 로드할 수 있어서
메모리 사용을 최적화
하고 앱이 더 빠르게 응답할 수 있습니다.

삽입, 삭제 및 업데이트와 같은 데이터 변경 사항을 실시간으로 변화를 감지하여
다시 데이터를 로드할 필요없이 UI업데이트를 할 수 있어서
데이터 세트가 자주 변경되는 시나리오에서 유용할 수 있습니다.

또 Paging은 LiveData 및 ViewModel과 같은 다른 안드로이드 아키텍처 컴포넌트와 원활하게 통합되도록 설계되었습니다.

사용안할 이유가 없는 라이브러리네요.

 

 

Q3. 그렇다면 왜 paging3야? 1,2는?

Paging3은 Paging 라이브러리의 최신 버전입니다.
원래 Paging 라이브러리에서 업그레이드된 Paging2의 후속 제품입니다.

Paging1으로 2017년에 도입되었으며 지금처럼 Paged List를 통해 대규모 데이터 세트를 관리할 수 있도록 설계되었습니다.
하지만 boundary callbacks에 대한 기능이 제한적이였고
다른 안드로이드 아키텍처 컴포넌트들과의 통합이 어려움이 어려웠습니다.

이를 해결하기 위해 2018년에 Paging2가 출시되었습니다.
Paging2는 boundary callbacks에 대한 더 나은 지원,
향상된 오류 처리 및 단순화된 API와 같은 몇 가지 새로운 기능을 도입했습니다.
하지만 데이터 불러오기 및 출력하는 API가 복잡하였고
Data Source가 유연하지 못해 여러 소스에서 데이터를 불러오기가 어려웠습니다.
애니메이션과 스크롤 동작에서도 문제가 발생하였습니다.

마침내 2020년에 도입된 Paging3이 도입되어 이전 버전에 비해 상당한 개선을 나타냅니다.
- 데이터를 로드하는 데 사용할 수 있도록 유연하고 강력한 Data source와 함께 제공됩니다.
- 항목 애니메이션의 자동 처리, 향상된 성능 및 개선된 스크롤 동작을 포함하여 리싸이클러뷰에 최적화하였습니다.
- 머리글 및 바닥글 지원데이터 로드 에러시 리로드하였습니다.
- 새로고침 기능 향상시켰습니다.
- 메모리와 CPU부담 줄임줄였습니다.
처음에 도입 되었을때만 하여도 Paging1이었지만 업그레이드하여 현재 Paging3까지 도달할 수 있었네요.
그렇다면 Paging1의 핵심 요소인 Paged List,
Paging2의 boundary callbacks,
Paging3의 유연하고 강력한 Data source는 과연 무엇일까요?

 

 

Q4. Paged List와 boundary callbacks 그리고 Data source가 뭐야?

Paged List는 사용자가 목록을 스크롤할 때, 덩어리로 데이터를 로드하고 표시할 수 있는 특별한 종류의 목록입니다.
각 PagedList 인스턴스는 전체 데이터 세트의 한정된 하위 집합을 나타내며 필요에 따라 점진적으로 로드할 수 있습니다.
RecyclerView와 함께 사용하여 페이지를 매긴 방식으로 데이터를 표시할 수 있습니다.

Boundary callbacks은 PagedList가 현재 로드된 데이터의 끝에 도달할 때 트리거되는 콜백입니다.
콜백 함수가 도달할 때 다음 데이터 청크 로드를 트리거하거나 다른 작업을 수행할 수 있습니다.
이를 통하여 무한 스크롤 기능도 구현 가능합니다.

Data source는 데이터 소스는 데이터를 Paged List로 불러오는 역할을 하는 클래스입니다.
데이터베이스 또는 네트워크와 같은 데이터 저장소에서 데이터를 로드하고
앱의 나머지 부분에서 사용할 수 있는 형식으로 변환하는 역할을 합니다.

Data source로 데이터를 불러오고

불러온 데이터 묶음을 Paged List으로 불러오고

Paged List의 끝에 도달하면 Boundary callbacks 함수가 트리거되는 역할을 하네요.

코드를 통해서 보면 이해가 더 잘 될 것 같은데요?

 

 

Q5. 어떻게 Paging3를 사용할 수 있니?

dependencies {
    implementation "androidx.paging:paging-runtime:$paging_version"
}

최신 버전: https://developer.android.com/jetpack/androidx/releases/paging

 

첫번째로 DataSource 클래스를 정의합니다.

1. 생성한 클래스에 PagingSource를 확장 후 오버라이드 함수를 상속받습니다.

 - load 메서드: 데이터 불러오기

 - getRefreshKey: 데이터 초기화


2. load메서드 안에 response에 자신이 호출할 api나 database 함수를 호출합니다.(UserModel처럼 반환형도 변경하기)

 

3. page와 pageSize 그리고 LoadResult.Page 객체를 반환됩니다.

class MyPagingSource(private val api: MyApi) : PagingSource<Int, UserModel>() {
    
    // UserModel처럼 api나 database 반환형 변경하기
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UserModel> {
        val page = params.key ?: 1
        val pageSize = params.loadSize
        return try {
            val response = api.getItems() // 자신이 만들어둔 api나 database 호출
            val items = response.body() ?: emptyList()
            LoadResult.Page(
                data = items,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (items.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
    
    // UserModel처럼 api나 database 반환형 변경하기
    override fun getRefreshKey(state: PagingState<Int, UserModel>): Int = 0
}​

 

두번째로 PagedList를 관리할 Pager object를 만듭니다.

Pager object에는 불러올 paged list에 대한 정보가 담긴 PagerCofig와 위에서 생성한 MyPagingSoure 인스턴스를 연결합니다.

그 후 LiveData로 Paged List를 생성합니다.

class MyViewModel : ViewModel() {
    val myAdapter: MyAdapter = MyAdapter()
    private val api = MyApi() // API초기화
    val config = PagingConfig(pageSize = 10)
    val pagingSourceFactory = { MyPagingSource(api) }
   
    // Paged List 생성
    val myPagedListLiveData: LiveData<PagingData<MyItem>> = createPagedListLiveData()


    // PagedList를 관리할 Pager object를 생성 후 
    // 출력할 데이터로 방출하는 LiveData 객체를 반환합니다.
    private fun createPagedListLiveData(): LiveData<PagingData<MyItem>> {
        val pager = Pager(config, pagingSourceFactory)
        return pager.liveData.cachedIn(viewModelScope)
    }
}

 

그리하여 생성된 paged List 데이터를 엑티비티에서 viewModel의 데이터를 옵저빙하여 리싸이클러뷰로 연결합니다.

class MyActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMyBinding
    private val myViewModel: MyViewModel by viewModels()
    private lateinit var myAdapter: MyAdapter

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

        binding.recyclerView.adapter = myViewModel.myAdapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        
        myViewModel.myPagedListLiveData.observe(this) { pagingData ->
            myViewModel.myAdapter.submitData(lifecycle, pagingData)
        }
    }
}

 

어뎁터는 지난 챕터1에서 사용한 DiffUtil을 사용한 Adapter입니다.

class MyAdapter : ListAdapter<MyItem, MyAdapter.MyViewHolder>(MyItemDiffCallback()) {

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

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

    inner class MyViewHolder(private val binding: ItemMyBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: MyItem) {
            binding.item = item
            binding.executePendingBindings()
        }
    }

    class MyItemDiffCallback : DiffUtil.ItemCallback<MyItem>() {
        override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
            return oldItem.id == newItem.id
        }

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

 

Q6. 어떻게 Paging3에서 데이터초기화를 할 수 있니?(2023.09.04)

페이지네이션이 처리되어 무거워진 데이터를 초기화할 수 있는 기능도

필수 요구사항인 것 같습니다.

이럴 때는 MyPagingSource에서 만들어둔 getRefreshKey를 이용해야합니다.

 

기본적으로 PagingSource를 상속받았다면 자동으로 오버라이드되고 따로 건드릴 필요는 없습니다.

class MyPagingSource(private val api: MyApi) : PagingSource<Int, UserModel>() {
        
    override fun getRefreshKey(state: PagingState<Int, UserModel>): Int = 0
}​

 

그리고 api를 호출하는 곳에 PagingSource를 초기화해주는 함수를 만들어줍니다.

fun getRefresh() {
    MyPagingSource().invalidate()
}

 

ViewModel에도 만들어둬야겠죠?

class MyViewModel : ViewModel() {
    private val api = MyApi() // API초기화

    // page초기화
    fun getRefresh(){
	    viewModelScope.launch(Dispatchers.IO){
            	api.getRefresh()
        }
    }
}

 

그리고 xml에서 최상위 단에 스와이프 레이아웃을 만들어 준 다음

    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
      
        <TextView/>
      	<Recyclerview/>
      	...
    </>

 

스와이프 레이아웃 이벤트가 발생할 때(위에서 아래로 드래그 앤 드랍)

viewModel의 getRefresh()함수를 호출해주시면 됩니다.

 

이렇게하면 데이터가 비어지게 되는데

바로 새로 호출한 데이터를 넣고 싶으면 getRefresh()에서

api의 리스트를 호출하는 함수를 바로 호출하면 되겠죠? 

class MyActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMyBinding
    private val myViewModel: MyViewModel by viewModels()
    private lateinit var myAdapter: MyAdapter

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

        binding.recyclerView.adapter = myViewModel.myAdapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
        
        // myPagedListLiveData 관찰
        myViewModel.myPagedListLiveData.observe(this) { pagingData ->
            myViewModel.myAdapter.submitData(lifecycle, pagingData)
        }
        
        // 스와이프 리프레쉬 레이아웃 이벤트 처리
        binding.swipeRefreshLayout.setOnRefreshListener {
            binding.swipeRefreshLayout.isRefreshing = false
            viewModel.getRefresh() // 페이징 초기화
        }
    }

}

 

그렇다면 이제 여러분의 코드에서

paging처리가 되어 있지 않은 리스트 데이터 호출을 하여

리싸이클러뷰를 버겁게 하는 녀석들을 수정하러 가볼까요?

그럼 오늘은 여기까지 감사합니다.

 

'안드로이드 연구소 > MVVM+AAC' 카테고리의 다른 글

[MVVM만들기] Room  (0) 2023.05.16
[MVVM만들기] Lifecycle  (1) 2023.05.15
[MVVM만들기] http통신(Retrofit2)  (0) 2023.05.12
[MVVM만들기] DataBinding  (0) 2023.05.12
[MVVM만들기] ViewModel + LiveData  (0) 2023.05.12
Comments