레이블이 리사이클러뷰인 게시물을 표시합니다. 모든 게시물 표시
레이블이 리사이클러뷰인 게시물을 표시합니다. 모든 게시물 표시

2019년 2월 9일 토요일

[안드로이드] RecyclerView Selection 가이드(번역)

RecyclerView Selection 가이드

며칠전에 저는 클린아키텍쳐에 대해 탐구해보고자 간단한 앱을 만들기 시작했습니다.
앱의 기능중에 유저의 다중선택을 받아서, 다른화면에 뿌려주어야 하는데, 이러한 다중선택 기능에 많은 시간을 쏟고싶지 않아서, recyclerview-selection 라이브러리를 쓰기로 결정했습니다. 이 포스트는 이 라이브러리를 구현하는 과정과, 겪었던 문제들에 대하여 살펴볼 예정입니다.

Step 0 - 앱 구축

가장 먼저 10개의 랜덤한 숫자의 목록을 보여주는 간단한 앱을 만들었습니다. 최대한 간단하도록 구성하여, Activity와 Adapter만 있도록 했습니다.
class MainActivity : AppCompatActivity() {

    private val adapter = MainAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter
        adapter.list = createRandomIntList()
        adapter.notifyDataSetChanged()
    }

    private fun createRandomIntList(): List<Int> {
        val random = Random()
        return (1..10).map { random.nextInt() }
    }
}
class MainAdapter : RecyclerView.Adapter<MainAdapter.ViewHolder>() {
    var list: List<Int> = arrayListOf()

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val number = list[position]
        holder.bind(number)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val itemView = LayoutInflater
            .from(parent.context)
            .inflate(R.layout.item_row, parent, false)
        return ViewHolder(itemView)
    }

    override fun getItemCount(): Int {
        return list.size
    }

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        private var text: TextView = view.findViewById(R.id.text)

        fun bind(value: Int) {
            text.text = value.toString()
        }
    }
}

Step 1 - RecyclerView - Selection 라이브러리 추가

build.gradle 파일에 다음과 같이 입력하여 라이브러리를 추가해 줍니다.
dependancies {
    implementation 'androidx.recyclerview:recyclerview-selection:1.0.0'
}

Step 2 - Key 타입 선택

공식문서에서는 KeyProvider를 구축하고 사용하기 위해 키로 사용할 타입을 정의하라고 되어 있습니다. 셀렉션 라이브러리는 Parcelable, Strnig, Long 세가지 타입을 키로 지원합니다. 다음은 몇몇 사용사례에 대해 어떤 타입을 키로 사용하면 좋을지에 대한 가이드라인입니다.
  • Parcelable : 어떤 Parcelable 타입도 키로 사용될 수 있습니다. 특히, 안드로이드의 Content Provider 프레임워크에서 사용되는 Uri를 사용할 때, Uri를 키로 사용한다면 좋은 사용사례가 될 것입니다.
  • String : 문자열 기반의 식별자를 가진 데이터라면, 키로 사용하기에 좋습니다.
  • Long : RecyclerView에서 이미 사용되고 있는 long 타입 기반의 식별자 체계를 사용핳고 있다면, Long 타입의 키는 좋은 선택이 될 것입니다. 그러나, 런타임에서 long타입은 안정적으로 접근하기에 제한적입니다. 기본적인 long 키에 대한 저장소를 사용한다면, 밴드 선택 기능은 지원되지 않습니다.
이 글에서는 Long 타입을 키로 사용하고, 리스트의 position 값을 키로써 사용하겠습니다. 우리의 아이디는 안정적이어야 하므로, 이를 위해 RecyclerView.Adapter의 setHasStableIds() 메서드를 이용하겠습니다. 저 메서드의 인자를 true로 주면, RecyclerView에서 long 타입의 유일한 id는 각각 하나의 아이템에만 매칭된다는 의미입니다.
init {
    setHasStableIds(true)
}
해당 설정 후에, RecyclerView.Adapter의 getItemId(position) 메서드를 상속받아서 적절한 id값으로 리턴할 수 있도록 해야 합니다.
override fun getItemId(position: Int): Long = position.toLong()

