Slide 1

Slide 1 text

Implementing the Paging Library Droidcon Lisbon ! @askashdavies

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Challenges @askashdavies

Slide 4

Slide 4 text

Up-To-Date @askashdavies

Slide 5

Slide 5 text

Large Data-Sets @askashdavies

Slide 6

Slide 6 text

Offline @askashdavies

Slide 7

Slide 7 text

State @askashdavies

Slide 8

Slide 8 text

Progress @askashdavies

Slide 9

Slide 9 text

! @askashdavies

Slide 10

Slide 10 text

ListView @askashdavies

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Pagination @askashdavies

Slide 15

Slide 15 text

Paging OnScrollListener @askashdavies

Slide 16

Slide 16 text

@askashdavies

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

RecyclerView @askashdavies

Slide 19

Slide 19 text

RecyclerView @askashdavies

Slide 20

Slide 20 text

Paging AsyncListUtil @askashdavies

Slide 21

Slide 21 text

Diffing DiffUtil / AsyncListDiffer @askashdavies

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

ListAdapter @askashdavies

Slide 24

Slide 24 text

ListAdapter Immutability @askashdavies

Slide 25

Slide 25 text

ListAdapter submitList(...) @askashdavies

Slide 26

Slide 26 text

Migration ListAdapter @askashdavies

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

ListAdapter @askashdavies

Slide 38

Slide 38 text

@askashdavies

Slide 39

Slide 39 text

Android JetPack Foundation Components @askashdavies

Slide 40

Slide 40 text

Android JetPack Architecture Components @askashdavies

Slide 41

Slide 41 text

Android JetPack Behaviour Components @askashdavies

Slide 42

Slide 42 text

Android JetPack UI Components @askashdavies

Slide 43

Slide 43 text

Android JetPack Paging Library @askashdavies

Slide 44

Slide 44 text

Android JetPack Paging Library • PagedListAdapter ⚙ • PagedList " • DataSource / DataSource.Factory • BoundaryCallback $ @askashdavies

Slide 45

Slide 45 text

@askashdavies

Slide 46

Slide 46 text

@askashdavies

Slide 47

Slide 47 text

@askashdavies

Slide 48

Slide 48 text

@askashdavies

Slide 49

Slide 49 text

@askashdavies

Slide 50

Slide 50 text

@askashdavies

Slide 51

Slide 51 text

@askashdavies

Slide 52

Slide 52 text

@askashdavies

Slide 53

Slide 53 text

@askashdavies

Slide 54

Slide 54 text

PagedList ❔ @askashdavies

Slide 55

Slide 55 text

PagedList PagedList : List @askashdavies

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Observability PagedList @askashdavies

Slide 59

Slide 59 text

PagedList LiveDataPagedListBuilder @askashdavies

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

@askashdavies

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

DataSource.Factory @askashdavies

Slide 72

Slide 72 text

Paging ❤ Room @askashdavies

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

@askashdavies

Slide 76

Slide 76 text

Remote Data Source Backend @askashdavies

Slide 77

Slide 77 text

Remote Data Source Index @askashdavies

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

@askashdavies

Slide 86

Slide 86 text

Architecture @askashdavies

Slide 87

Slide 87 text

@askashdavies

Slide 88

Slide 88 text

Database @askashdavies

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

BoundaryCallback class UserBoundaryCallback( private val service: UserService, private val dao: UserDao, private val query: String ) : PagedList.BoundaryCallback() { private var page: Int = 0 override fun onZeroItemsLoaded() { requestItems() } override fun onItemAtEndLoaded(itemAtEnd: User) { requestItems() } private fun requestItems() { GlobalScope.launch { // Ignore structured concurrency dao.insert(service.users(query, page, 50)) page++ } } } @askashdavies

Slide 93

Slide 93 text

BoundaryCallback class UserBoundaryCallback( private val service: UserService, private val dao: UserDao, private val query: String ) : PagedList.BoundaryCallback() { private var page: Int = 0 override fun onZeroItemsLoaded() { requestItems() } override fun onItemAtEndLoaded(itemAtEnd: User) { requestItems() } private fun requestItems() { GlobalScope.launch { // Ignore structured concurrency dao.insert(service.users(query, page, 50)) page++ } } } @askashdavies

Slide 94

Slide 94 text

BoundaryCallback class UserRepository( private val service: UserService ) { fun users(query: String): 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() } } @askashdavies

Slide 95

Slide 95 text

BoundaryCallback class UserRepository( private val service: UserService ) { fun users(query: String): 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() } } @askashdavies

Slide 96

Slide 96 text

BoundaryCallback class UserRepository( private val service: UserService, private val dao: UserDao ) { fun repos(query: String): LiveData> { val factory: DataSource.Factory = dao.repos(query) val config: PagedList.Config = PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(true) .setPrefetchDistance(50) .build() return LivePagedListBuilder(factory, config) .build() } } @askashdavies

Slide 97

Slide 97 text

BoundaryCallback class UserRepository( private val service: UserService, private val dao: UserDao ) { fun repos(query: String): LiveData> { val factory: DataSource.Factory = dao.repos(query) val callback = RepoBoundaryCallback(service, dao, query) val config: PagedList.Config = PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(true) .setPrefetchDistance(50) .build() return LivePagedListBuilder(factory, config) .setBoundaryCallback(callback) .build() } } @askashdavies

Slide 98

Slide 98 text

Error Handling @askashdavies

Slide 99

Slide 99 text

Error Handling ! @askashdavies

Slide 100

Slide 100 text

Error Handling class UserBoundaryCallback : PagedList.BoundaryCallback() { // LiveData of network errors. private val _errors = MutableLiveData() val errors: LiveData get() = _errors /* * ... * */ } @askashdavies

Slide 101

Slide 101 text

Error Handling class UserRepository( private val service: UserService, private val dao: UserDao ) { fun repos(query: String): LiveData> { val factory: DataSource.Factory = dao.repos(query) val callback = RepoBoundaryCallback(service, dao, query) val config: PagedList.Config = PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(true) .setPrefetchDistance(50) .build() return LivePagedListBuilder(factory, config) .setBoundaryCallback(callback) .build() } } @askashdavies

Slide 102

Slide 102 text

Error Handling class UserRepository( private val service: UserService, private val dao: UserDao ) { fun repos(query: String): Pair>, LiveData> { val factory: DataSource.Factory = dao.repos(query) val callback = UserBoundaryCallback(service, dao, query) val config: PagedList.Config = PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(true) .setPrefetchDistance(50) .build() val data: LiveData> = LivePagedListBuilder(factory, config) .setBoundaryCallback(callback) .build() return data to callback.errors } } @askashdavies

Slide 103

Slide 103 text

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/ • 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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

Happy Paging! @askashdavies