Delightful Delegate Design

When developing a library, designing an easy to use API while hiding unnecessary implementation details from clients is fundamental. Today I'd like to show you some API design choices we've made for our library Krate, an Android SharedPreferences wrapper.

Delegates 101

Delegates (or to be completely precise, delegated properties) let you take functionality you'd write in a property's custom getter or setter and extract it into a class, making that logic easily reusable.

The standard library provides some handy delegates, for example lazy, which you initialize with a lambda that can produce the value to be returned by your property. lazy will only execute this lambda when the property is first read, and then cache its value:

val pi: Double by lazy {  
    print("Calculating... ")
    val sum = (1..50_000).sumByDouble { 1.0 / (it * it) }
    sqrt(sum * 6.0)
}

println(pi) // Calculating... 3.1415702709353357  
println(pi) // 3.1415702709353357  
println(pi) // 3.1415702709353357  

Now, this looks rather magical at first, as if it was a special language feature. However, as with many "built-in" features in Kotlin, this is merely clever use of language features that are available to everyone - we can create our own delegates very simply. We won't look at how lazy is implemented, although you can check it out by looking at its implementation in the standard library's source.

Instead, we'll take a look at a simplified version of one of the delegates that our library, Krate includes.

internal class IntDelegate(  
        private val sharedPreferences: SharedPreferences,
        private val key: String
) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return sharedPreferences.getInt(key, 0)
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        sharedPreferences.edit().putInt(key, value).apply()
    }
}

As you can see, we wrote a regular class, and conformed to the requirements of delegates, which is to implement the getValue and setValue operators with the given signatures. Note that the latter of these is optional: you only need it if you want your delegate to be applicable for a var, and not just a val. These requirements are an example of the many conventions around operators in Kotlin.

This delegate can be used very similarly to the built-in one we've seen before, using the by keyword to delegate the property's implementation to an instance of our class:

var score: Int by IntDelegate(sharedPreferences, "score")

println(score) // 0  
score = 34  
println(score) // 34  

The behaviour we see here isn't particularly impressive at first, as a simple Int would behave the same way. However, this property will keep its value persistently through application restarts!

Hiding the details

We could provide the delegate class above as-is for our clients, but there's no reason for them to know about or use this concrete implementation directly, which is why we've marked it as internal.

For this next part, you'll have to be familiar with the library's Krate interface. This is extremely simple - anything can be a Krate as long as it contains a SharedPreferences instance:

public interface Krate {  
    public val sharedPreferences: SharedPreferences
}

With that, what we can do instead of making our delegate implementation public is to create a factory function for it, which can also provide nicer API (not unlike lazy):

public fun Krate.intPref(key: String): IntDelegate {  
    return IntDelegate(sharedPreferences, key)
}

