Search code examples
androidandroid-architecture-componentsandroid-jetpackandroid-paging

Debugging pagination with Paging Architecture Component


So I wrote a simple notes functionality with the paging library using LiveData and Room database but I don't see it actually paginating anything. Perhaps I am not debugging correctly to confirm that pagination is happening. Any advice is appreciated.

In my DAO:

@Query("SELECT * FROM notes ORDER BY id ASC")
fun allNotes(): DataSource.Factory<Int, NoteEntity>

Then in my NotesRepository, I convert from NoteEntity to Note to keep my data layer implementation detail and the rest of the app separate (I am a fan of Clean Architecture). So the result is DataSource.Factory<Int, Note>:

override fun allNotes(): DataSource.Factory<Int, Note> =
        notesDao.allNotes().map { mapper.fromDb(it) }

In my ViewModel where PAGE_SIZE is 20:

val noteList: LiveData<PagedList<Note>> =
        LivePagedListBuilder(
                getNotesUseCase.allNotes(), PAGE_SIZE)

My Note is simply:

data class Note(
    val id: Long = 0,
    val text: String
)

Then I am displaying the notes in RecyclerView.

My ViewHolder contains:

class NoteViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.note_item, parent, false))  {
   private val idView = itemView.findViewById<TextView>(R.id.noteId)
   private val nameView = itemView.findViewById<TextView>(R.id.noteText)
   private var note: Note? = null

   /**
    * Items might be null if they are not paged in yet. PagedListAdapter will re-bind the
    * ViewHolder when Item is loaded.
    */
    fun bindTo(note: Note?) {
        this.note = note
        idView.text = note?.let { it.id.toString() } ?: ""
        nameView.text = note?.text ?: ""
    }
}

My Adapter contains:

class NoteAdapter(
    private val clickListener: ClickListener) : PagedListAdapter<Note, NoteViewHolder>(diffCallback) {

override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
    Timber.d("Binding view holder at position $position")
    val note = getItem(position)

    with(holder) {
        bindTo(note)
        note?.let {
            itemView.setOnClickListener {
            clickListener(note)
        }
    }
}

}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder =
        NoteViewHolder(parent)

companion object {
    /**
     * This diff callback informs the PagedListAdapter how to compute list differences when new
     * PagedLists arrive.
     */
    private val diffCallback = object : DiffUtil.ItemCallback<Note>() {
        override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean =
                oldItem.id == newItem.id

        override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean =
                oldItem == newItem
        }
    }
}

And my Fragment contains:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    setupRecyclerView()

    viewModel = ViewModelProviders.of(this, noteListViewModelFactory).get(NoteListViewModel::class.java)
    viewModel.noteList.observe(this, Observer { pagedNoteList ->
        pagedNoteList?.let { render(pagedNoteList) }
    })
}

private fun render(pagedNoteList: PagedList<Note>) {
    recyclerViewAdapter.submitList(pagedNoteList)
}

The data in the RecyclerView is loaded successfully and I see the debug logs when each item is bound. My understanding is PagedListAdapter I use requests new pages as the user scrolls and handles the new PagedLists by computing differences on a background thread.

My data source contains 100 items so I would expect that with page size of 20, there are 5 calls made for data when a testing on an average size phone.

How do I debug this to ensure the correct number of calls? Also, in the implementation above, is one of these being used by default (PositionalDataSource, ItemKeyedDataSource or PageKeyedDataSource)?


Solution

  • Answering my own question:

    It appears that in Room PositionalDataSource is being used by default.

    I was able to figure out how my Notes are being loaded in chunks by adding the following log statement inside my NoteAdapter.onBindViewHolder():

    Log.d("NoteAdapter", currentList)
    

    where currentList is a property of the super class PagedListAdapter.

    /**
     * Returns the PagedList currently being displayed by the Adapter.
     * etc.
     */
    @Nullable
    public PagedList<T> getCurrentList() {
        return mDiffer.getCurrentList();
    }
    

    As user scrolls, new items are bound to view holder and the log shows the contents of the currentList that's being iterated over.

    Also note that when you set the page size to N, initial list size will contain a count of 3 * N. Then each time user scrolls thru them all, N more items will be put into currentList. This is because I used the default configuration when requesting page list:

    val noteList: LiveData<PagedList<Note>> =
            LivePagedListBuilder(
                    getNotesUseCase.allNotes(), PAGE_SIZE)
                    .build()
    

    Instead, I could have done something like this to control initialLoadSizeHint:

    private val pagedListConfig = PagedList.Config.Builder()
            .setEnablePlaceholders(true)
            .setInitialLoadSizeHint(INITIAL_LOAD_SIZE)
            .setPageSize(PAGE_SIZE)
            .build()
    
    val noteList = LivePagedListBuilder<Int,Note>(getNotesUseCase.allNotes(), pagedListConfig).build()
    

    By default, initialLoadSizeHint equals PAGE_SIZE * 3.