I have learned to delegate.
The Problem: Boilerplate Code
SharedPreferences
provide a relatively simple way of storing key-value pairs on Android.
The application might not only use them to save settings, but also to keep its persistent state.
However, when the app grows more complex, using SharedPreferences
might become painful.
The boilerplate code required to store and retrieve a preference takes more lines
than the actual operation performed on its value.
For example, consider a LoginActivity
that only displays input fields for username and password.
As a user, I want the application to remember my username so that I don’t need to type it again
the next time I want to log in—true story (pun intended).
How can this be done in Java?
First, we need to obtain an instance of SharedPreferences
object. Assuming our code is in the Activity
,
we can do it by calling getSharedPreferences()
:
SharedPreferences sharedPreferences =
getSharedPreferences(
"it.czerwinski.myapp.MY_PREFERENCES",
Context.MODE_PRIVATE);
Or, if multiple preferences files are not needed, we may use the default shared preferences instead:
SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this);
We may then retrieve a String
by calling the getString()
method, which requires two arguments:
the name of the preference, and the default value, returned if the preference does not exist:
usernameEditText.setText(
sharedPreferences.getString("USERNAME", ""));
Storing a value, however, is not that straightforward. SharedPreferences
object only provides
methods to get preferences. To modify their values, we need to use an Editor
,
returned by the edit()
method:
sharedPreferences.edit()
.putString("USERNAME", usernameEditText.getText().toString())
.apply();
We shouldn’t forget to call apply()
. Otherwise, the value would not be saved.
This code looks simple and clean… as long as we only need two or three calls to SharedPreferences
.
But typically, that is not the case, is it?
Anko Comes To The Rescue
Kotlin language is probably the best thing that has happened to Android developers since first Android phones hit the market. It allowed creation of powerful yet lightweight tools that simplify our lives.
One of those tools, Anko, provides an
extension property
of the Context
class, called defaultSharedPreferences
, that eliminates the need
to explicitly initialize the SharedPreferences
object.
Nonetheless, we still need to retrieve and save the values in the same inconvenient manner:
usernameEditText.setText(
defaultSharedPreferences.getString("USERNAME", ""))
defaultSharedPreferences.edit()
.putString("USERNAME", usernameEditText.text.toString())
.apply()
Utilize Kotlin Properties
If the same statements occur in code more than once, they should be extracted into a separate method,
or in this case, two methods—getUsername
and setUsername
.
But since we are using Kotlin, why not declare a property with defined custom
getter and setter?
We can also make it an extension property, so it might be referred from any Activity
in our app:
var Context.username: String
get() = defaultSharedPreferences.getString("USERNAME", "")
set(value) = defaultSharedPreferences.edit()
.putString("USERNAME", value)
.apply()
As a result, the calls to save and restore the username become much cleaner:
usernameEditText.setText(username)
username = usernameEditText.text.toString()
However simple, this approach still has its drawbacks.
The declaration of the username
property, even though a bit complicated,
at least keeps SharedPreferences
in one place.
But imagine that there are 10 similar properties in the app… or 20… or even 50…
Delegate
There are certain common kinds of properties, that, though we can implement them manually every time we need them, would be very nice to implement once and for all […]
The above quotation perfectly conveys our case. And Kotlin offers us a solution—delegated properties.
So let’s create a delegate that handles values stored in SharedPreferences
:
class StringSharedPreferenceDelegate(
context: Context,
private val key: String,
private val defaultValue: String) {
private val sharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
operator fun getValue(ref: Any?, property: KProperty<*>): String =
sharedPreferences.getString(key, defaultValue)
operator fun setValue(ref: Any?, property: KProperty<*>, value: String): Unit {
sharedPreferences.edit()
.putString(key, value)
.apply()
}
}
Now, the property can be delegated to an instance of StringSharedPreferenceDelegate
in a single line of code:
var username: String by StringSharedPreferenceDelegate(context, "USERNAME", "")
Reuse
[…] and put into a library.
Probably most Android applications use SharedPreferences
at some point.
So I decided to implement generalized version of delegates and put them into a reusable
library.
To use the delegates simply add a dependency to your build.gradle
(the package is available in
jcenter repository):
dependencies {
compile 'it.czerwinski.android:delegates-shared-preferences:0.1'
}
A delegated property can then be defined in your Activity
class:
var username by stringSharedPreference("USERNAME", "")
or in any other class containing a reference to the Context
:
var username by context.stringSharedPreference("USERNAME", "")
It might be a good idea to define an additional extension function, at least for those preferences, which are used in many different classes:
fun Context.usernameSharedPreference() =
stringSharedPreference("USERNAME", "")
var username by usernameSharedPreference()
What’s Next?
So far, the library only supports several basic data types:
Int
, Long
, Float
, Double
, Boolean
, String
and Set<String>
.
But in the future, I plan to implement delegates for other common types,
e.g. Array
, List
, Date
, as well as data
classes.
In the meantime, feel free to use the existing features. The library is still in development stage, but it has been fully tested.
Credits
- Social media image by Gerd Altmann from Pixabay