This is a snippet from the book “Kotlin and Android Development featuring Jetpack” by Michael Fazio, covering how to build a ListAdapter
class for a native (Kotlin-based) Android app.
A ListAdapter
class is used along with a RecyclerView
to display a list of items. The ListAdapter
handles connecting the proper row data to a layout and sending all that information into the RecyclerView
.
Create a Custom List Adapter
The PlayerSummaryAdapter
class is responsible for managing all the PlayerSummary
items in our list and handling how they’re displayed. We use a custom RecyclerView.ViewHolder
inner class
(meaning it lives inside PlayerSummaryAdapter
) to bind a PlayerSummary
item to the layout, then the RecyclerView
library handles the rest. All we need to do in PlayerSummaryAdapter
is tell the RecyclerView
what to do when creating and binding a new ViewHolder
, plus how to tell the difference between PlayerSummary
items in the list.
After creating PlayerSummaryAdapter
in the adapters package, first up is the PlayerSummaryViewHolder
inner class
. The PlayerSummaryAdapter
class both contains and depends on this class, so we’ll create it first then wrap PlayerSummaryAdapter
around it. The PlayerSummaryViewHolder
class inherits from RecyclerView.ViewHolder
and has a single function, bind()
, which takes in a PlayerSummary
object.
The bind()
function doesn’t do much other than assign binding.playerSummary
to the item
value. The binding value is an instance of PlayerSummaryListItemBinding
, which was generated by the data binding library when we added the generic <layout>
tag to the player_summary_list_item.xml
file. The item value, then, is the PlayerSummary
object coming into the method. Once that assignment is complete, bind()
then ensures bindings are executed so the data shows up properly with the executePendingBindings()
function.
inner class PlayerSummaryViewHolder(
private val binding: PlayerSummaryListItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: PlayerSummary) {
binding.apply {
playerSummary = item
executePendingBindings()
}
}
}
The PlayerSummaryAdapter
class around this inner class
inherits from ListAdapter
, which takes two type parameters and a DiffUtil.ItemCallback
instance. The type parameters are the type of item in the list (PlayerSummary
) and the type of ViewHolder
for those items (PlayerSummaryAdapter.PlayerSummaryViewHolder
). The callback piece is a new private class
at the end of the file (uncreatively) called PlayerSummaryDiffCallback
. That class looks like this:
private class PlayerSummaryDiffCallback :
DiffUtil.ItemCallback<PlayerSummary>() {
override fun areItemsTheSame(
oldItem: PlayerSummary,
newItem: PlayerSummary
): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(
oldItem: PlayerSummary,
newItem: PlayerSummary
): Boolean = oldItem == newItem
}
With both PlayerSummaryDiffCallback
and PlayerSummaryViewHolder
ready, we can get PlayerSummaryAdapter
created. This class, which inherits from ListAdapter
, will also contain a few overridden functions that we’ll create in a bit. The class declaration plus the other class and function from before together look like this:
class PlayerSummaryAdapter :
ListAdapter<PlayerSummary, PlayerSummaryAdapter.PlayerSummaryViewHolder>(
PlayerSummaryDiffCallback()
) {
//Overridden functions will go here in a bit.
inner class PlayerSummaryViewHolder(
private val binding: PlayerSummaryListItemBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: PlayerSummary) {
binding.apply {
playerSummary = item
executePendingBindings()
}
}
}
}
private class PlayerSummaryDiffCallback :
DiffUtil.ItemCallback<PlayerSummary>() {
override fun areItemsTheSame(
oldItem: PlayerSummary,
newItem: PlayerSummary
): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(
oldItem: PlayerSummary,
newItem: PlayerSummary
): Boolean = oldItem == newItem
}
There should be an error with the PlayerSummaryAdapter
as written since we’ve yet to implement the two abstract
functions from ListAdapter
: onCreateViewHolder()
and onBindViewHolder()
. Both functions are effectively one step so we can get them done pretty quickly.
onCreateViewHolder()
needs to know how to build instances of PlayerSummaryViewHolder
. That means we’re inflating our layout using the DataBindingUtil
class as we have done a few times in this book, sending that into a new PlayerSummaryViewHolder
instance, and returning that from the function.
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): PlayerSummaryViewHolder =
PlayerSummaryViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.player_summary_list_item,
parent,
false
)
)
onBindViewHolder()
is even more straightforward as it uses a PlayerSummaryViewHolder
instance from onCreateViewHolder()
, then sends a PlayerSummary
item into the bind()
function. We use the getItem()
function from the ListAdapter
class to get the correct PlayerSummary
based on where we are in the list. This is a major advantage of inheriting from the ListAdapter
class - it does almost all the work for us as far as handling the items and retrieving the correct one.
override fun onBindViewHolder(
viewHolder: PlayerSummaryViewHolder,
position: Int
) {
viewHolder.bind(getItem(position))
}
The PlayerSummaryAdapter
is now ready for use, so we can head over to the RankingsFragment
class to get everything connected.
Connect Adapter to RecyclerView
Here, we’re expanding on what we set up earlier with RankingsFragment
. Inside the onCreateView()
function, we instantiate a PlayerSummaryAdapter
object, then assign that to the RecyclerView
. Retrieving that RecyclerView
object turns out to be easier than previous times we’ve gotten view components because the entire view we inflated earlier is a <RecyclerView>
. As a result, we can convert the view value into a RecyclerView
instance, then assign the adapter property. We’re also going to add an ItemDecoration
to the RecyclerView
, which adds light gray lines between each row.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_rankings, container, false)
val playerSummaryAdapter = PlayerSummaryAdapter()
if (view is RecyclerView) {
with(view) {
adapter = playerSummaryAdapter
addItemDecoration(
DividerItemDecoration(context, LinearLayoutManager.VERTICAL)
)
}
}
return view
}
This is another great example of smart casting in Kotlin that we first saw in Chapter 5. Since we checked that view is an instance of RecyclerView
, view
is treated in that entire block as a RecyclerView
instance without having to create a new value.
Also, we normally would have assigned a value to the layoutManager
property on RecyclerView
like this:
layoutManager = LinearLayoutManager(context)
However, it wasn’t required since we already handled setting a LayoutManager
in the <RecyclerView>
tag inside fragment_rankings.xml
.
The RecyclerView
is now complete and has an assigned adapter to handle all its data. The last piece we need to cover here is how to get that data from the database into the PlayerSummaryAdapter
. To do that, we’re going to create RankingsViewModel
and observe a LiveData
value from there.
Thanks for Reading
Hopefully you enjoyed this preview of “Kotlin and Android Development featuring Jetpack” and gained some insight into custom ListAdapter
classes in Android.
Want to see more about the Penny Drop app or Android development? “Kotlin and Android Development featuring Jetpack” can be found in eBook form (PDF/ePub/mobi) on PragProg.com and soon as a physical book from various fine booksellers.
For more info about the RecyclerView
and ListAdapter
classes, check out the “Create dynamic lists with RecyclerView” article from the Android Developer team.