Step 3 - KeyProvider의 구현

Key로 사용할 타입을 정했다면, KeyProvider를 구현할 차례입니다. 이 글에서는, 라이브러리에서 미리 구현체로 제공하는 StableIdKeyProvider를 이용할 것입니다. 필요하다면 KeyProvider를 직접 구현하셔야 합니다.

Step 4 - ItemDetailsLookup

ItemDetailsLookup 클래스는 사용자가 선택한 항목들에 대한 정보를 라이브러리에 제공하는 역할을 합니다. ViewHolder가 현재 보여주고 있는 아이템 정보에 대해, 이 클래스 내의 추상클래스인 ItemDetails<Key>를 ViewHolder가 반환할 수 있도록 구현해야 합니다. 라이브러리에서는 선택동작을 수행할 때 발생하는 MotionEvent를 가지고 선택을 판별하며, MotionEvent의 좌표값을 이용해 RecyclerView에서 해당 View를 얻어옵니다. 얻어온 View를 이용하여 ViewHolder 객체를 얻을 수 있습니다. ItemDetails<Key> 인터페이스를 미리 구현한 ViewHolder 객체이므로 아이템의 정보를 얻을 수 있습니다.
// ItemDetailsLookup 클래스
class MyItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
    override fun getItemDetails(event: MotionEvent): ItemDetails<Long>? {
        val view = recyclerView.findChildViewUnder(event.x, event.y)
        if (view != null) {
            return (recyclerView.getChildViewHolder(view) as MainAdapter.ViewHolder)
                .getItemDetails()
        }
        return null
    }
}
// ViewHolder에서 
fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
    object : ItemDetailsLookup.ItemDetails<Long>() {
        override fun getPosition(): Int = adapterPosition
        override fun getSelectionKey(): Long? = itemId
    }

Step 5 - 선택된 항목 하이라이트 하기

유저들은 본인이 선택한 항목의 UI가 하이라이트 되어 본인이 어느것을 선택했는지 확인할 수 있기를 원할 것입니다. 하이라이트를 여러가지 방법으로 표현할 수 있을 것입니다. Gmail 앱처럼 선택된 항목의 ViewHolder의 특정 부분에 애니메이션을 줄 수도 있겠지만, 이 포스트에서는 간단하게 항목의 배경색을 변경하는 것으로 구현할 것입니다.
먼저, 뷰의 상태에 따라 배경색이 변하는 새로운 Drawable을 구성합니다.
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/holo_blue_light" android:state_activated="true" />
    <item android:drawable="@android:color/white" />
</selector>
다음에는, 만든 Drawable을 뷰의 백그라운드로 지정합니다.
<View
   ...
   android:background="@drawable/item_drawable"
   ... />
다음으로, 어댑터를 변경하여 뷰홀더가 선택상태를 제어할 수 있도록 합니다. 이를 위해 어댑터 내부에서 Tracker 필드를 추가해 줍니다. Tracker는 특정 항목이 선택되었는지의 여부를 라이브러리가 계속 추적할 수 있도록 도와줍니다. 이 예제에서는 Tracker 객체를 Activity에서 생성하여 어댑터에 넣어줄 것입니다.(tracker 생성에 관하여는 Step 6에서 설명합니다.)
class MainActivity : Activity() {
    var tracker : SelectionTracker<Long>? = null
    var adapter : RecyclerView.Adapter = // Adapter 객체
    ...
    
    override fun onCreate(var savedInstanceState : Bundle?) {
        ...
        tracker = // SelectionTracker 생성 로직 (Step 6)
        // adapter 내부에 setTracker()를 구현했다고 가정
        adapter.tracker = tracker
        ...
    }
}
어댑터의 onBindViewHolder() 메서드에서 tracker의 isSelected(key)를 호출하여, 해당 항목이 선택되었는지를 판별하여 UI를 갱신해줍니다.
// Adapter.onBindVIewHolder
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val number = list[position]
    tracker?.let {
        holder.bind(number, it.isSelected(position.toLong()))
    }
}
// ViewHolder 내부
fun bind(value: Int, isActivated: Boolean = false) {
    text.text = value.toString()
    itemView.isActivated = isActivated
}

