Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Droidcon NYC: Implementing the Paging Library

Droidcon NYC: Implementing the Paging Library

The Android Paging Library makes it easy to integrate complex paging behaviour, gradually loading small chunks of data at a time to help reduce usage of network bandwidth and system resources.

The library allows you to implement this behaviour using compositional components in a decoupled architecture making your code more reliable, scalable, and testable. Furthermore, you’ll be able to use familiar components such as LiveData or RxJava to interface with your existing architecture.

In this talk you’ll learn:
- how to integrate the PagedList component into your architecture
- how to implement a DataSource to load snapshots when necessary
- how to use BoundaryCallback to signal the end of available data
- how to integrate LiveData or RxJava to fit your project

Ash Davies

August 26, 2019
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

  1. Implementing the Paging Library
    Droidcon NYC
    !
    @askashdavies

    View Slide

  2. View Slide

  3. Challenges
    @askashdavies

    View Slide

  4. Up-To-Date
    @askashdavies

    View Slide

  5. Large Data-Sets
    @askashdavies

    View Slide

  6. Offline
    @askashdavies

    View Slide

  7. State
    @askashdavies

    View Slide

  8. Progress
    @askashdavies

    View Slide

  9. !
    @askashdavies

    View Slide

  10. ListView
    android:id="@+id/list_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
    @askashdavies

    View Slide

  11. ArrayAdapter
    // extends BaseAdapter>
    list.adapter = ArrayAdapter(
    context,
    R.layout.simple_list_item_1,
    arrayOf("Kotlin", "Java" /* ... */)
    )
    @askashdavies

    View Slide

  12. BaseAdapter
    class ListAdapter : BaseAdapter() {
    override fun getView(position: Int, convertView: View, container: ViewGroup) {
    val view = convertView ?: layoutInflater.inflate(
    R.layout.simple_list_item_1,
    container,
    false
    )
    convertView
    .findViewById(R.id.text1)
    .text = getItem(position)
    return convertView
    }
    }
    @askashdavies

    View Slide

  13. BaseAdapter
    class ListAdapter() : BaseAdapter() {
    var items: List = emptyList()
    set(value) {
    field = value
    notifyDataSetChanged()
    }
    override fun getCount(): Int = items.size
    override fun getItem(position: Int): String = items[position]
    override fun getView(position: Int, convertView: View, container: ViewGroup) {
    /* ... */
    }
    }
    @askashdavies

    View Slide

  14. Pagination
    @askashdavies

    View Slide

  15. Paging
    OnScrollListener
    @askashdavies

    View Slide

  16. BaseAdapter / ListView
    • Manages list of it's own data
    • Manages view inflation and configuration
    • Notify entire data set of change
    • Not capable of diffing items
    @askashdavies

    View Slide

  17. RecyclerView
    @askashdavies

    View Slide

  18. RecyclerView
    @askashdavies

    View Slide

  19. Paging
    AsyncListUtil
    @askashdavies

    View Slide

  20. Diffing
    DiffUtil / AsyncListDiffer
    @askashdavies

    View Slide

  21. DiffUtil
    Myers Diff Algorithm
    medium.com/skyrise/the-myers-diff-algorithm-and-kotlin-observable-properties-69dfb18541b

    View Slide

  22. ListAdapter
    @askashdavies

    View Slide

  23. ListAdapter
    Immutability
    @askashdavies

    View Slide

  24. ListAdapter
    submitList(...)
    @askashdavies

    View Slide

  25. Migration
    ListAdapter
    @askashdavies

    View Slide

  26. RecyclerView.Adapter
    class UserAdapter : RecyclerView.Adapter() {
    private var items: List = emptyList()
    override fun getItemCount() = items.size
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(items[position])
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    /* ... */
    }
    fun updateList(items: List) {
    val result: DiffResult = DiffUtil.calculate(DiffCallback(this.items, items))
    result.dispatchUpdatesTo(this)
    }
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(item: User) {
    /* ... */
    }
    }
    }
    @askashdavies

    View Slide

  27. RecyclerView.Adapter
    class UserAdapter : RecyclerView.Adapter() {
    private var items: List = emptyList()
    override fun getItemCount() = items.size
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(items[position])
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    /* ... */
    }
    fun updateList(items: List) {
    val result: DiffResult = DiffUtil.calculate(UserComparator(this.items, items))
    result.dispatchUpdatesTo(this)
    }
    class UserViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(item: User) {
    /* ... */
    }
    }
    }
    @askashdavies

    View Slide

  28. DiffUtil.Callback
    class UserComparator(
    private val oldItems: List,
    private val newItems: List
    ) : DiffUtil.Callback() {
    override fun getOldListSize(): Int = oldItems.size
    override fun getNewListSize(): Int = newItems.size
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    return oldItems[oldItemPosition].id == newItems[newItemPosition].id
    }
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    return oldItems[oldItemPosition] == newItems[newItemPosition]
    }
    }
    @askashdavies

    View Slide

  29. DiffUtil.Callback
    class UserComparator(
    private val oldItems: List,
    private val newItems: List
    ) : DiffUtil.Callback() {
    override fun getOldListSize(): Int = oldItems.size
    override fun getNewListSize(): Int = newItems.size
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    return oldItems[oldItemPosition].id == newItems[newItemPosition].id
    }
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    return oldItems[oldItemPosition] == newItems[newItemPosition]
    }
    }
    @askashdavies

    View Slide

  30. DiffUtil.ItemCallback
    object UserComparator : DiffUtil.ItemCallback() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
    return oldItem.id == newItem.id
    }
    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
    return oldItem == newItem
    }
    }
    @askashdavies

    View Slide

  31. DiffUtil.ItemCallback
    object UserComparator : DiffUtil.ItemCallback() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
    return oldItem.id == newItem.id
    }
    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
    return oldItem == newItem
    }
    }
    @askashdavies

    View Slide

  32. RecyclerView.Adapter
    class UserAdapter : RecyclerView.Adapter() {
    private var items: List = emptyList()
    override fun getItemCount() = items.size
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(items[position])
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    /* ... */
    }
    fun updateList(items: List) {
    /* ... */
    }
    class UserViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(item: User) {
    /* ... */
    }
    }
    }
    @askashdavies

    View Slide

  33. ListAdapter
    class UserAdapter : ListAdapter(UserComparator) {
    private var items: List = emptyList()
    override fun getItemCount() = items.size
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(items[position])
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    /* ... */
    }
    fun updateList(items: List) {
    /* ... */
    }
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(item: User) {
    /* ... */
    }
    }
    }
    @askashdavies

    View Slide

  34. ListAdapter
    class UserAdapter : ListAdapter(UserComparator) {
    private var items: List = emptyList()
    override fun getItemCount() = items.size
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(items[position])
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    /* ... */
    }
    fun updateList(items: List) {
    /* ... */
    }
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(item: User) {
    /* ... */
    }
    }
    }
    @askashdavies

    View Slide

  35. ListAdapter
    class UserAdapter : ListAdapter(UserComparator) {
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(items[position])
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    /* ... */
    }
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(item: User) {
    /* ... */
    }
    }
    }
    @askashdavies

    View Slide

  36. ListAdapter
    @askashdavies

    View Slide

  37. @askashdavies

    View Slide

  38. Android JetPack
    Foundation Components
    @askashdavies

    View Slide

  39. Android JetPack
    Architecture Components
    @askashdavies

    View Slide

  40. Android JetPack
    Behaviour Components
    @askashdavies

    View Slide

  41. Android JetPack
    UI Components
    @askashdavies

    View Slide

  42. Android JetPack
    Paging Library
    @askashdavies

    View Slide

  43. Android JetPack
    Paging Library
    • PagedListAdapter

    • PagedList
    "
    • DataSource / DataSource.Factory
    • BoundaryCallback
    $
    @askashdavies

    View Slide

  44. @askashdavies

    View Slide

  45. @askashdavies

    View Slide

  46. @askashdavies

    View Slide

  47. @askashdavies

    View Slide

  48. @askashdavies

    View Slide

  49. @askashdavies

    View Slide

  50. @askashdavies

    View Slide

  51. @askashdavies

    View Slide

  52. @askashdavies

    View Slide

  53. PagedList

    @askashdavies

    View Slide

  54. PagedList
    PagedList : List
    @askashdavies

    View Slide

  55. PagedList
    PagedListBuilder
    • Data sources / cache management
    • Page size / prefetch distance
    • Offline characteristics
    • Loading behaviour
    @askashdavies

    View Slide

  56. PagedList
    PagedListBuilder
    • LiveDataPagedListBuilder
    • RxPagedListBuilder
    • FlowPagedListBuilder1
    1 github.com/chrisbanes/tivi/blob/master/data-android/src/main/java/app/tivi/data/FlowPagedListBuilder.kt
    @askashdavies

    View Slide

  57. PagedList
    LiveDataPagedListBuilder
    @askashdavies

    View Slide

  58. PagedList
    LiveDataPagedListBuilder
    class UserRepository(private val service: UserService) {
    fun users(): LiveData> {
    /* ... */
    }
    }
    @askashdavies

    View Slide

  59. @askashdavies

    View Slide

  60. PagedList
    LiveDataPagedListBuilder
    class UserRepository(private val service: UserService) {
    fun users(): LiveData> {
    val factory: DataSource.Factory = service.users()
    return LivePagedListBuilder(factory, PAGE_SIZE).build()
    }
    companion object {
    private const val PAGE_SIZE = 20
    }
    }
    @askashdavies

    View Slide

  61. PagedList.Config
    LiveDataPagedListBuilder
    class UserRepository(private val service: UserService) {
    fun users(): LiveData> {
    val factory: DataSource.Factory = service.users()
    val config: PagedList.Config = PagedList.Config.Builder()
    .setPageSize(PAGE_SIZE)
    .build()
    return LivePagedListBuilder(factory, config).build()
    }
    companion object {
    private const val PAGE_SIZE = 20
    }
    }
    @askashdavies

    View Slide

  62. PagedList.Config
    LiveDataPagedListBuilder
    class UserRepository(private val service: UserService) {
    fun users(): LiveData> {
    val factory: DataSource.Factory = service.users()
    val config: PagedList.Config = PagedList.Config.Builder()
    .setPageSize(PAGE_SIZE)
    .setInitialLoadSizeHint(50)
    .build()
    return LivePagedListBuilder(factory, config).build()
    }
    companion object {
    private const val PAGE_SIZE = 20
    }
    }
    @askashdavies

    View Slide

  63. PagedList.Config
    LiveDataPagedListBuilder
    class UserRepository(private val service: UserService) {
    fun users(): LiveData> {
    val factory: DataSource.Factory = service.users()
    val config: PagedList.Config = PagedList.Config.Builder()
    .setPageSize(PAGE_SIZE)
    .setInitialLoadSizeHint(50)
    .setPrefetchDistance(10)
    .build()
    return LivePagedListBuilder(factory, config).build()
    }
    companion object {
    private const val PAGE_SIZE = 20
    }
    }
    @askashdavies

    View Slide

  64. PagedList.Config
    LiveDataPagedListBuilder
    class UserRepository(private val service: UserService) {
    fun users(): LiveData> {
    val factory: DataSource.Factory = service.users()
    val config: PagedList.Config = PagedList.Config.Builder()
    .setPageSize(PAGE_SIZE)
    .setInitialLoadSizeHint(50)
    .setPrefetchDistance(10)
    .setEnablePlaceholders(false)
    .build()
    return LivePagedListBuilder(factory, config).build()
    }
    companion object {
    private const val PAGE_SIZE = 20
    }
    }
    @askashdavies

    View Slide

  65. Placeholders
    Advantages
    • Continuous scrolling
    • Less abrupt UI changes
    • Scrollbars maintain consistency
    • Accurately indicate loading state
    @askashdavies

    View Slide

  66. Placeholders
    Disadvantages
    • Irregular sized items cause UI jank
    • Prepare view holder without item
    • Data set must be quantifiable
    @askashdavies

    View Slide

  67. PagedList.Config
    RxPagedListBuilder
    class UserRepository(private val service: UserService) {
    fun users(): Observable> {
    val factory: DataSource.Factory = service.users()
    val config: PagedList.Config = PagedList.Config.Builder()
    .setPageSize(PAGE_SIZE)
    .build()
    return RxPagedListBuilder(factory, config)
    .buildObservable() // or buildFlowable()
    }
    companion object {
    private const val PAGE_SIZE = 20
    }
    }
    @askashdavies

    View Slide

  68. Coroutines
    FlowPagedListBuilder
    class UserRepository(private val service: UserService) {
    fun users(): Flow> {
    val factory: DataSource.Factory = service.users()
    val config: PagedList.Config = PagedList.Config.Builder()
    .setPageSize(PAGE_SIZE)
    .build()
    return FlowPagedListBuilder(factory, config).buildFlow()
    }
    companion object {
    private const val PAGE_SIZE = 20
    }
    }
    github.com/chrisbanes/tivi/blob/master/data-android/src/main/java/app/tivi/data/FlowPagedListBuilder.kt

    View Slide

  69. DataSource.Factory
    @askashdavies

    View Slide

  70. Paging ❤ Room
    @askashdavies

    View Slide

  71. Paging ❤ Room
    @Dao
    interface UserDao {
    @Query("SELECT * FROM user")
    fun users(): DataSource.Factory
    }
    @askashdavies

    View Slide

  72. Paging ❤ Room
    @Dao
    interface UserDao {
    @Query("SELECT * FROM user")
    fun users(): DataSource.Factory
    }
    @askashdavies

    View Slide

  73. @askashdavies

    View Slide

  74. Remote Data Source
    Backend
    @askashdavies

    View Slide

  75. Remote Data Source
    Index
    @askashdavies

    View Slide

  76. DataSource
    • PositionalDataSource
    !
    • ItemKeyedDataSource
    "
    • PageKeyedDataSource
    #
    @askashdavies

    View Slide

  77. PositionalDataSource
    PositionalDataSource
    • Able to scroll to different elements
    • Load pages of requested sizes
    • Load pages at arbitrary positions
    • Assumed ordering by integer index
    • Provide a fixed item count
    @askashdavies

    View Slide

  78. PositionalDataSource
    PositionalDataSource
    • loadInitial()
    • requestedStartPosition
    • requestedLoadSize
    • pageSize
    • placeholdersEnabled
    • loadRange()
    • startPosition
    • loadSize
    @askashdavies

    View Slide

  79. ItemKeyedDataSource
    ItemKeyedDataSource
    • Great for ordered data sets
    • Items can be uniquely identified
    • Item key indicates position
    • Detect items before or after
    @askashdavies

    View Slide

  80. ItemKeyedDataSource
    ItemKeyedDataSource
    • getKey()
    • loadInitial()
    • requestedInitialKey
    • requestedLoadSize
    • placeholdersEnabled
    • loadAfter()
    • key
    • requestedLoadSize
    • loadBefore()
    • key
    • requestedLoadSize
    @askashdavies

    View Slide

  81. PageKeyedDataSource
    PageKeyedDataSource
    • Common for API responses
    • GitHub
    • Twitter
    • Reddit
    @askashdavies

    View Slide

  82. PageKeyedDataSource
    PageKeyedDataSource
    • loadInitial()
    • requestedLoadSize
    • placeholdersEnabled
    • loadAfter()
    • key
    • requestedLoadSize
    • loadBefore()
    • key
    • requestedLoadSize
    @askashdavies

    View Slide

  83. @askashdavies

    View Slide

  84. Architecture
    @askashdavies

    View Slide

  85. @askashdavies

    View Slide

  86. Database
    @askashdavies

    View Slide

  87. Source of truth
    • Consistent data presentation
    • Simple process - need more, load more
    • Gracefully degrades on failure
    • Optionally refresh on observe
    @askashdavies

    View Slide

  88. BoundaryCallback
    • Signals end of data from database
    • Triggers network load to populate
    • Provided to PagedListBuilder
    @askashdavies

    View Slide

  89. BoundaryCallback
    PagedList.BoundaryCallback
    public abstract static class BoundaryCallback {
    public void onZeroItemsLoaded() { }
    public void onItemAtFrontLoaded(@NonNull T itemAtFront) { }
    public void onItemAtEndLoaded(@NonNull T itemAtEnd) { }
    }
    @askashdavies

    View Slide

  90. Further Reading
    • Florina Muntenescu: Migrating to Paging Library
    youtube.com/watch?v=8DPgwrV_9-g
    • Chris Craik & Yigit Boyar: Manage infinite lists with RecyclerView and Paging
    youtube.com/watch?v=BE5bsyGGLf4
    -- ADB: Prefetch and Paging
    androidbackstage.blogspot.com/2018/10/episode-101-prefetch-and-paging.html
    • Android Paging Codelab
    codelabs.developers.google.com/codelabs/android-paging/#0
    • Google Samples: Paging with Network Sample
    github.com/googlesamples/android-architecture-components/tree/master/PagingWithNetworkSample
    • Chris Banes: FlowPagedListBuilder
    github.com/chrisbanes/tivi/blob/master/data-android/src/main/java/app/tivi/data/FlowPagedListBuilder.kt
    @askashdavies

    View Slide

  91. Implementing the Paging Library
    bit.ly/github-repo-search
    bit.ly/paging-library
    @askashdavies

    View Slide

  92. Happy Paging!
    @askashdavies

    View Slide