Android Generic TableView — Fully Customizable Library for Presenting Data

Daniel Dimovski
10 min readJan 10, 2022

--

A few months ago I was telling you about How I learned to stop copying and love the reusable components by explaining the custom AccordionLayout component I had created. Today we are going to revisit the topic of the reusable components by taking a look at one more library that you might find useful.

Let me tell you the story of Michael Bolton. Michael works as a software developer in a company called Initech. He’s been working on their Android application in the past few years and recently they’ve decided to add some management options to the application. Naturally, they turned to Michael for the job.

It was a simple request — you get a list of employees and their corresponding clients and you should present the data. The example CSV file contained three pieces of information, ID, first and last name:

1,Gideon,Mouatt
2,Meggie,Corrie
3,Ki,Wasbey
4,Naomi,Durnall
5,Riane,Bygrove
….

The list of clients also looked pretty simple — id, company name, phone number, the id of the employee that is assigned to this company and a country code:

1,Mayer Inc,720–935–3119,41,PH
2,Abshire Inc,738–229–9554,1,BT
3,Lynch Group,522–480–2357,37,ID
4,Paucek-Parisian,678–290–9772,94,ID

Obviously Michael decided to use the RecyclerView for this simple task. So, he created a model and bound it with a ViewHolder*:

data class Employee (val id: Int, val firstName: String, val lastName: String)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/employee_id" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/first_name" />

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/last_name" />

</LinearLayout>
class EmployeeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
private val id: TextView = itemView.findViewById(R.id.employee_id)
private val firstName: TextView = itemView.findViewById(R.id.first_name)
private val lastName: TextView = itemView.findViewById(R.id.last_name)

fun bindView(employee: Employee){
id.text = employee.id.toString()
firstName.text = employee.firstName
lastName.text = employee.lastName
itemView.setOnClickListener{
//todo open the clients view
}
}
}

So far, so good. Now Michael needed to do the same for the list of clients. A bit repetitive, but still not a big deal. So, he created a new model and thought maybe he could reuse the ViewHolder. But the Client model has an extra field that needs to be displayed, which turns out to be a bit problematic:

data class Client (val id: Int, val companyName: String, val phone: String, val assignedTo: Int, countryCode: Int)

Michael was at a crossroad. Does he create a new adapter and ViewHolder for this purpose, or does he try to reuse some of the components? He takes the high road, reusing what he already has, with slight modifications. So he adds a new TextView to the XML which will be used to display the country code for the Clients, but will be hidden for the Employees and refactors the names to represent the new way the fields are being used. Not too good, but serves the purpose.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/column1" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/column2" />

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/column3" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/column4" />
</LinearLayout>
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
private val column1: TextView = itemView.findViewById(R.id.column1)
private val column2: TextView = itemView.findViewById(R.id.column2)
private val column3: TextView = itemView.findViewById(R.id.column3)
private val column4: TextView = itemView.findViewById(R.id.column4)
fun bindView(employee: Employee){
column1.text = employee.id.toString()
column2.text = employee.firstName
column3.text = employee.lastName
column4.visibility = View.GONE
itemView.setOnClickListener{
//todo open the clients view
}
}
fun bindView(client: Client){
column1.text = client.id.toString()
column2.text = client.companyName
column3.text = employee.phone
column4.text = employee.countryCode
column4.visibility = View.VISIBLE
}
}

So a new version with this implementation is released and the bosses are happy they now have insights into their employees performance. But they want more. So they add more information to the data Michael needs to process. They now want to see if the employee is full-time employee or not and also manage their status directly from the app. Initech’s bosses only care about performance (which, in this scenario, means more clients), so they also want an option to remove an employee from the company, in case they feel their performance isn’t good enough.

Michael doesn’t like this request, but he has to do it if he doesn’t want to be the one removed from the employee list. But he is going to dislike this request even more when he goes back to his initial implementation. New columns? Toggle option for the full time status? Remove button? Where does this all fit in with the previous implementation!? He should have known better — every experienced software engineer has to know that the client requests change. And Often. What is Michael supposed to do? Try to make use of the code he already has by complicating things even further? Or try to find a better way to do it, a way that will enable Michael to implement these and any subsequent requests his bosses might have?