Step 6 - Tracker 생성

Step 5에서 잠시 언급했던 대로, MainActivity에서 Tracker를 생성할 것입니다. Tracker 생성을 위해 라이브러리에서 제공하는 SelectionTracker.Builder 클래스를 사용합니다. Builder 클래스를 이용하여 tracker를 간단하게 생성하고, 생성할 때 Tracker에 설정할 수 있는 옵션값에 대해서도 살펴보겠습니다.
먼저, Builder클래스를 사용하기 위해 필요한 인자들은 다음과 같습니다.
  • selectionId : Activity / fragment에서 선택동작을 식별하기 위한 String 타입의 id
  • recyclerView : tracker를 추가할 RecyclerView
  • keyProvider : 선택 Key의 Source
  • detailsLookup : RecyclerView의 항목 정보에 대한 Source
  • storage : 선택 상태의 저장에 대한 정책
class MainActivity : Activity() {
    var tracker : SelectionTracker<Long>? = null
    var adapter : RecyclerView.Adapter = // Adapter 객체
    ...
    
    override fun onCreate(var savedInstanceState : Bundle?) {
        ...
        tracker = SelectionTracker.Builder<Long>(
            "mySelection",
            recyclerView,
            StableIdKeyProvider(recyclerView),
            MyItemDetailsLookup(recyclerView),
            StorageStrategy.createLongStorage()
        ).withSelectionPredicate(
            SelectionPredicates.createSelectAnything()
        ).build()
        ...
    }
}
위의 예제에서, Step 4에서 구현했었던 MyItemDetailsLookup를 제외하고는, 모두 라이브러리에서 제공하는 기본 클래스입니다. 마지막으로 어떤 제약사항 없이 여러개의 아이템을 선택하도록 하는 SelectionPredicate를 지정해 주었습니다.(SelectionPredicates.createSelectAnything()) SelectionPredicate 를 직접 상속받아 구현할 수도 있습니다. 모든 메서드의 결과값은 boolean으로, true를 반환하면 해당 선택을 허용하는 것이고, false를 반환하면 선택을 서용하지 않는 것입니다. 이 메서드를 이용하여, 선택에 대하여 다양한 제약사항을 추가할 수 있습니다.
return object : SelectionTracker.SelectionPredicate<K>() {
    override fun canSetStateForKey(key: K, nextState: Boolean): 
    Boolean {
        return true
    }

    override fun canSetStateAtPosition(position: Int, nextState:  
    Boolean): Boolean {
        return true
    }

    override fun canSelectMultiple(): Boolean {
        return true
    }
}
RecyclerView에서 항목 선택을 시작하려면, 선택하려는 아이템을 롱클릭했을 때 선택모드로 진입합니다. 이에 대하여 이 글의 마지막에서 살펴보겠습니다.
지금까지 구현한 코드는 다음의 Github에서 확인가능합니다.

Selection Observer

라이브러리에서는 Observer를 등록하여 선택 동작에 대해 관찰할 수 있도록 지원합니다.
이번에는, 두개의 아이템을 선택하면 두 아이템의 합을 계산하여 다른 화면에서 보여주는 아주 간단한 예제를 만들어보겠습니다. 다음의 예제에서 Tracker 객체에 Observer를 등록하는 것을 볼 수 있습니다.
tracker?.addObserver(
    object : SelectionTracker.SelectionObserver<Long>() {
        override fun onSelectionChanged() {
            super.onSelectionChanged()
            val items = tracker?.selection!!.size()
            if (items == 2) {
                launchSum(tracker?.selection!!)
            }
        }
    })
launchSum() 메서드에서는 전달받은 selection을 map 연산을 이용하여 ArrayList로 변환하여 새로운 Activity에 전달합니다.
private fun launchSum(selection: Selection<Long>) {
    val list = selection.map {
        adapter.list[it.toInt()]
    }.toList()
    SumActivity.launch(this, list as ArrayList<Int>)
}
지금까지 구현한 코드는 다음의 Github에서 확인가능합니다.

