
Once upon a time on a calm Sunday evening, I randomly opened my email inbox, only to find out that there had apparently been a large number of crashes on the Simple app. The crime: Sudden crashes in the app when the nurses tried to register a new patient. This in itself was enough to make the team's Monday a manic one, with all energies focused on solving this high-priority bug.
Sentry (our crash reporting tool) indicated that this was happening in our 25% rolled-out production release. Since we were not able to retrace or reproduce this at our end, we initially thought of chalking it off to a technical glitch. But as soon as we rolled the app out to 100%, the crash was officially reported by multiple users. And so Watson (Sentry) and the team (Sherlock) went to the crime scene to look around for possible cues.
Our first step in this case was to go to the patient registration screen in the app and try to reproduce the crash. If we could reproduce the crash in our machine, it could help us in debugging the problem.
It is to be noted here that we had recently added a type-ahead search feature to the patient's village address on the patient entry screen, i.e. "Division and Area" form field as shown here. But even after multiple tries, we were not able to reproduce this issue, and this led us to an uncharted territory and brought along a lot of questions.

At this point, the only clue we had to investigate further was what our Watson - Sentry additionally handed to us—Sentry Logs. The message in the Sentry logs said - NullPointerException: Attempt to invoke interface method int `java.util.List.size()` on a null object reference

It was clear that we were messing up with nullability somewhere, which led to the exception. We now knew at least what the problem was, and all that was left was to figure out what was causing it and then fix it.
We began tracing back our steps using the Sentry logs, and started from the exact point where the crash culprit was. In this case, it was in the ArrayAdapter.getCount
method.
public class ArrayAdapter<T> extends BaseAdapter implements Filterable,
ThemedSpinnerAdapter {
...
@Override
public int getCount() {
**return mObjects.size();**
}
...
}
The approach we followed here was quite akin to what you'd do otherwise as well—question every single thing possible, till you can reach the answer. Almost like a modified, extended version of the 5 why analysis if you will.
"Why is the getCount
method throwing a NullPointerException
?"
One possible reason here could be that mObjects
is null and size()
is being called with a null object reference. So we checked where mObjects
was being set. But the objects
list that was being passed in the constructor was defined as non-nullable here, and so it wasn't possible to add null
in mObjects
.
public class ArrayAdapter<T> extends BaseAdapter implements Filterable,
ThemedSpinnerAdapter {
...
**private List<T> mObjects;**
...
private ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
@IdRes int textViewResourceId, **@NonNull List<T> objects**, boolean objsFromResources) {
mContext = context;
mInflater = LayoutInflater.from(context);
mResource = mDropDownResource = resource;
**mObjects = objects**;
mObjectsFromResources = objsFromResources;
mFieldId = textViewResourceId;
}
...
}
We then looked at the method we were using to pass in add elements to the mObjects
list. But the collection
array that was being passed in mObjects
was defined as non-nullable here as well. This meant that the library methods were handling the nullability properly.
public class ArrayAdapter<T> extends BaseAdapter implements Filterable,
ThemedSpinnerAdapter {
...
public void **addAll(@NonNull Collection<? extends T> collection)** {
synchronized (mLock) {
if (mOriginalValues != null) {
mOriginalValues.addAll(collection);
} else {
**mObjects.addAll(collection);**
}
mObjectsFromResources = false;
}
if (mNotifyOnChange) notifyDataSetChanged();
}
...
}
So, we backtracked even further to where this method was being called and where we were passing the collection
to this method.
class EditPatientScreen(context: Context, attributeSet: AttributeSet) : RelativeLayout(context, attributeSet), EditPatientUi, HandlesBack {
...
override fun setColonyOrVillagesAutoComplete(
colonyOrVillageList: List<String>
) {
colonyOrVillageEditText.setAdapter(villageTypeAheadAdapter)
villageTypeAheadAdapter.clear()
**villageTypeAheadAdapter.addAll(colonyOrVillageList)**
}
...
}
Even here, the colonyOrVillageList
was non nullable too.
We thought then that maybe we were doing something wrong in the colonyOrVillageEditText
where we were setting the adapter, and to verify, we went back and took a quick look at the layout folder. However, everything looked as expected and the documented way of defining MaterialAutoCompleteTextView
layout was being followed.
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/colonyOrVillageEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/patientedit_colony_or_village"
android:imeOptions="flagNoFullscreen"
android:inputType="textPostalAddress|textCapSentences" />
The next why we asked ourselves then was this: "Why was the colonyOrVillageList
somehow passing a null
even when it wasn't Nullable
?" To get an answer to this, we decided to see where we were loading the colonyOrVillageList
from.
class EditPatientViewRenderer(
private val ui: EditPatientUi
) : ViewRenderer<EditPatientModel> {
override fun render(model: EditPatientModel) {
...
if (**model.hasColonyOrVillagesList**) {
**ui.setColonyOrVillagesAutoComplete(model.colonyOrVillagesList!!)**
}
...
We checked if the model had the list or not and then sent a non null list to the setColonyOrVillagesAutoComplete
method. But then we asked ourselves —"Why did we have a null in this list somehow, especially if this was clearly non nullable everywhere?"
We had to go further down the road and take a quick look at where were loading the list from in theEditPatientModel
@Query("""
SELECT DISTINCT colonyOrVillage
FROM PatientAddress
ORDER BY colonyOrVillage ASC
""")
abstract fun getColonyOrVillages(): **List<String>**
We were primarily getting this list from the SQL query above, but it is to be noted that even this method returned a non nullable list of strings.
It was time to ask ourselves the last why question again and dig a little deeper:
"Why did we have a null in this list somehow, especially if this was clearly non nullable everywhere?"
"Where are we loading this list of colonyOrVillages from?"
To answer, we were loading it from the PatientAddress
table. And so, we looked it up.
@Entity
@Parcelize
data class PatientAddress(
...
**val colonyOrVillage: String?,**
...
) : Parcelable {
And guess what? The colonyOrVillage
parameter in PatientAddress
was NULLABLE, which meant that we were allowing to add null in place of colonyOrVillage
in the PatientAddress
table! We'd finally hit the jackpot and the road ahead seemed easier.
We tried to reproduce the bug by adding a few nulls in colonyOrVillage
column, and a few others having some non nullable values. And voila, it worked! The solution was pretty evident, all we had to do was add a small null
check in our SQL query that was and we were good to go.
@Query("""
SELECT DISTINCT colonyOrVillage
FROM PatientAddress
**WHERE colonyOrVillage IS NOT NULL**
ORDER BY colonyOrVillage ASC
""")
abstract fun getColonyOrVillages(): List<String>
On a second thought, there was another question that popped up too—"If this was the case, why did we not get any errors when we allowed null items in a non nullable list? But what we realised was that Room does not do a good job in handling null safety errors as the Kapt for Room is not written in Kotlin. Because of that, it doesn't know whether the type we set it is a nullable or non-null to throw compilation warnings. And instead, it returns null for every possible value. So here even though we set the return type to List<String> , we still get a return value of List<String?>.
At the end of the day, this small journey of finding our criminal and solving the crime has brought to light a few basic things we can learn about debugging problems:
- Error logs/stack traces are your Watson(best friend). They will help you get to the starting point from where you need to backtrack, until you find what you're looking for.
- It's always a good idea to repeatedly ask yourself the why questions till you can get to the root cause of the bug. Remember: Machines are deterministic. Bugs are caused due to human errors.
- Issues reported during partial rollouts should not be ignored or underestimated.