Skip to content
Nate Ebel edited this page Jul 20, 2022 · 9 revisions

πŸ–₯ Lab 11: Displaying List Data with RecyclerView

Let’s update our app to display a hardcoded list of data in MyNotesFragment

Β 

πŸ“ Objectives

  1. Remove textView and noteDetailButton from fragment_my_notes.xml

  2. Add a RecyclerView to fragment_my_note.xml and constrain it to fill the screen.

    1. The FloatingActionButton should still be visible on top of the RecyclerView
  3. Create item_note.xml to represent individual Note list items in the UI. The layout should include:

    1. An ImageView with id noteImage
    2. A TextView with id titleTextView
    3. A TextView with id categoryText
    4. A TextView with id contentText
  4. Create a file named MyNotesListAdapter.kt within the mynotes package

  5. Within MyNotesListAdapter.kt, create a NoteViewholder class that binds view references from item_note.xml and bind Notes into the view

    1. NoteViewHolder should extend RecyclerView.ViewHolder
    2. Pass an instance of ItemNoteBinding as a constructor property to NoteViewHolder and pass binding.root to the RecyclerView.ViewHolder constructor
    3. Create a bindNote(note: Note) method that takes a note
    4. Within bindNote(note: Note), update the fields from the binding class using the passed Note
  6. Create an object class NoteDiffUtil that extends DiffUtil.ItemCallback<Note>

    1. Override both areItemsTheSame() and areContentsTheSame() to perform equality comparison of two Notes (see hints)
    2. When we create an instance of our RecyclerView.Adapter class, we will pass this callback to it
    3. The DiffUtil.ItemCallback makes our RecyclerView update more efficient by calculating which Views need updated
  7. Create a MyNotesListAdapter class to convert a List<Note> into views for the RecyclerView

    1. MyNotesListAdapter should extend ListAdapter<Note, NoteViewHolder>
    2. Pass NoteDiffUtil to the ListAdapter constructor so we get the efficient diffing we are after
    3. Override onCreateViewHolder() which should inflate ItemNoteBinding and create an instance of NoteViewHolder
    4. Override onBindViewHolder() to bind the new note data to an instance of NoteViewHolder
      1. Get the Note for the current position by calling getItem(position)
      2. Bind the data by passing the Note to our holder.bindNote(note) method
  8. Within MyNotesFragment, set a LinearLayoutManager to your RecyclerView

    1. ex binding.notesList.layoutManager = LinearLayoutManager(requireContext())
    2. Without a LayoutManager being set, a RecyclerView will throw an exception when it tries to draw its content
    3. A LayoutManager is in charge of determining how child views should be laid out within the RecyclerView.
    4. LinearLayoutManager will draw elements in a vertically scrolling list
  9. Set an instance of MyNotesListAdapter to our RecyclerView

    1. Within MyNotesFragment, create an instance of MyNotesListAdapter and store it as a class property named notesAdapter: ex private val notesAdapter = MyNotesListAdapter()
    2. Set notesAdapter to the RecyclerView by adding binding.notesList.adapter = notesAdapter within onCreateView()
  10. Refactor MyNotesViewModel.UiState.notes to be a List<Note> rather than List<String>

  11. Call notesAdapter.submitList(uiState.notes) within the collection of MyNotesViewModel.state

    1. This will update our RecyclerView.Adapter, and by extension our RecyclerView, anytime our state changes
  12. Add an init{} block within MyNotesViewModel and update the UiState to include the data from SAMPLE_NOTES

    1. ex state.update { currentState -> currentState.copy(notes = SAMPLE_NOTES) }
  13. Re-deploy your app and observe the list of Notes

  14. Respond to list item selections by showing NoteDetailFragment

    1. Add a function parameter to your MyNotesListAdapter to respond to list item clicks. The function should take a Note as a parameter and return Unit
    2. In MyNotesListAdapter,onBindViewHolder() add a click listener to the item view by calling setOnClickListener{}
    3. Within the View's click listener, call back into the click listener passed to the adapter
    4. Update your adapter initialization within MyNotesFragment by passing {} - we will respond to clicks in the next steps
  15. Pass the selected Note to NoteDetailFragment when item is selected

    1. Add id 'kotlin-parcelize' to the plugins{} block of app/build.gradle
    2. Update Note to implement Parcelable and add @Parcelize annotation to the class
    3. Parcelable is an Android-specific version of Serializable designed to be more efficient. Making our Note Parcelable allows us to pass it in a Bundle and forward it as an Argument to a navigation destination.
    4. Open main_navigation.xml and add an argument to NoteDetailsFragment named selectedNote of type Note
    5. Rebuild the project to generate a navigation action class
    6. Within the click handler passed to MyNotesAdapter navigate to NoteDetailsFragment using findNavController().navigate(MyNotesFragmentDirections.actionMyNotesFragmentToNoteDetailsFragment(note))
  16. Display the displayed Note data in NoteDetailsFragment

    1. Within NoteDetailsFragment, we can access the passed arguments using private val args: NoteDetailsFragmentArgs by navArgs()
    2. We then will create a class named NoteDetailsViewModelFactory that takes the selected note as a constructor property and implements ViewModelProvider.Factory
    3. Add a constructor property named note to NoteDetailsViewModel with type Note
    4. Override create() within NoteDetailsViewModelFactory to return an instance of NoteDetailsViewModel; passing the selected Note
    5. Within NoteDetailsFragment update the call to by viewModels() by passing factoryProducer = { NoteDetailsViewModelFactory(args.selectedNote) }. This will pass the selected Note to our view model when it's accessed for the first time.
    6. In NoteDetailsViewModel update the state to pull its data from the passed Note