선택상태를 Lifecycle 변화에도 유지하기

위의 구현들은 Android의 라이프사이클 변화를 고려하지 않은 구현이라 상태가 유지되지 않습니다. 단적인 예로, 스크린을 회전한다면 선택내용들은 전부 유실될 것입니다. 그러한 상황이라면 다음과 같은 크래시를 보게 될 것입니다.
java.lang.NullPointerException: Attempt to invoke virtual method ‘int androidx.recyclerview.widget.RecyclerView$ViewHolder.getAdapterPosition()’ on a null object reference
이 크래시는 Child 뷰가 분리되어 있는 동안 더이상 선택되지 않은 항목을 삭제하려고 할 때, StableIdKeyProvider 내부에서 값을 얻어오지 못하여 생기는 것입니다. StableIdKeyProvier를 유지하는 대신, ItemKeyProvider를 직접 구현하여 해결하는 방식으로 접근해보겠습니다.
class MyItemKeyProvider(private val recyclerView: RecyclerView) : ItemKeyProvider<Long>(ItemKeyProvider.SCOPE_MAPPED) {

    override fun getKey(position: Int): Long? {
        return recyclerView.adapter?.getItemId(position)
    }

    override fun getPosition(key: Long): Int {
        val viewHolder = recyclerView.findViewHolderForItemId(key)
        return 
            viewHolder?.layoutPosition ?: RecyclerView.NO_POSITION
    }
}

또다른 크래시 이슈와 해결책

크래시를 발생시킬 수 있는 또다른 상황이 있습니다. 유저가 동적으로 선택할 아이템의 개수를 변경하는 상황을 생각해봅시다. 유저가 동적으로 선택할 갯수를 입력할 수 있도록 EditText를 추가하여, 동적으로 입력된 숫자만큼만 아이템을 선택할 수 있도록 변경할 수 있을 것입니다.
StableIdKeyProvider를 그대로 사용했다면, EditText를 선택하는 순간 위에서 살펴본 바와 같은 크래시가 났을 것입니다. 이 해답도 이전에 보았던대로 ItemKeyProvider를 직접 구현하는 것입니다.
이 모든 과정에 대환 최종 코드는 Github에서 확인하세요.

주의사항 및 결론

저는 항목 선택기능에 대하여, 큰 공수를 들이지 않고 빨리 해결해보고자 이 라이브러리를 사용하려고 했었습니다. 불해하게도, 바로 위에 언급했던 크래시들과 같은 이슈들을 다루느라, 오히려 시간을 더 많이 소비했습니다.
또 다른 문제는 다중 선택모드로 진입하는 유일한 방법은 선택하고자 하는 아이템을 롱클릭하는 것 밖에 업다는 것이었습니다. 저는 버튼을 이용하여 선택모드로 변경하는 걸 원했지만, 부질없는 생각이었습니다. 부질없는 이유를 알고싶다면, 라이브러리가 어떻게 동작하는지 알아야 합니다.
싱글탭과 롱클릭 이벤트를 다루기 위해 TouchInputHandler 클래스가 존재합니다. 내부 코드에서, 싱글탭일 때 다음과 같은 코드를 발견할 수 있을 것입니다.
if (mSelectionTracker.hasSelection()) { ... }
위 코드에서 볼 수 있듯이 mSelectionTracker.hasSelection() 가 true값을 반환하여 if문이 참이 되어야만 선택에 대한 뭔가를 처리할 수 있습니다. 롱클릭은 저러한 체크를 하고있지 않고, 바로 롱클릭한 항목을 선택 리스트에 추가합니다. hasSelection()메서드는 DefaultSelectionTracker에서 구현된 것으로, SelectionTracker.Builder로 새로운 Tracker 인스턴스를 만들었을 때 기본적으로 사용되는 메서드입니다. 이 메서드의 구현체는 다음과 같습니다.
@Override
public boolean hasSelection() {
    return !mSelection.isEmpty();
}
TouchInputHandler를 상속받아서 싱글탭 메서드를 다시 구현하면 가장 좋겠지만, 이 클래스는 final 로 선언되어 있어서 상속이 불가능합니다. 그럼에도 굳이 어떻게든 해보고 싶어서 MotionInputHandler 클래스를 상속받아서 필요한 모든 메서드를 구현하면 될 것이라고 생각할 수도 있겠지만, 그건 공수가 많이 듭니다.
SelectionTracker.Builder 에서 Tracker를 생성하기 위해 build() 메서드를 호출하는 시점에 TouchInputHandler 가 생성되어 할당되기 때문에, 특정 InputHandler 인스턴스를 사용할 수 없도록 되어 있습니다. 즉, 커스텀 Tracker를 만들고, 라이브러리에서 제공하는 Builder 클래스를 사용하지 않은 채 생성하여, 커스텀 InputHandler 인스턴스를 지정해주는 과정이 필요합니다. 이렇게까지 노력을 하느니, 기존에 해오던 방식대로 직접 선택기능을 구현하는게 더 빠를 것입니다.
이 글을 다 읽고, 글에서 언급된 크래시들을 빨리 해결하여 선택기능에 대한 (제한적이지만) 빠른 해결책을 원한다면, 라이브러리를 쓰는 것이 나쁘지는 않습니다. 단, 커스터마이징이 어느정도 가능하지만, 커스터마이징을 하는 오버헤드가 너무나도 크다는 것을 염두해 두어야 합니다.

