Introduction
Shared Preferences is one of built-in storage mechanism in Android. Android has few storage options which are available for developers:
- Files
- Databases
- Shared Preferences
Shared Preferences is one of the most useful ways of storing simple data. This solution allows you to store key-value pairs.
Let us start with getting an object which allows us to use SharedPreference API. A preference framework provides a default file for shared preference, or we can create another one with a custom name. Both versions work with SharedPreference interface.
We can get default shared preference object use the getDefaultSharedPreferences
method.
The getDefaultSharedPreferences
function is a part of the PreferenceManager class, which helps to create preferences hierarchy from activities and XML files. This method requires a context object, which can be an application or an activity one. It returns an instance of a SharedPreference interface, which gives us the possibilities to get and edit preferences.
Let us get an object which allows us to work with Preferences API using getDefaultSharedPreferences
method.
After adding a preference, the file with preferences will be created and stored in the "/data/data/APP_PACKAGE/shared_prefs/" folder in the following file: APP_PACKAGE_preferences.xml
.
We can also get non-default shared preference object use the getSharedPreferences
method.
- The
getDefaultSharedPreferences
function returns an instance of SharedPreferences interface, which gives us the possibility to get and edit preferences. - The
name
parameter of the function is the name of a preference file. - The
mode
parameter is a file creation mode. In the current version of API (28) only one mode is not deprecated, it’s called a MODE_PRIVATE. It means that the preference file can be accessed by the calling application or another application with the same User ID (UID).
val prefs = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
Default and non-default shared preference files use the same structure:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">Alex</string>
</map>
The getSharedPreferences
functions have a name
parameter; it means that we can create multiple files with preferences.
val userPrefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
val appPrefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
An implementation of SharedPreferences interface you can find here.
We have two layers of the storing preferences: in memory storage and file storage. Under the hood, we are interacting with an in-memory layer for better performance. However, these two layers will be synchronised.
In-memory layer stores everything in HashMap.
Before moving to save and getting preferences. Let us check which data we can store in preferences:
Boolean
Float
Int
Long
String
Set<String>
Let us take a look at preference file with properties of each type.
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="welcomeFlowFinished" value="true" />
<float name="welcomeFlowTime" value="7.35" />
<set name="devices">
<string>Nexus 5</string>
<string>OnePlus 3T</string>
<string>Pixel 2XL</string>
</set>
<long name="lastVisit" value="1541627846640" />
<int name="welcomeFlowSkippedScreens" value="0" />
<string name="userName">Alex</string>
</map>
Two main interfaces, which allow us, to work with preferences, are SharedPreferences
and Editor
. You can find an Android implementation of these two interfaces here.
The SharedPreferences
interface provides methods for accessing and modifying preferences stored in the XML file. For any particular change, we should use SharedPreferences.Editor
object.
The SharedPreferences.Editor
interface is used for modifying shared preferences. All changes are collected until we will use commit()
or apply()
methods.
Save data
We can save data in two different ways which have an impact on performance. Let us start with the commit()
method.
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.edit()
.putLong("lastVisit", Date().time)
.commit()
The commit method writes data to the disk use main thread which will be blocked until this operation wouldn’t be finished. However, from another side, we can be sure that data was added/updated in the disk when we check the result of commit function. The commit function returns a Boolean value. In the case when data was written to disk, this function returns true, otherwise false.
An opposite approach is using asynchronous operations. We can use the apply()
function for it.
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.edit()
.putLong("lastVisit", Date().time)
.apply()
The apply method updates in the memory storage of file preference and starts asynchronously commit changes into disk storage. As a result, the apply function cannot notify us about success or failure. It means that we cannot be sure when the operation is finished and which results of this operation we have.
Read data
We can use read data from shared preferences with one of the following methods:
String getString(String key, @Nullable String defValue)
Set<String> getStringSet(String key, @Nullable Set<String> defValues)
int getInt(String key, int defValue)
long getLong(String key, long defValue)
float getFloat(String key, float defValue)
boolean getBoolean(String key, boolean defValue)
All methods require key and default value, it allows us to avoid unexpected results. Let’s take a look at examples of using functions for different types.
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val isWelcomeFlowFinished = prefs.getBoolean("welcomeFlowFinished", false)
val welcomeFlowTime = prefs.getFloat("welcomeFlowTime", 0.0f)
val welcomeFlowSkippedScreens = prefs.getInt("welcomeFlowSkippedScreens", 0)
val lastVisitTime = prefs.getLong("lastVisit", 0L)
val userName = prefs.getString("userName", "unknown")
val devices = prefs.getStringSet("devices", emptySet())
Additional operations
We can use additional operations with Shared Preferences:
- Check if a value for a key exists;
- Get all key-value pairs in a map;
- Observe changes for Shared Preference properties;
- Remove key-value pair by a key;
- Clear all key-value pairs from preferences;
Check if a value for a key exists
The boolean contains(String key) method check that preferences have a key-value pair for the required key.
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.contains("device")
Get all key-value pairs in a map
The Map<String, ?> getAll() function provides a Map with key-value pairs. The main problem of this method is that data in memory layer or file can be desynchronized with this collection.
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val preferences = prefs.all
Observe changes for Shared Preference properties
The void registerOnSharedPreferenceChangeListener method allows you to observe changes for preferences. However, you should remember about using unregisterOnSharedPreferenceChangeListener
method to avoid memory leaks.
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.registerOnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == "devices") {
...
}
}
Remove key-value pair by a key
The SharedPreferences.Editor remove(String key) method allows you to remove key-value pair by required key. It will be done when the commit()
method will be called.
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.edit()
.remove("devices")
.commit()
Clear all key-value pairs from preferences
The SharedPreferences.Editor clear() method allows you to remove all key-value pairs from preferences. It will be done when the commit()
method will be called.
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.edit()
.clear()
.commit()
Shared Preferences under the hood
Preference framework uses two layers of storing preferences; it’s in-memory and disk storages.
Preference framework provides simple API for working with Shared Preferences. As we know, they are stored in files. Let us take a look at these files in detail. All Shared Preferences are stored in an XML file, which you can find in the "/data/data/APP_PACKAGE/shared_prefs/" folder. However, you should have access to the application folder on the device. If you use the emulator, you can do it without any problem.
Here you can find the content of the Shared Preference file.
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="welcomeFlowFinished" value="true" />
<float name="welcomeFlowTime" value="7.35" />
<set name="devices">
<string>Nexus 5</string>
<string>OnePlus 3T</string>
<string>Pixel 2XL</string>
</set>
<long name="lastVisit" value="1541627846640" />
<int name="welcomeFlowSkippedScreens" value="0" />
<string name="userName">Alex</string>
</map>
The main reason why only your application (and applications with the same User ID) has access to the Shared Preference is that they are stored in the application data folder. Android install each package with a new UID (User ID) by default. It means that each application has access only to own application folder.
Exploring Kotlin KTX from a shared preference perspective
Recently many developers are moving to Kotlin. Google provides a solution to avoiding boilerplate during working with Android API, and it called Android KTX. This is a set of extension functions which allows you to write less code for basic operations with Android API. We can find extension functions which can be used with Shared Preferences in SharedPreferences.kt file.
The current version (1.0.0) of Android-KTX has only one extension function for shared preferences. Let us compare a version of the code with and without Android-KTX.
Android code
sharedPreferences.edit()
.putBoolean(key, value)
.apply()
Android KTX code
sharedPreferences.edit {
putBoolean(key, value)
}
However, this function uses the apply method for saving preferences. If you would like to use a commit method, you should change it a little bit.
Android code
sharedPreferences.edit()
.putBoolean(key, value)
.commit()
Android KTX code
sharedPreferences.edit(commit = true) {
putBoolean(key, value)
}