Β 

πŸ–₯ Lab 11 Hints: Displaying List Data with RecyclerView

Β 

πŸ’‘ Helpful Resources

Β 

πŸ’‘ How to create the RecyclerView.ViewHolder?

RecyclerView.ViewHolder takes the view for a list item as its constructor parameter. How we provide that View is up to us. In this example, we are using the root property from a ViewBinding class.
We could also choose to pass a raw view into our ViewHolder instead of a binding class, and cache view references ourselves using findViewById().

class NoteViewHolder(val binding: ItemNoteBinding) : RecyclerView.ViewHolder(binding.root) {

  // this method is a convention, not a requirement
  // it encapsulates the binding of new data to the view within the ViewHolder
  // without this, we'd need to bind the data within RecyclerView.Adapter.onBindViewHolder()
  fun bindNode(note: Note) {
    binding.titleTextView.text = note.title
    binding.categoryText.text = note.category
    binding.contentText.text = note.content
  }
}

Β 

πŸ’‘ How to create an instance of DiffUtil.ItemCallback?

The idea behind this ItemCallback class is that it helps ListAdapter perform efficient diffing of items in the case where the contents of a ListAdapter are updated. Rather than completely rebinding every element, it will attempt to determine the smallest number of elements that must be updated based on what is on the screen.

object NoteDiffUtil : DiffUtil.ItemCallback<Note>() {
  override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean {
    return oldItem == newItem // if your data class has a unique id, you should compare those ids here instead of the full object
  }

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

Β 

πŸ’‘ How to create a RecyclerView.Adapter?

class MyNotesListAdapter(private val noteClickHandler: (Note) -> Unit) : ListAdapter<Note, NoteViewHolder>(NoteDiffUtil) {

  // creates a binding class that caches references to our views
  // this caching makes list scrolling more efficient by avoiding view lookups as elements enter and leave the screen
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
    val binding = ItemNoteBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    return NoteViewHolder(binding)
  }

  // rebinding our data allows us to update the content of an existing ViewHolder/View
  // this makes scrolling more efficient by avoiding re-inflation of views
  override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
    val note = getItem(position) // getItem() pulls data from the internal collection maintained by `ListAdapter`
    holder.bindNode(note)
    holder.itemView.setOnClickListener { noteClickHandler(note) }
  }
}

Β 

πŸ’‘ How to add the parcelize plugin?

// app/build.gradle
plugins {
  id 'com.android.application'
  id 'org.jetbrains.kotlin.android'
  id 'kotlin-parcelize'
  ...
}

Β 

πŸ’‘ How to add a simple ripple animation when I select my list item?

Try adding android:foreground="?attr/selectableItemBackground" to the root view of your list item layout

Β 

πŸ’‘ How to show my list of data in a grid rather that list?

RecyclerView supports drawing different configurations by using LayoutManagers.

Take a look at GridLayoutManager to draw items in a grid

Clone this wiki locally