2018년 4월 17일 화요일

[안드로이드] Architecture Component 3 - Paging Library 공식문서 번역

Paging Library

페이징 라이브러리는 앱이 데이터 소스로부터 필요한 정보를 점진적으로 읽어오는 작업을 쉽게 만들어주는 라이브러리로, 디바이스에 과부하가 걸리거나, 큰 데이터베이스로부터의 쿼리를 기다리지 않도록 만들어준다.

개요

대부분의 앱들은 많은 양의 데이터를 이용하여 동작하지만, 사실은 화면에 보여야 할 필요가 있는 일부분의 데이터만 불러와서 보여지면 된다. 예를들어, 어떤 앱에서 잠재적으로 보여줄 수 있는 데이터를 몇천개 가지고 있더라도, 한번에 몇십개씩만 엑세스하면 된다. 이부분을 앱이 신경쓰고 있지 않다면, 앱에서 불필요한 데이터까지 모두 요청하게 되고, 이는 불필요한 네트워크 트래픽의 발생과, 디바이스의 성능저하를 초래한다. 데이터가 원격저장소에 저장되거나 동기화되면, 이 또한 앱이 느려지는 원인이 되고, 사용자의 데이터요금을 낭비하게 된다.
기존의 Android API는 콘텐츠의 페이징을 허용했지만 중요한 제약 조건과 단점이 있었다.
  • CursorAdapter는 데이터베이스의 쿼리결과를 ListView의 항목들에 쉽게 매핑시켜주었지만, 데이터베이스 쿼리를 UI 스레드에서 하는것과 Cursor를 이용하기 때문에 데이터의 페이징을 비효율적으로 하는 단점이었다. 더 많은 정보를 확인하려면 Large Database Queries on Android 블로그 글을 확인한다.
  • AsyncListUtil은 RecyclerView에서 포지션 기반의 페이징을 허용하지만, 포지션 기반이 아닌 페이징은 허용하지 않는다. 또한, 셀수 있는 데이터셋에서 null을 플레이스 홀더로 강제로 사용한다.
새로운 페이징 라이브러리는 이러한 문제들을 해결한다. 이 라이브러리는 필요한 데이터를 요청하는 프로세스를 간소화하는 몇몇 클래스들을 포함한다. 이 클래스들은 Room과 같은 기존의 아키텍쳐 컴포넌트 라이브러리들과도 원활하게 잘 동작하도록 구성되어 있다.

기능

페이징 라이브러리의 클래스들을 사용하면, 이 섹션에서 소개되는 작업들을 수행하도록 도와준다.

