Every software developer has heard of the phrase that goes something like “There are only two hard things in programming: naming things and caching”.
Caching is hard. Really hard. Especially in Android. I think that original phrase actually says that “cache invalidation” is hard rather than caching itself. But for the purposes of this specific post, I’ll just say that caching in general is hard especially in hardware devices with limited memory like our smartphones.
One of the Android projects that I’m working on needed to cache data to the SQLite database in order for us to
- Improve performance
- Implement some features that required us to keep track of certain changes in data on the client side
To meet the project’s requirements, I added on an ORM to help me make database reads and writes. For awhile, this was working fine. I was able to cache the data that I needed to and was able to implement some of the features that required us to keep changes that were being made to the data on the client side. Another positive side-effect of me implementing caches was that the app was someone offline-mode enabled, which was an added feature that was not part of the requirement. Everything sounded great until the problem started after we released the build as an alpha release.
Ah, the infamous Android fragmentation, or perhaps the more positive spin to the term “fragmentation” would be the wide variety of choices in devices that consumers can make if they’re buying an Android device rather than an iPhone. I test my builds on my OnePlus 3T and the emulator running on my computer. And in those environments, my builds work fine. However, as soon as we released the builds in alpha mode, I started getting all sorts of exception errors in Firebase Exception monitoring.
The 3 main types of errors that were getting reported were:
- Database errors
Some of these NullPointerExceptions were just me not writing code to defend against these Exceptions, mostly due to not being familiar with the dataset of my client’s API. So sometimes, some data’s attributes would have a null value, and I wasn’t handling those exception cases in my code. Thus the crashes. Okay, fair enough, I’ll handle these exceptions accordingly.
However, some of these exceptions were being caused by the records being retrieved from the database being null. What the heck???
Here’s one thing I realized about caching on the client side. When you have data coming from an API, and then you cache that data locally, and also treat that data (at times) as the real data, you technically have two sources of truths when it comes to the integrity of the data. It’s basically handling data at two places at a time. And sometimes, especially for an API with a large dataset with difficult-to-predict data integrity (some random attributes being null or empty), some of your database reads may not retrieve the data that you were expecting. And if they don’t retrieve the data that you were expecting in your code, then you’ll accidentally run into NullPointerExceptions.
Which brings me to the next item…
By database errors, I’m referring to general database errors that may happen on mobile devices. Things like errors that may happen when the devices tries to make a connection to the database and when it tries to close a connection to the database.
I’ve ended up getting a quite a few of these database error exceptions in Firebase and for whatever reason, majority of them were from Samsung devices. I’ve heard a long time ago that Samsung devices were kind of like the Internet Explorer of the Android development world in that it’s very difficult to get anything working consistently on them. Maybe that statement is completely incorrect, but a majority (if not all) of these general database errors were from Samsung devices.
But, the fact of the matter is, Samsung devices are very popular in the market. A lot of people use Samsung devices as their primary device. So, blaming it on Samsung is not the right thing to do. Because of these weird database exceptions, I had to write custom code just to circumvent these exceptions.
Let’s say we have a books table with an author_id column as a foreign key. In our codebase, we have a Book model and an Author model. Just for the sake of example, I’m using SugarORM to write a query that will retrieve a list of all Books in our SQLite database with an author_id of 2.
List<Book> books = Book.find(Book.class, "author_id = ?", 2);
Now, this will work without any issues in most cases… except in cases where your user’s device has so much data that it actually makes your user’s device run out of memory while performing the query.
Yep, that will happen 🙂 In web development, we can simply increase the memory of our database, but hardware is a scarce resource in mobile devices. Thus, we have to be mindful of how we write our queries and whether that query and the sheer size of data that may be returned could cause the devices to run out memory and crash the app.
In conclusion, caching is hard. I think it’s even harder on constrained environments like mobile devices. I now find Android apps with offline features to be admirable pieces of work.
So, what’s the solution to all these problems that can come with implementing caching in your Android apps? This project where I learned these lessons, I will have to continue plunging on until the number of exceptions decrease and we can release the app since the caching mechanisms are already in place and removing those will probably increase the amount of work that I’ll need to do to complete the app.
However, in the future apps that I work on, I’ll be avoiding caching any data as much as possible, unless a feature in the requirements specifically calls for it. Even if I have to implement the caches, I will take every precaution necessary to prevent silly exceptions and go light on caching to keep the codebase simple and easier to debug/understand.