We all know the correct answer, yet so many of us opt in for the (seemingly) easier way. But if Michael doesn’t stop and refactor the code now, he’ll end up in bigger problems when the requests change (yet again). So, how do we go about making this code reusable, how do we account for every next request? We can’t really know what the next request might be, but we can plan for it nonetheless. By separating the pieces of code in different components that can then be extended, we’ll save ourselves a lot of trouble.

What Michael needs is a fully customizable, extendable and independent module that will allow him to alter the data presentation easily. Now he could develop this from scratch, but the good news is — I already did. Generic TableView has been developed to do exactly that — allow users to present data in versatile ways by using some pre-defined components, or simply extend from them and customize them to their needs.

It all starts with a GenericListElement, which should be extended by every class that you want to present in a table. The class takes one mandatory and 3 optional parameters:

The columnMap is a map of a GenericView implementations and a boolean value that indicates whether this column should be displayed or not. This parameter is mandatory. The GenericView is an abstract class that has to be implemented in order to be instantiated. There are several provided implementations, but you can always create your own custom view and use it.

The type is an optional parameter that you can provide, but are not required to. This indicates the ‘action type’ of the row, or rather the right-most element that is usually used for performing an action for the view. There are several pre-defined RowTypes (ButtonRowType, ChevronRowType, PositiveNegativeRowType and the default — GenericRowType). If you don’t supply this parameter, the row won’t have any ‘action’ attached to it. The actionTextRes and actionIconRes are also optional and they are used to set the text of the action and the drawable resource, if applicable.

This is all tied together with the GenericListAdapter and its paged alternative GenericPagedListAdapter. The adapter takes several parameters for different listeners or UI presentation options. Refer to the documentation for more details.

Now on to Michael’s problem, how does this apply to that situation? By using this library, Michael can now specify different number of columns for different situations and use different type for every column, e.g. use a CheckBox for the full time/part time column or add a button for the ‘action’ column. Let’s take a look how that would work:

Instead of a regular RecyclerView, let’s use the custom HeaderRecyclerView that will also add a header with the columns’ names:

<com.deluxe1.generic_tableview.view.HeaderRecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/recycler"
/>

And then let’s create the adapter for this RecyclerView:

adapter = GenericListAdapter(
maxDataColumns = 3,
showHeader = true,
onItemSelectedListener = this,
onRowActionsListener = this,
onRowClickListener = this,
actionTypeDetector = MyActionTypeDetector(),
highlightColorResId = R.color.teal_a700,
alternateColoring = true
)
binding.recycler.setAdapter(adapter)

So let’s see what’s going on here — we are defining the maximal number of data columns (this is not including the ‘action’ column) to 3 (first name, last name and full time status, we don’t want to display the id, but only use it to make a relation with the Clients file). Then we are annotating that we want to show a header with the column names. The onItemSelectedListener, onRowActionsListener and onRowClickListener are all listeners that enable the corresponding actions, so we can handle user’s long press, click on the row and click on the row action column.

The highlightColorResId indicates the color resource used for selection if the onItemSelectedListener is provided, and if alternate coloring is set to true, every second row will have a darker/lighter variant for better visual distinction.

Now this one is important: actionTypeDetector. If you want to use a custom view with a custom action, you’ll have to provide your own ActionTypeDetector that will map your custom view with the corresponding ViewHolder. This is what makes this library fully extendable, because you can add as many ActionTypes as you’d like, you just need to let the adapter know about them. Here’s an example:

class MyActionTypeDetector : ActionTypeDetector() {

override fun getActionTypeForInt(value: Int): ActionType =
when (value) {
MyActionType.getIntValue() -> MyActionType
CustomButtonActionType.getIntValue() -> CustomButtonActionType
else -> super.getActionTypeForInt(value)
}
}

With this I am adding two more ActionTypes, MyActionType and CustomButtonActionType that I can use with this adapter. The ActionTypes in turn tell the adapter which ViewHolder to use — this is where you define your custom layout:

object CustomButtonActionType : ActionType() {
override fun <T : GenericListElement> getViewHolder(
binding: GenericViewHolderBinding,
onRowActionsListener: OnRowActionsListener<T>?,
maxColumns: Int
): GenericViewHolder<T> = CustomButtonViewHolder(binding, maxColumns, onRowActionsListener)
}