데이터를 어떻게 가져오는지 정의하기

DataSource클래스를 이용하여 페이징된 데이터를 얻어올 데이터 소스를 정의한다. 데이터 접근 방법에 따라, 이 클래스를 상속받아 서브클래스로 구성할 수도 있다.
  • 로드할 데이터가 다음 / 이전 키를 포함하고 있다면, PageKeyedDataSource 클래스를 사용한다. 예를들어, 네트워크로부터 소셜미디어의 게시물을 가져올 경우, 연속적인 다음페이지를 로드하기 위해서, 다음페이지의 토큰을 전달해야 할 수도 있다.
  • 항목 N의 데이터를 이용해 N+1의 데이터를 가져와야 한다면, ItemKeyedDataSource클래스를 사용한다. 예를들어, 스레드된 코멘트들을 가져와야 한다면, 다음 코멘트를 가져오기 위해, 하나의 코멘트의 아이디를 전달해야 할 수도 있다.
  • 데이터 저장소에서 선택한 위치로부터 데이터의 페이지를 가져와야한다면, PositionalDataSource클래스를 사용한다. 이 클래스는 "1200번 위치로부터 20개의 데이터 반환"과 같이, 선택된 위치로부터 시작하는 데이터의 집합을 요청할 수 있다.
만약 Room 라이브러리를 사용하여 데이터를 관리하고 있다면, 이 Room 라이브러리는 자동적으로 PositionalDataSource인스턴스를 생성하기 위해 DataSource.Factory를 발생시킬 수 있다.
@Query("select * from users WHERE age > :age order by name DESC, id ASC")
DataSource.Factory<Integer, User> usersOlderThan(int age);

메모리로부터 데이터 로드

PagedList클래스는 DataSource로부터 데이터를 불러옵니다. 개발자는 데이터가 한번에 얼마나 로드가 되어야 하고, 미리 패치되어야 하는지 설정하여, 사용자가 데이터로드가 완료될 때까지 기다려야 하는 시간을 최소화할 수 있다. 이 클래스는 RecyclerView.Adapter와 같은 다른 클래스에게 업데이트 신호를 제공하여, 페이지가 로드되었을 때 RecyclerView의 컨텐츠를 업데이트하도록 도와준다.
앱의 구조에 따라, PagedList 클래스 사용을 위한 몇몇 옵션들이 있다. 더 많은 내용을 보려면, 다음의 데이터 로딩 구조를 참고한다.

UI에 데이터 나타내기

PagedListAdapter 클래스는 PagedList로부터 데이터를 UI에 나타내기 위한 RecyclerView.Adapter의 구현체이다. 예를들어, 새로운 페이지가 로드되었을 때, PagedListAdapter는 데이터가 갱신되었음을 RecyclerView에게 알려준다. 이는 RecyclerView가 실제 항목으로 플레이스홀더를 대체하도록 하여 적절한 애니메이션을 수행할 수 있다.
또한 PagedListAdapter는 백그라운드 스레드를 사용하여 한 PagedList에서 다른 PagedList 로의 변경 사항을 계산하고 (예 : 데이터베이스 변경이 업데이트 된 데이터로 새 PagedList를 생성하는 경우) 필요에 따라 목록 내용을 업데이트하기 위해 notifyItem ... () 메소드를 호출한다. 그리고나서 RecyclerView는 필요한 변화를 수행한다. 예를들어, PagedList 버전사이에 항목들의 위치가 변경되면, RecyclerView는 해당 항목을 새로운 위치로 애니메이션으로 움직인다.

데이터 갱신 관찰하기

페이징 라이브러리는 실시간 업데이트가 가능한 PagedList 컨테이너를 생성하기 위한 클래스들을 제공하고 있다.
  • LivePageListBuilder : 이 클래스는 개발자가 제공한 DataSource.Factory를 통해 LiveData<PagedList>를 생성한다. 다음의 샘플코드에서 보는것처럼, 만약 데이터베이스를 관리하기 위해 Room 라이브러리를 사용한다면, DAO는 PositionalDataSource를 사용하여 DataSource.Factory를 생성할 수 있다.