Using this method in a Krate implementation would look like this (we're using the SimpleKrate base class, which implements Krate by opening the default SharedPreferences using the provided Context):

class MyKrate(context: Context) : SimpleKrate(context) {  
    var score: Int by intPref("score")
}

Let's summarize what we've achieved here:

  • This function isn't available globally, only within Krate instances, due to being an extension on Krate. This is sensible scoping, as we always want these delegates to be within a Krate, because...
  • This way clients no longer have to provide a SharedPreferences instance for every delegate they declare, as this extension can access the sharedPreferences property of the Krate it was called on.
  • We've made our public API explicitly public, as recommended by the official Kotlin coding conventions.

As great as the code above seems, it won't compile: earlier we've marked our IntDelegate class internal, and a public function can't return an internal type. The compiler won't allow it, as it wouldn't make sense to expect our clients to use the returned value without knowing its type. And of course, the whole idea for creating this function was that clients aren't supposed to know about our concrete class!

We could create an interface for our class to implement and use that as our function's return type, but the good news is that two very handy interfaces for delegates are already provided for us in the standard library: ReadOnlyProperty and ReadWriteProperty. While we've seen that we can create delegates without them, using them makes the whole process better.

Our class allows both reading and writing the stored value, so we'll implement ReadWriteProperty:

internal class IntDelegate(  
        private val key: String
) : ReadWriteProperty<Krate, Int> {

    override operator fun getValue(thisRef: Krate, property: KProperty<*>): Int {
        return thisRef.sharedPreferences.getInt(key, 0)
    }

    override operator fun setValue(thisRef: Krate, property: KProperty<*>, value: Int) {
        thisRef.sharedPreferences.edit().putInt(key, value).apply()
    }
}

Again, let's look at what we've gained:

  • We don't have to know the signatures of the operators by heart anymore. We can just implement the interface, and then generate the methods to override!
  • The first type parameter of ReadWriteProperty lets us constrain the type of class that this delegate can be used in. In our case, we can state that this property can only be in a Krate.
    • We'll still keep our factory methods as extensions on Krate, this way they won't pollute the global namespace. If they weren't extensions, they'd still show up in autocompletion everywhere, they'd just produce an error when not used in a Krate.
  • The thisRef parameter of our methods finally makes sense: it refers to the Krate instance that our property is in, should we need to access it. And we actually have a use for it here: we no longer have to pass in a SharedPreferences instance to the delegate class, as we can access it via thisRef.

To fix our previous visibility issue, we can now make our factory function return a ReadWriteProperty<Krate, Int>:

public fun Krate.intPref(key: String): ReadWriteProperty<Krate, Int> {  
    return IntDelegate(key)
}

This is perfect for us: we're returning a well known type from the standard library instead of a custom one. This return type contains the information that the returned delegate can only be a property of a Krate, that it can both be read and written (e.g. it may be a var), and that it's of type Int.

Generics trouble

With the basics out of the way, let's get to the hard part. The method shown above covered the primitive types, but there comes a time when you need to store more complex data, and you want to do it quickly - without a database.

We've decided to add support for this in Krate, and for our new Gson based delegate, things got a bit more complicated. This delegate needed to be generic, since its purpose is to store any arbitrary type by serializing it into a String (and back).

We'll need to provide the generic parameter to Gson when deserializing a value, which we can do so by passing in a Type instance to the fromJson method. Since we can't get this from a T directly, we'll resort to the trick of using a TypeToken, usually used to get proper Type representation for nested types.

To simplify this example, we'll use a ReadOnlyProperty in the snippets, as the getter's implementation is what's in the spotlight here.

internal class GsonDelegate<T : Any>(  
        private val key: String
) : ReadOnlyProperty<Krate, T?> {
    override operator fun getValue(thisRef: Krate, property: KProperty<*>): T? {
        val string = thisRef.sharedPreferences.getString(key, null)
        return Gson().fromJson(string, object : TypeToken<T>() {}.type)
    }
}

We'll also create a wrapper function, using what we've learnt with our IntDelegate:

public fun <T : Any> Krate.gsonPref(  
    key: String
): ReadOnlyProperty<Krate, T?> {
    return GsonDelegate(key)
}

This looks pretty good on first look, and it even compiles! We'll have plenty of trouble at runtime though, in the form of ClassCastExceptions.

What went wrong here? The type being passed in is generic, but on the JVM generic types are really only checked at compilation time. At runtime, anything that's generic will essentially be an Object (or in Kotlin terms, Any). This yields good compilation time and runtime performance compared to some other approaches of handling generic types, but it also happens to be the source of our issues in this case.

The reason this causes trouble is that Gson makes heavy use of runtime reflection. When it inspects the type we pass in, it sees that it's the type "T", but since generic types are erased at runtime, it doesn't know what T stood for in the case of any given call to fromJson. All it knows is that it needs to create an Object, which it will do so by mapping the JSON string to a completely general JSON representation using nested ArrayLists and LinkedTreeMaps instead of our specific model objects that we'd expect to get.

You can read more about the effects of type erasure in these materials I've prepared for a conference workshop last year.

Reifying

Kotlin provides us with the reified keyword for situations just like this, when a type parameter's concrete value needs to be made available at runtime. A type parameter for a class can't be reified, so we'll need to create our Type instance outside of the class, and receive it as a parameter:

internal class GsonDelegate<T : Any>(  
        private val key: String,
        private val type: Type // the type to use for deserialization
) : ReadOnlyProperty<Krate, T?> {
    // ...
}

The good news is that we've been creating wrapper functions around our delegates anyway, so we can reify the type parameter in the function that creates our GsonDelegate:

inline fun <reified T : Any> Krate.gsonPref(  
    key: String
): ReadWriteProperty<Krate, T?> {
    return GsonDelegate(key, object : TypeToken<T>() {}.type)
}

Unfortunately, reified type parameters are only available to functions that are also inline, which wasn't required of our delegate functions before. With the addition of inline, this function won't compile, because just like our other delegate implementation, GsonDelegate is an internal class.

Using internal declarations in an inline function isn't allowed, as the process of inlining will essentially copy the function's body to the call site, where our internal declarations wouldn't be accessible from (this is explained in more detail in the official documentation).

Boundaries

There's yet another Kotlin feature we can make use of to circumvent this issue, @PublishedApi. Using this annotation on an internal declaration - such as our class - will make it available for inlining while maintaining its internal visibility.

@PublishedApi
internal class GsonDelegate<T : Any>(  
    // ...
) : ReadOnlyProperty<Krate, T?>  { /* ... */ }

This way, client code can include it indirectly via the inline function and it will be there in the compiled client code, but it still can't be referenced directly from client source code.

Since compiled client code might now refer to our internal declaration, it should be treated as public API in terms of introducing any changes to it. Even though client source code can't reference it, already compiled client code will still refer to it, which raises concerns of binary compatibility. You can read about what binary compatibility is and why it matters, even in the context of Android in this article.

In the actual library code, you'll see that we've added an extra layer of indirection for these reified functions. This way, only some helper functions needed to be marked with @PublishedApi, and our actual GsonDelegate class remained entirely internal. This means we can swap out this class freely as long as the helper functions' signatures are untouched.

What's next?

If you're developing Android apps and you're looking for an easy to use SharedPreferences solution, please do check out Krate! We've just released 0.1.0, which brings Gson support to the library (in a separate artifact).

Want to know how importing libraries carelessly can cause security problems with Gradle? Read our article telling the story of a jcenter issue we've encountered.

For more about Kotlin API design, you can read my article about designing DSLs, containing lots of example code, with implementations available in a repository as well.

Finally, continuing on the topic of how to create great Kotlin libraries, this article by Adam Arold lays out several important things to keep in mind.

That's it for now, thanks for reading!