Using some Kotlin features is easy to write clean and readable code. Thanks to data classes, properties, extension functions and delegates a Kotlin class is usually smaller and easier to read compared to the equivalent Java class. In this post we’ll see how to use Kotlin delegated properties to simplify Android code. If you are not familiar with this subject you can take a look at the official documentation or to this post.
The Kotlin standard library contains many useful delegates, for example the lazy function can be used to create a property initialized only when it’s used for the first time. This delegate can be useful to easily initialize a property managed using Dagger:
private val navigationController by lazy { (applicationContext as GithubApp).component.navigationController()}
Using Dagger in the standard way you don’t need something similar because the property would be populated invoking an inject
method on a component. In a demo project you can find on GitHub I am trying to use Dagger in a simplified way, Activities and Fragments dependencies are manually retrieved from the Dagger component.
The code of the previous example can be simplified defining two extension properties to retrieve the Dagger Component from a Context
or from a Fragment
:
val Context.component: AppComponentget() = (applicationContext as GithubApp).component
val Fragment.component: AppComponentget() = activity.component
And now the property can be defined in an easy way:
private val navigationController by lazy { component.navigationController()}
If your class contains just a few injected field you can define them using a lazy delegate, you don’t need to define and invoke inject
method, the property doesn’t need any annotations (so no annotation processing is involved) and it can be declared private and withoutlateinit
keyword.
Another delegate defined in the Kotlin standard library is the map delegate. Using it a key-value map can be used in a static way. Sometimes we need to manage a map with a predefined set of keys, for example using Firebase Cloud Messaging the parameters sent by the server are available in a map:
class MyMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage?) {
super.onMessageReceived(message)
val data = (message?._data_ ?: _emptyMap_())._withDefault_ { "" }
val title = data\["title"\]
val content = data\["content"\]
_print_("$title $content")
}
}
Using the key defined as a string is error prone, I know you can define them in a constant somewhere but defining static final field (or something in a Kotlin object) is so Java 1.4 style. We can improve this code using a class with two properties managed using map delegates:
class NotificationParams(val map: Map<String, String>) {val title: String by mapval content: String by map}
The code can be rewritten using this class, it won’t compile if there is a typo in the field (that corresponds to the map key):
override fun onMessageReceived(message: RemoteMessage?) {super.onMessageReceived(message)val data = (message?.data ?: emptyMap()).withDefault { "" **}
** val params = NotificationParams(data)
_print_("${params.title} ${params.content}")
}
On GitHub there are already many libraries that can be used to simplify SharedPreferences usage in Kotlin. Let’s see how a custom delegate can be very useful to write a property saved in a shared preference. First of all let’s write an extension function of the SharedPreferences
class that defines a delegate:
fun SharedPreferences.int(defaultValue: Int = 0,key: String? = null): ReadWriteProperty<Any, Int> {return object : ReadWriteProperty<Any, Int> {override fun getValue(thisRef: Any, property: KProperty<*>) =getInt(key ?: property.name, defaultValue)
override fun setValue(thisRef: Any, property: KProperty<\*>,
value: Int) =
edit().putInt(key ?: property.name, value).apply()
}}
This code is not easy to understand if you are not familiar with Kotlin constructs (and maybe even if you are familiar with them). It defines a new extension method int
that can be invoked on a SharedPreferences
object, this method returns a ReadWriteProperty
that defines how the property will be read and written. There are two optional arguments: the default value and the key used to store the value in the shared preferences (the property name is used if the key is not provided).
Using this method we can define a class with a field connected to the shared preferences:
class MyClass(prefs: SharedPreferences) {var count by prefs.int()}
Every time the property is invoked the value is read (or written) from the shared preferences.
We can define similar methods for the other types, to avoid copy and paste we can use a generic method (I know, this is even less readable than the previous definition):
private inline fun <T> SharedPreferences.delegate(defaultValue: T,key: String?,crossinline getter: SharedPreferences.(String, T) -> T,crossinline setter: Editor.(String, T) -> Editor): ReadWriteProperty<Any, T> {return object : ReadWriteProperty<Any, T> {override fun getValue(thisRef: Any, property: KProperty<*>) =getter(key ?: property.name, defaultValue)
override fun setValue(thisRef: Any, property: KProperty<\*>,
value: T) =
edit().setter(key ?: property.name, value).apply()
}
}
The additional parameters are two functions (both are extension function to simplify the way they are provided when the delegate
function is invoked):
SharedPreferences
objectEditor
This function is defined as inline
to avoid runtime overhead, using this keyword the getter
and setter
parameters are not translated into a class in the bytecode (more infos are available in the official documentation).
And now we can easily write the methods for all the types that can be written in a shared preferences (here the fact that in Kotlin generics can be used also for the primitive types is really useful):
fun SharedPreferences.int(def: Int = 0, key: String? = null) =delegate(def, key, SharedPreferences::getInt, Editor::putInt)
fun SharedPreferences.long(def: Long = 0, key: String? = null) =delegate(def, key, SharedPreferences::getLong, Editor::putLong)
//...
These delegates can be used to write a class that stores a token and count how many times the token is saved (it’s not too useful, it’s just an example):
class TokenHolder(prefs: SharedPreferences) {var token by prefs.string()private set
var count by prefs._int_()
private set
fun saveToken(newToken: String) {
token = newToken
count++
}
}
When count++
is invoked we are reading the value from the shared preferences and saving that value incremented by one, something like this (but in a readable and compact form!):
prefs.edit().putInt("count", prefs.getInt("count", 0) + 1).apply()
SharedPreferences
is an Android SDK class, using this delegate we are using it in many classes, some of them are probably classes of the business logic of the app. Many developers like to keep these classes Android-free to test them using a JVM test. Using this delegate our class is testable on the JVM even if we are using an Android SDK class. We can write a JVM test using a SharedPreferences
fake implementation that uses a map to store the values:
@Test fun shouldCount() {val prefs = FakeSharedPreferences()val tokenHolder = TokenHolder(prefs)
tokenHolder.saveToken("a")
tokenHolder.saveToken("b")
assertThat(tokenHolder.count).isEqualTo(2)
assertThat(prefs.getInt("count", 0)).isEqualTo(2)
}
You can find the Kotlin version of FakeSharedPreferences
here and the original Java version in the Calendar repository (59 Vs 166 lines of code!).
EDIT: I created a small library based on the example of this post to use Kotlin delegates for shared preferences, you can find it on GitHub here: github.com/NaluLabs/prefs-delegates
And that’s all for the first part of this post, in the second part we’ll see how to use Kotlin delegates to simplify Architecture Components usage (you can have a preview looking at this demo project on GitHub).