LiveData<PagedList<Item>> pagedItems =
        LivePagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchExecutor(myNetworkExecutor)
                .build();
  • RxpagedListBuilder : 이 클래스는 LivepagedListBuilder와 유사하게, RxJava2 기반의 기능을 제공한다. 이 클래스는 아키텍쳐 라이브러리의 RxJava2(android.arch.paging:rxjava2:1.0.0-alpha1)기반으로 제공된다. 다음의 코드에서 볼 수 있듯이, 이 클래스를 사용하여, PagedList를 구현할 때 Flowable과 Observable을 생성할 수 있다.
Flowable<PagedList<Item>> pagedItems =
        RxPagedListBuilder(myDataSource, /* page size */ 50)
                .setFetchScheduler(myNetworkScheduler)
                .buildFlowable(BackpressureStrategy.LATEST);

데이터 흐름 생성하기

페이징 라이브러리의 컴포넌트는 백그라운드 스레드의 생산자로부터 UI스레드의 화면 출력까지의 데이터 흐름을 구성한다. 예를들어, 새로운 항목이 데이터베이스에 추가되면, DataSource는 갱신될 것이고, LiveData<PagedList> / Flowable<PagedList> 는 백그라운드 스레드에서 새로운 PagedList를 생산한다.
paging-threading.gif
Figure1 : 페이징 라이브러 컴포넌트들은 대부분의 작업을 백그라운드 스레드에서 수행한다. 그래서 UI스레드에 큰 부담이 되지 않는다.
새로 생성된 PagedList는 UI스레드에서 PagedListAdapter로 보내진다. PagedListAdapter는 백그라운드 스레드에서 DiffUtil을 이용하여, 기존의 리스트와 새로운 리스트 사이의 차이를 계산한다. 비교가 끝났으면, PagedListAdapter는 리스트의 차이점을 이용하여 RecyclerView.Adapter.notifyItemInserted() 를 호출하여 UI에 항목들을 삽입한다.
UI스레드의 RecyclerView는 삽입된 아이템을 확인하여 적절하게 애니메이셔닝 한다.

데이터베이스 예제

다음의 코드조각들은 모든 라이브러리들이 함께 동작하도록 하는 몇몇 방법을 보여준다.

LiveData를 이용하여 페이징된 데이터 관찰하기

다음의 코드는 모든 컴포넌트들이 함께 동작하는 것을 보여준다. 사용자가 데이터베이스에 추가, 삭제, 변경할 때, RecyclerView의 내용은 자동적으로, 효율적으로 갱신된다.
@Dao
interface UserDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM user ORDER BY lastName ASC")
    public abstract DataSource.Factory<Integer, User> usersByLastName();
}

class MyViewModel extends ViewModel {
    public final LiveData<PagedList<User>> usersList;
    public MyViewModel(UserDao userDao) {
        usersList = new LivePagedListBuilder<>(
                userDao.usersByLastName(), /* page size */ 20).build();
    }
}

class MyActivity extends AppCompatActivity {
    private UserAdapter<User> mAdapter;

    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        mAdapter = new UserAdapter();
        viewModel.usersList.observe(this, pagedList ->
                mAdapter.submitList(pagedList));
        recyclerView.setAdapter(mAdapter);
    }
}

class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
    public UserAdapter() {
        super(DIFF_CALLBACK);
    }
    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User user = getItem(position);
        if (user != null) {
            holder.bindTo(user);
        } else {
            // Null defines a placeholder item - PagedListAdapter will automatically invalidate
            // this row when the actual object is loaded from the database
            holder.clear();
        }
    }
    public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<User>() {
        @Override
        public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // User properties may have changed if reloaded from the DB, but ID is fixed
            return oldUser.getId() == newUser.getId();
        }
        @Override
        public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // NOTE: if you use equals, your object must properly override Object#equals()
            // Incorrectly returning false here will result in too many animations.
            return oldUser.equals(newUser);
        }
    }
}

RxJava2를 이용하여 페이징된 데이터 관찰하기

