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 ofDatastore<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 yourDataStore
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.
Top comments (8)
I have some questions about DataStore:
Hello. Happy to answer some of your questions.
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.
Yes, through the Device File Explorer. Open it with View - Tool Windows - Device File Explorer.
A binary file format. Unfortunately I do not know if there is a viewer for it.
Well, as the docs say:
So you should use the same file.
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.
No, I'm sorry, nothing besides what's shown in the link (article)
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
Yes.
I might do a short demo in the future, but as of tight schedule can't say when.
Too bad.
Please let me know if you add a demo.
Sorry, I had almost missed your comment. These are very interesting questions. I'll see if I can provide some answers. Please stay tuned.
for reading check (protoc)
stackoverflow.com/a/70345554/2437655
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")
.As you can see, an instance of
PreferenceDataStoreSingletonDelegate
is returned. Here's the interesting part:So, next stop:
PreferenceDataStoreFactory.create()
:So, the final destination is
PreferencesSerializer
. It overrides two suspending functions,readFrom()
andwriteTo()
. 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. 😎