Simplify your Android code by delegating to sealed classes

Don’t you just hate having to religiously go through the same rituals when working with Bundles, Intent extras or SharedPreferences? Copy-pasting the same lines of code time after time feels like a constant chore, is more error-prone, not to mention less maintainable. So let’s see how we can tackle this issue with Kotlin. First we’ll go over two language features you’re probably familiar with, then combine them to address the above problem.

Delegation

Think of property delegates in Kotlin as a language feature that helps reduce duplicated code in custom property getters and setters. So instead of writing

var hour = 16
set(value) {
field = value.coerceIn(0, 23)
}
var minute = 20
set(value) {
field = value.coerceIn(0, 59)
}
var second = 0
set(value) {
field = value.coerceIn(0, 59)
}

…we could have something like this:

var hour by IntervalDelegate(16, 0, 23)
var minute by IntervalDelegate(20, 0, 59)
var second by IntervalDelegate(0, 0, 59)

Much better isn’t it? Now, the standard library includes a few useful delegates, like lazy() and observable(), but writing your own is pretty easy and can be a powerful tool in eliminating boilerplate.

Keep in mind that custom delegates are, in fact, just classes; so you can initialize member properties in their constructors as well as restrict access to them. So if the IntervalDelegate from the above example is only useful to us in a single file, there is no need to expose it — we can make it private instead.

Let’s dive into the details. There are two types of delegates based on the type of the property they will delegate to:

  • Read-only can only be used for read-only properties (val-s)
  • Read-write can be used for both read-only and read-write properties (val-s and var-s)

Writing such a class might seem a bit cumbersome at first. Luckily, there are two interfaces that can help: ReadOnlyProperty which only contains the method signature for an accessor and ReadWriteProperty which, as you might have guessed, contains both an accessor and a mutator signature. Implementing one of these will ensure that your delegate will work as intended, however, keep in mind that you don’t have to go that way. Writing the correct methods and using the operator keyword would suffice. Conveniently, you can copy-paste all the code you need from these interfaces — actually implementing them only makes sense if you leave your class open to extensions and want to enforce subclasses to be used as delegates, like we will a bit further down.

All in all, this is how an implementation of IntervalDelegate might look like:

The first generic parameter refers to the type of the object which will contain the property (in our case Any since we don’t care about that) while the second one defines the type of the property itself (Int).

As you might imagine, this trick comes quite handy when dealing with the Android SDK and once you get the hang of it, you might find yourself writing custom delegates all over the place. Furthermore, combining this with the concept of sealed classes can prove to be even more powerful.

Sealed classes

These are Kotlin’s way of dealing with algebraic data types. The most obvious thing that sets them apart from enums is that each type can have its unique constructor. Again, given that, we’re just dealing with classes, it’s possible to nest them or to define abstract members which allows us to do stuff like this:

The cherry on the top is that the sealed keyword ensures the compiler that all subclassing is done within the scope of that file, thus we’re able to write exhaustive type checks:

fun getScreenName(screen: Screen) = when (screen) {
Screen.Menu,
Screen.Leaderboard -> screen.screenName
is Screen.Game -> "${screen.screenName}, ${screen.score} points"
}

The function above will compile just fine, we don’t need an else branch.

Also, note how the smart casting infers that if the argument is of Screen.Game type, it must have a score property.

Putting it all together

Now, what if we were to write a sealed class that enforces its subclasses to implement one of the delegate interfaces mentioned earlier? A really powerful use case for this would be to eliminate boilerplate when dealing with containers of type-safe key-value pairs (such as Bundle, Intent or SharedPreferences).

How much fun would it be to read from, or write to Bundles the Kotlin way — by using the property access syntax and extension properties? We could save and restore an Activity instance’s state like this:

…or initialize Fragments with arguments the following way:

To implement the BundleDelegate used above, we could combine delegation and sealed classes:

This might seem like quite a lot of code at first glance, but considering how you only have to write it once and never think about it again, the benefits you get probably worth the effort of setting it up. Naturally, more child classes can be added at will should you want to support more data types. You might think about adding optional constructor parameters for default values too. Also, not having to duplicate the key constants reduces the possibility of errors.

We could also write a similar class for working with Intent extras.

In conclusion: if you ever wanted to write a maintainable wrapper for SharedPreferences that nicely encapsulates all of its implementation details, here is my suggestion:

Usage (after injecting an instance of SharedPrefManager with your preferred DI framework):

if (sharedPrefManager.booleanField) {
sharedPrefManager.intField = 3
} else {
sharedPrefManager.stringField = "You get the point."
}

As you can see, the syntactic sugar offered by Kotlin allows us to wrap our entire persistence handling into easy-to-read statements and assignments. SharedPrefManager does not leak the keys used to save the data, and can easily be changed to any other implementation, without affecting the API consumers. Last but not least: defining a new field is done with just a single line of code.

Using delegation and sealed classes to simplify working with Intents, Bundles or SharedPreferences are just three examples I found useful, but certainly there are other ways to benefit from this technique — do let me know if you can think of other use cases! Shares, claps, and feedback are also more than welcome.