LiveData대신 RxJava2를 사용하려고 한다면, Observable이나 Flowable 객체를 생성한다.
class MyViewModel extends ViewModel {

    public final Flowable<PagedList<User>> usersList;

    public MyViewModel(UserDao userDao) {
        usersList = new RxPagedListBuilder<>(userDao.usersByLastName(),
                /* page size */ 50).buildFlowable(BackpressureStrategy.LATEST);
    }
}
그리고, 다음의 코드와 같이 해당 데이터의 관찰을 시작 / 중지할 수 있다.
class MyActivity extends AppCompatActivity {
    private UserAdapter<User> mAdapter;
    private final CompositeDisposable mDisposable = new CompositeDisposable();

    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        mAdapter = new UserAdapter();
        recyclerView.setAdapter(mAdapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        myDisposable.add(mViewModel.usersList.subscribe(flowableList ->
                mAdapter.submitList(flowableList)));
    }

    @Override
    protected void onStop() {
        super.onStop();
        mDisposable.clear();
    }
}
UserDao, UserAdapter 클래스의 코드는 LiveData의 예제와 같으므로 생략한다.

데이터 로딩 구조 선택하기

페이징 라이브러리를 이용해 데이터를 페이징하는 두가지의 기본적인 방법이 있다.

네트워크 or 데이터베이스

첫번째로, 하나의 소스(로컬저장소이든 네트워크이든)로부터 페이징을 할 수 있다. 이 경우 다음 샘플과 같이 LiveData를 이용하여 로드된 데이터를 UI에 전달한다.
데이터의 소스를 지정하려면 LivePagedListBuilder에 DataSource.Factory를 전달한다.
paging-network-or-database.png
Figure2 : 하나의 데이터소스는 DataSource.Factory를 제공하여 컨텐츠를 로드한다.
데이터베이스를 관찰할 때, 컨텐츠의 변화가 발생하면, 데이터베이스는 새로운 PagedList에 푸쉬한다. 네트워크 페이징인 경우에(백엔드가 업데이트를 보내지 않을 때), 스와이프 리프레시 같은 시그널은 현재 데이터소스를 무효화함으로써 새로운 PagedList를 받아올 수 있다. 이는 모든 데이터를 비동기적으로 새로고침한다.
PagingWithNetworkSample의 메모리 + 네트워크 저장소 구현은 스와이프 리프레시, 네트워크 에러, 재시도를 처리하는 동안, Retrofit을 이용하여 어떻게 네트워크 DataSource.Factory를 구현했는지를 보여준다.

네트워크 and 데이터베이스

두번째로, 로컬저장소로부터 데이터를 페이지할 수 있고, 로컬저장소는 네트워크로부터 추가 데이터를 페이징한다. 이는 종종 네트워크 연결을 최소화하고, 백엔드의 캐시로 데이터베이스를 사용하여 더 나은 연결지양 경험을 제공한다.
이 경우, LiveData를 이용하여 데이터베이스로부터 컨텐츠를 페이징하고, 최신 데이터의 시그널을 관찰하기 위해, LivePagedListBuilder에 BoundaryCallback을 전달한다.
paging-network-plus-database.png
Figure3 : 데이터베이스는 네트워크 데이터의 캐시로 이용된다. UI는 데이터베이스로부터 데이터를 로드하고, 데이터가 없을 때, 네트워크로 신호를 보내서 네트워크로부터 데이터베이스로 데이터를 로드한다.
이후, 이 콜백을 네트워크 요청에 연결하면 데이터가 데이터베이스에 바로 저장된다. UI는 데이터베이스의 갱신을 구독하고 있으므로, 새로운 데이터의 변경은 자동적으로 UI로 전달된다.
PagingWithNetworkSample의 데이터베이스 + 네트워크 저장소 구현은 스와이프 리프레시, 네트워크 에러, 재시도를 처리하는 동안, Retrofit을 이용하여 어떻게 네트워크의 BoundaryCallback을 구현하는지 보여준다.

원문 : https://developer.android.com/topic/libraries/architecture/paging.html