The CustomButtonActionType indicates that it uses a CustomButtonViewHolder, which is an implementation of the GenericViewHolder that returns a MaterialButton for the action (with additional customizations):

class CustomButtonViewHolder<T : GenericListElement>(binding : GenericViewHolderBinding,
maxColumns : Int,
private val onRowActionsListener: OnRowActionsListener<T>?) :
GenericViewHolder<T>(binding, maxColumns) {

override fun getView(element: T): View {
return MaterialButton(
ContextThemeWrapper(
binding.container.context,
R.style.button
)
).apply {
//todo customize the button here
}
}
}

It sounds a bit complicated, but it’s easy once you get the hang of it. This is the way I thought will be best to make the library extendable and customizable. If you have a better idea on how to do it, I’d like to hear about it in the comments.

Okay, moving on. So, we have our adapter ready and set up. The Activity implements the listeners we provided to the adapter.

For the sample application I am getting the data from two files that correspond to the ones described in the beginning, with a few extra properties. You can see these files in the /raw folder of the sample app. First, we need to create the model that will extend the GenericListElement and tell the adapter which properties to use and how to display them. It’s pretty straightforward. We want to display the first name, last name and the full time property — which should be editable, but we don’t want to show the id. And we want the ‘action’ to be a button with ‘remove’ action. This is what it looks like:

data class Employee (val id: Int, val firstName: String, val lastName: String, val fullTime: Boolean)
: GenericListElement(
mapOf(
CustomTextView(R.string.id, id.toString()) to false,
CustomTextView(R.string.first_name, firstName, 1f) to true,
CustomTextView(R.string.last_name, lastName, 1.2f) to true,
CustomBooleanView(R.string.full_time, value = fullTime, true) to true
), type = CustomButtonActionType, R.string.remove
)

Then we parse these files and pass the data to the adapter.

override fun onResume() {
super.onResume()
adapter.setAdapterData(getEmployees())
}

private fun getEmployees(): List<Employee> {
val employees = arrayListOf<Employee>()
val ins: InputStream = resources.openRawResource(
resources.getIdentifier(
"employees",
"raw", packageName
)
)
ins.bufferedReader().forEachLine { line ->
val lineValues = line.split(",").map { it.trim() }
employees.add(Employee(lineValues[0].toInt(), lineValues[1],lineValues[2], lineValues[3].toBooleanStrict()))
}
return employees
}

The data is presented, the rows are selectable, clickable and they have a separate button for a different action (remove in this case). We only need to define what to do when each of these actions is performed.

override fun onRowClicked(row: Employee) {
startActivity(Intent(this, ClientActivity::class.java).apply {
putExtra("EMPLOYEE_ID", row.id)
putExtra("EMPLOYEE_NAME", "${row.firstName} ${row.lastName}")
})
}

override fun onItemSelected(item: Employee, isSelected: Boolean, totalSelected: Int) {
if (totalSelected > 0) {
menuItem?.isVisible = true
title = "$totalSelected Selected"
} else {
menuItem?.isVisible = false
resetActionBar()
}
}

override fun onAction(element: Employee, action: RowAction) {
adapter.removeItem(element)
}

And this is it. With a few lines of code in our activity, the data is presented the way we want it. You can find the full code on my GitHub, with additional documentation explaining the internal logic of the library. The second request for the list of clients is also implemented there and you can see it’s pretty much the same thing we did here, but getting the desired behavior with slight modifications in code. And the good thing is, we can do this for any data set, we just need a different model (and possibly the different view, if you want to customize it). The rest stays exactly the same.

  • Parts of the code are omitted on purpose to make this, admittedly lengthy read, smaller in size.
  • The names of the employees, the companies and their numbers are made up. They have been generated by Mockaroo.
  • Michael is not a real person nor is Initech a real company. All similarities are purely coincidental (except those that aren’t).
  • Please note also that the code snippets shown in this blog post may not compile — refer to the GitHub repo for fully functional sample.
  • If something doesn’t work, I must have put a semicolon in the wrong place or something.

--

--