The dark side of persistence & code shrinking in Android

Blog Infos
Author
Published
Topics
, ,
Author
Published
Posted by: Rotem Matityahu
Intro

This is a story about my recent battle with persistence in Android combined with an awesome tool called code shrinker.

When dealing with object persistence in Android, one has several implementation options:
A simple file, a database,ย SharedPreferences, or with the new and shiny Jetpackย DataStore.

If you choose to store objects in SharedPreferences, youโ€™re probably going to use a JSON representation.
When Androidโ€™s code shrinker joins this party, things can get pretty messy.

In this article, Iโ€™m not going to cover the capabilities of the Android code shrinker (R8/Progaurd), as there are plenty of articles about it.
Instead, I will showcase a problem I faced, my thinking process, and how I managed to solve it.

Letโ€™s start with an example, shall we?

Iโ€™m developing an app that has some user custom settings.
Since there is a single settings object, for simplicity, I decided to store the settings object as a JSON using SharedPreferences.

class Settings(
val volume: Int,
val userName: String,
val fontSize: Float,
val privacyEnabled: Boolean
)
view raw Settings.kt hosted with ❤ by GitHub

After the app was released, I realized that the font size should be taken from the system settings (because who among us wants to mess with the users’ font settings?). Naively enough, I openedย Settingsย class, removedย fontSizeย from it, and deployed the version to production.

A couple of days later, I noticed an issue in production โ€” theย Settingsobject couldnโ€™t be parsed.

What the heck?

I looked for theย Settingsย key in the SharedPreferences file (inย /data/data/<packageName>/shared_prefs/<fileName>) and this is what I saw:

{
"a":55,
"b":"rotem matityahu",
"c":30.0,
"d":true
}

First I was relieved because there was some data in the file; I thought it was deleted somehow.

The shrinker obfuscates the membersโ€™ names to short and meaningless names, in the same order as the class declares them. This process happens in each build.
In the first app version, the obfuscatedย Settingsย structure looks like this:
{โ€œaโ€: Int, โ€œbโ€: String, โ€œcโ€: Float, โ€œdโ€: Boolean}
Now letโ€™s take a look at 2 scenarios:

  • In the updated version the first field was deleted:ย The shrinker will assign the fields in the same order theyโ€™re written in the object and theย Settingsย structure will look like this:ย {โ€œaโ€: String, โ€œbโ€: Float, โ€œcโ€: Boolean}.
  • In the updated version the first and second fields were swapped:The shrinker will change the structure to beย :ย {โ€œaโ€: String, โ€œbโ€: Int, โ€œcโ€: Float, โ€œdโ€: Boolean}.

In both scenarios, thereโ€™s a fieldsโ€™ย typesย mismatchย between the first and the updated versions. Therefore, the persisted object in the first versionโ€™s scheme will be failed to be parsed in the updated versionโ€™s scheme.

In my case, it was theย fontSizeย removal.
Got it! To fix the issue I just need to addย fontSizeย again to theย Settingsobject (in the same order as it was before) and the parsing will succeed because all the original members exist in theย Settingsย class.

But did that really solve my issue? Not so fast.
I added the missing member, so why is theย Settingsย object still not being parsed?

Then it finally hit me โ€” Iย didnโ€™tย useย the member; even invoking the memberโ€™s getter would have been enough.
The code shrinker will discard members that arenโ€™t used (and on the way almost made me go to a shrink).ย This is what it does! It is equivalent to removing the members by yourself.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Combining Flutter with Protobuf to build a powerful mobile app

In this session, I will explain how to use Protobuf in a Flutter app to communicate between client-server. I will also discuss my learnings while using Protobuf in Flutter, and what are the pros and…
Watch Video

Combining Flutter with Protobuf to build a powerful mobile app

Angga Dwi Arifandi
Mobile Engineer
ING Netherlands

Combining Flutter with Protobuf to build a powerful mobile app

Angga Dwi Arifandi
Mobile Engineer
ING Netherlands

Combining Flutter with Protobuf to build a powerful mobile app

Angga Dwi Arifan ...
Mobile Engineer
ING Netherlands

Jobs

No results found.

Getting it right

As responsible developers, we always test our code during development, but we do it using a debug build (because who has time to wait for Proguard in every build?). Eventually, these types of issues fall between the cracks.

Ok, so how can we prevent and resolve persistent data migration issues in Android, similar to the one Iโ€™ve encountered?

Prevention

First and foremost, think carefully about which data persistence API is better for your use case when dealing with objects:

  1. Collections of objects:ย Prefer using a database. Thereโ€™s a migration process to implement in order to add or remove members from DB objects.
  2. Single objects:ย Forget about SharedPreferences and go take a look atย Proto DataStore. It offers compile-time safety and supports migration from version to version, using a predefined scheme.

If you still insist on using SharedPreferences and JSON, always provide a name to serialized members.
Depending on the JSON parsing library youโ€™re using, there are several options. With Gson, for example, you can useย @SerializeNameย and inย Moshiย you have theย @Jsonย annotation (most serialization libraries will have a similar feature).
This way, you are decoupling the members’ names and how they are being saved, which protects you from any future change in the stored object structure.
Take notice though, this solution might expose your business logic through the member names.

For a more generic solution, you can create a customย annotation, annotate the persisted object and add a Progaurd rule:ย keepnames.
Usage:ย -keepnames @com.your.annotation.package.PersistedObject public class * { *; }
By using this rule, the shrinker will keep the membersโ€™ names and wonโ€™t discard unused members. When storing an object this way, its membersโ€™ names will be saved as they are (without any obfuscation).
For that reason, beware of changing the members’ names after the object has been saved since the parse will fail.

Recovery

Thereโ€™s actually just one solution, and you probably knew it was coming: Migration. Parse the Object in the old format, map it to the new format and save it again.
Jetpack DataStore offers a simple and straightforward migration API from SharedPreferences.

Digesting it

As developers, weโ€™re not always aware of what is happening to our code after the shrinker does its thing; this can lead to bugs when we mix it with persistence.
Always keep in mind that your object is being shrunk and obfuscated by the shrinker.
I know itโ€™s kind of obvious, but sometimes we forget it.

My 2 cents

Preferring SharedPreferences was wrong in the first place.
I strongly believe that Jetpack Proto DataStore is the best solution for this use case and the future of persisting single objects. Besides being a superior communication protocol, it defeats JSON yet in another battle โ€” the battle of persistence (2โ€“0).

Thank you for reading and I hope you enjoyed it.

 

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE
blog
Shimmer is an effect that improves the user experience by providing a smoother transition…
READ MORE
blog
Following that, I wanted to see how it would be like to write tests…
READ MORE
blog
Publishing libraries as a Java, Kotlin, or Android developer can be a complex and…
READ MORE
Menu