Beyond preferences

As Android developers we are used to having a choice. There are always several approaches to do something. Now, while being able to choose is generally a very good thing, it can make developer life pretty challenging. First, you need to know that there are alternatives. Second, you need to know them good enough to make a sound decision. And third, well, there is @Deprecated.

In the beginning

Storing and retrieving user settings has always been very simple on Android. android.preference.PreferenceManager has been around since API level 1.

val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val current = prefs.getBoolean("hasBeenSet", false)
println(current)
prefs.edit().putBoolean("hasBeenSet", !current).apply()

Only trouble is... We shouldn't use it any longer.

Let's briefly look at the advertised successor, androidx.preference. As you will see shortly, it is straightforward to use in most cases. The first step is to include the library in your build.gradle:

implementation 'androidx.preference:preference-ktx:1.1.1'

If you now change the import statement to androidx.preference.PreferenceManager the above example works without further changes. But there is more to preferences, for example the integration in the user interface of an app.

These classes (android.preference.CheckBoxPreference, PreferenceScreen and so on) have been deprecated as well, so we need to use the replacements provided by Jetpack Preference. The migration of basic preferences types is simple, yet you may face some effort when you have custom classes. I am not going into detail here, though. Because while this chapter might be closed (we do have a new king now) on other platforms, on Android it is not. Allow me to present...

Jetpack Security

Just because they are so easy to read and write, preferences have been picked for scenarios in which they are not the best choice. In other words:

Do not store a password using the preferences api

Preferences are written to xml files, which can be read easily once an attacker has gained access to the internal files directory of an app. Things would be much harder if that file was encrypted. That's what (among other things) Jetpack Security can do. The docs say:

Safely manage keys and encrypt files and sharedpreferences.

Let's take a look.

implementation 'androidx.security:security-crypto:1.1.0-alpha03'

Here's how to set things up:

val masterKey = MasterKey.Builder(this)
  .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
  .build()
val prefs = EncryptedSharedPreferences.create(this,
  "secret_shared_prefs",
  masterKey,
  EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
  EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

I am not going into detail. Basically you obtain a master key and feed it to EncryptedSharedPreferences.create(). The important thing is: afterwards you can access preferences in the same way like I showed in the beginning, using getBoolean(), edit() and putBoolean().

Now, isn't this cool?

It is.

But it's not the end of the story.

Jetpack DataStore

Back in September 2020 the Android Developers Blog featured a post called Prefer Storing Data with Jetpack DataStore. It starts by saying:

Welcome Jetpack DataStore, now in alpha - a new and
improved data storage solution aimed at replacing
SharedPreferences. Built on Kotlin coroutines and Flow,
DataStore provides two different implementations: Proto
DataStore
, that lets you store typed objects (backed
by protocol buffers) and Preferences DataStore, that stores
key-value pairs. Data is stored asynchronously,
consistently, and transactionally, overcoming most of the
drawbacks of SharedPreferences.

If you are interested in what these drawbacks are, please be sure to read the post. It's both interesting and enlightening. My point is: there is yet another way to read and write key-value-pairs. As it is set to replace shared preferences, let's see how to use Preferences DataStore.

implementation "androidx.datastore:datastore-preferences:1.0.0-rc02"

Here's how to prepare a data store. The docs say:

Use the property delegate created by preferencesDataStore
to create an instance of Datastore<Preferences>. Call it
once at the top level of your kotlin file, and access it
through this property throughout the rest of your
application. This makes it easier to keep your DataStore
as a singleton.

val Context.dataStore by preferencesDataStore("user_preferences")

My example is accessing a boolean value. We define it like this:

val key = booleanPreferencesKey("hasBeenSet")

Here is how to prepare a read:

val flow: Flow<Boolean> = dataStore.data
  .map { currentPreferences ->
    currentPreferences[key] ?: false
  }

To get the value we use

lifecycleScope.launch {
  println(flow.first())

I you the same coroutine to change the value:

dataStore.edit { settings ->
  val currentCounterValue = settings[key] ?: false
  settings[key] = !currentCounterValue
}

Granted, this may look strange at first sight. But please keep in mind that we are witnessing a strong movement towards coroutines in general and flows in particular. So our app code will inevitably become more asynchronous.

Conclusion

The old preferences api has been deprecated for quite a while, so we should now try to get rid of it. Google advocates Jetpack Datastore quite a bit, so it may be a safe bet to switch to it. On the other hand, the other alternatives I have presented, work well, too, and may feel a little more common. Anyway, the choice is yours. Which one would you pick? Please share your thoughts in the comments.

29