Implement Android RecyclerView list of checkboxes with “select all” option — double tier!
Creating a list of checkboxes isn’t a difficult mission. Adding a “select all” is arguably not too complicated as well.
But… what if we wanted to add subcategories? And… implement this in….. a recyclerView? Now this becomes a bit more complicated and confusing, and what is easier than finding a code that does exactly that with a simple explanation? So here we go…
Let’s start from the end, this is what we are trying to achieve:
A list of items, with sub categories and a “select all” option.
(Yeah, yeah, I know tomato is actually a fruit…)
Let’s define the row types so we are on the same page:
Here are the different scenarios we need to cover:
- If TopHeader is checked or unchecked => Update state of ALL the other items accordingly
- If CatHeader is checked or unchecked => Update state of the child items accordingly AND check if the TopHeader needs to be updated
- if a ProductRow is checked or unchecked => Check if CatHeader or TopHeader need to be updated
When would the parent need to be updated?
- If all it’s children are now checked , the parent must be checked as well
- If at least one of it’s children is unchecked, the parent must be unchecked
In this example we are passing a list of type RowModel, which includes the
- rowType
- productName for the product rows
- category — just a String in this example
- isChecked — which determines the state of the checkbox, so we can control a checkbox while computing (onBindViewHolder) a different row (holder)
[This is all simplified for the example, it probably makes more sense that the products and categories are objects]
Here is the list I created for the example:
The recyclerView is defined in mainActivity, where each row is a checkbox defined in row_layout.xml:
The adapter is where everything happens, let’s break it into pieces and see how we cover all the scenarios listed above.
[Again, this is simplified for the example, using different view types for the recyclerView is probably better practice]
In onBindViewHolder we set the text according to the view type in (holder.checkBox.text = … )
and add a few design lines, which I won’t elaborate on in this article.
Now, at the top we add:
holder.checkBox.isChecked = item.isChecked
setCheckboxTextColor(item.isChecked, holder)
If the data item isChecked == true
, we check the UI checkbox accordingly, and if necessary, grey out lines that aren’t checked, which will look like this:
The setCheckboxTextColor method is implemented in the adapter:
Now it is time to set the listener:
The first two lines, common for any view type, change the data item for the current holder and the text color.
item.isChecked = isChecked
setCheckboxTextColor(item.isChecked, holder)
Now let’s look at each view type separately:
TopHeader view:
RowType.TopHeader -> {
val indexList = mutableListOf<Int>()
productList.filter { it.rowType != RowType.TopHeader }.forEach {
it.isChecked = isChecked
indexList.add(productList.indexOf(it))
}
indexList.forEach {
notifyItemChanged(it)
}
}
These lines change the “isChecked” data for all the other items in the list. In IndexList we keep the positions that need to be notified to update when calling notifyItemChanged(it).
Note: from experience, updating the table with notifyDataSetChanged() can cause issues as it will be updating the header again
CatHeader view:
RowType.CatHeader -> {
val indexList = mutableListOf<Int>()
productList.filter { it.rowType == RowType.ProductRow && it.category == item.category }
.forEach {
it.isChecked = isChecked
indexList.add(productList.indexOf(it))
}
indexList.forEach {
notifyItemChanged(it)
}
isAllItemsSameStatus() //for header
}
So very similar to the top header code, we update all the children according to the father, but here we also filter on the category so we only change the correct items. After updating the children we need to see if the parent (top header in this case) must be updated as well, which is done in isAllItemsSameStatus() which we’ll get to soon
ProductRow:
RowType.ProductRow -> {
isAllItemsSameStatus(item.category) //set prep area accordingly
isAllItemsSameStatus() //set top header
}
Product row does not need to update any fellow rows, but might affect the top and category checkboxes which are implemented in isAllItemsSameStatus()
In order to update the headers we need 3 parameters:
- row: RowModel — the row data to be updated. For Top header it is always productList[0]. As to category row, we need to find it by calling:
row = productList.find { it.rowType == RowType.CatHeader && it.category == cat }
2. isChecked: Boolean — the state of the checkbox to be updated. For Top header we check if the number of rows that are checked equal the number of rows in total (minus the top header):
isChecked = productList.filter { it.rowType != RowType.TopHeader && it.isChecked }.size == productList.size — 1
For category row we do the same on the children only:
val subList = productList.filter { it.category == it.category && it.rowType == RowType.ProductRow }
isChecked = subList.filter { it.isChecked }.size == subList.size
3. position: Int — to determine which holder needs to be update, for top header it is always row 0
For category holder we find it:
position = productList.indexOf(catRow)
And finally update the header row:
Important! Checkboxes in recyclerView can cause issues, since the recyclerView, true to it’s name, recycles the views and the checkbox listener gets messed up. Therefore we add:
holder.checkBox.setOnCheckedChangeListener(null)
at the top, removing the listener that was already assigned to this holder. Be sure to add it before the checkbox is assigned.
And… That’s it! I hope this was clear, please ask post any questions in the comments and best of luck!
Full code can be found here: https://github.com/sharonelev/checkboxSelections