DEV Community

loading...
Cover image for Beyond preferences

Beyond preferences

Thomas Künneth
Developer. Speaker. Listener. Loves writing. GDE Android. Confessing mobile computing addict ;-)
・4 min read

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()
Enter fullscreen mode Exit fullscreen mode

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

A deprecation note

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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

val key = booleanPreferencesKey("hasBeenSet")
Enter fullscreen mode Exit fullscreen mode

Here is how to prepare a read:

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

To get the value we use

lifecycleScope.launch {
  println(flow.first())
Enter fullscreen mode Exit fullscreen mode

I you the same coroutine to change the value:

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

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.


Source

Discussion (7)

Collapse
androiddeveloperlb profile image
AndroidDeveloperLB

I have some questions about DataStore:

  1. Is DataStore ready to replace SharedPreferences completely? Including usage in SettingsActivity/PreferenceFragmentCompat ?
  2. Is there any way to see the content of the new files (seems they are in "files/datastore/....preferences_pb" path) ? Via the IDE? Somewhere else? What is the format of these files, anyway?
  3. Is there an equivalent of getDefaultSharedPreferences ? Or I always have to tell which file to use ?
  4. I know you shouldn't create large SharedPreferences files, as it could be inefficient and even cause OOM. Is it true for DataStore too?
  5. Do you know of an example of using it together with the new Splash Screen API, so that it could load which theme the user has chosen in the settings of the app before setting the content of the Activity ?
Collapse
tkuenneth profile image
Thomas Künneth Author

Hello. Happy to answer some of your questions.

Is DataStore ready to replace SharedPreferences
completely? Including usage in SettingsActivity/PreferenceFragmentCompat?

It covers quite a lot of aspects. I don't know, though, if the coverage is complete. Regarding UI, there is a nice article by Jordan Hansen covering this.

Is there any way to see the content of the new files
(seems they are in "files/datastore/....preferences_pb" path) ?
Via the IDE? Somewhere else?

Yes, through the Device File Explorer. Open it with View - Tool Windows - Device File Explorer.

Device File Explorer

What is the format of these files, anyway?

File opened in Androi Studio

A binary file format. Unfortunately I do not know if there is a viewer for it.

Is there an equivalent of getDefaultSharedPreferences ? Or I always have to tell which file to use ?

Well, as the docs say:

Creates a property delegate for a single process DataStore. This should only be called once in a file (at the top level), and all usages of the DataStore should use a reference the same Instance.

So you should use the same file.

I know you shouldn't create large SharedPreferences files, as it could be inefficient and even cause OOM. Is it true for DataStore too?

A main feature of DataStore is being asynchronous. It was built to overcome inefficiencies of the old preferences api. I did not do any profiling, though.

Sincerely hope this helps.

Collapse
androiddeveloperlb profile image
AndroidDeveloperLB
  1. OK seems it's supported there. Just not sure how to do it. Know of any nice sample of using it there?
  2. Yes I meant how to read them and view them...
  3. So I should choose some file that will be used for all (if I use a single file), right?
  4. Thanks.
  5. What about the theme&splashScreen question?
Thread Thread
tkuenneth profile image
Thomas Künneth Author

OK seems it's supported there. Just not sure how to do it. Know of any nice sample of using it there?

No, I'm sorry, nothing besides what's shown in the link (article)

Yes I meant how to read them and view them...

I'm aware of now viewer app, sorry. But as you can see from my code, access is really simple, so if I were to inspect the file, I would probably just iterate over the keys and print the values to console

So I should choose some file that will be used for all (if I use a single file), right?

Yes.

What about the theme&splashScreen question?

I might do a short demo in the future, but as of tight schedule can't say when.

Thread Thread
androiddeveloperlb profile image
AndroidDeveloperLB

Too bad.
Please let me know if you add a demo.

Collapse
tkuenneth profile image
Thomas Künneth Author

Sorry, I had almost missed your comment. These are very interesting questions. I'll see if I can provide some answers. Please stay tuned.

Collapse
tkuenneth profile image
Thomas Künneth Author

I was asked if I knew if Preferences DataStore could use encrypted files. I didn't. As this is a very interesting question, I decided to take a look at the source code. Here's what I found.

To prepare the data store, I used val Context.dataStore by preferencesDataStore("user_preferences").

source code of preferencesDataStore()

As you can see, an instance of PreferenceDataStoreSingletonDelegate is returned. Here's the interesting part:

code snippet from PreferenceDataStoreSingletonDelegate

So, next stop: PreferenceDataStoreFactory.create():

source code of PreferenceDataStoreFactory.create()

So, the final destination is PreferencesSerializer. It overrides two suspending functions, readFrom() and writeTo(). I guess that the changes would need to be made there. In theory the api might be expanded to pass in alternative implementations.

So, I hope you enjoyed this short dig. 😎