Kotlin, an exploration of Sealed Classes

… and their part in shifting the UI async boundary.

James Shephard
BBC Product & Technology
5 min readSep 11, 2017

--

There are a number of teams developing for Android in the Design and Engineering department in Salford, and most are starting to use Kotlin to some degree. As part of an assessment of the language I’ve been exploring some of the features… And so far I’m feeling very positive about it.

One of the things that has caught my eye (once I got over null-in-the-type-system) was sealed classes.

An example of generic success and failure sealed classes.

There is an analogue in Swift, enumerations with associated values, and both of these appear to solve a problem I’ve circled a number of times.

The obvious place for using sealed classes is to make the access to data that is only present in some circumstances into a compile-time concern. For instance, given a list of data objects each of which is of a different class, you probably want to expose only those attributes that apply to the specific class.

In Java, there are a few options:

  1. Have a single object with all the attributes, document the object’s interface and maybe enforce access at runtime by throwing an exception (or just return null). This makes more sense when the attributes are simply optional e.g.hasImage() and an associated getImageUrl().
  2. Get the caller to cast to the specific class (similar, but where the access methods live on separate classes).
  3. Use a receiver object that is called-back with an instance of the specific class.

Here’s some slightly contrived example implementations. These illustrate messages that are received over a process boundary, and the data classes those messages are deserialised into. The messages represent user actions relating to programmes and episodes.

Documenting and restricting access at run-time:

Casting to a specific sub-class:

Using a receiver object (this is starting to look like the visitor pattern):

Blurgh.

The first relies on the caller of the interface doing the right thing, as does the second. The third looks odd and breaks the procedural flow of the code; suddenly there’s some code that’s run in a callback — but … when is it run? When are its side-effects applied? Does it run on the same thread? Will it have completed by the time the function returns? It looks strange on a data object.

Sealed classes, and the when() syntax, provide a compelling compile-time-enforced mechanism for dealing with this case:

A clearer, safer, interface for these data objects. Shown used here:

There’s lots going on here, but of note is Kotlin’s smart casting. The episodeId is accessible only in the call to downloadEpisode because Kotlin transparently casts message to a DownloadEpisodeMessage. Similarly in the other cases. No explicit casting, no complicated contracts with calling code. All just nice.

Is this just advanced switching? Yes, of course, and if we weren’t talking about data objects I’d be going on about replacing switching with polymorphism. But we are, so I’m not.

Alternatively this could apply to an optional value on a data object. Given an episode that may or may not be downloadable via a URL, in the case that it’s not, using sealed classes to make the download URL inaccessible at compile time adds safety and clarity:

But couldn’t we just use a nullable type? Well, yes, but it’s a opportunity to give a name to a thing, rather than relying on null meaning “not available to download”.

A tool to avoid receiver callbacks?

This mechanism got me thinking about how asynchronous, failure-prone, calls are made in our code. Network calls from the main (UI) thread are marshalled to and from a background thread via callbacks.

Not our code, just an contrived example, don’t look too closely.

The receiver-callback pattern is useful here for two reasons:

  1. It allows for the asynchronous nature of the call.
  2. With two callback methods (one for success and one for failure) we can allow for different results; objects of different classes for the two mutually exclusive eventualities, as in the shape example.

In the spirit of single responsibility it would be nice to separate these two concerns — allowing work to happen on a background thread and allowing for different results.

There’s also another issue: There’s no compile-time-guarantee that just one, or either of the callbacks will be called. That fact has to be communicated in the names of methods and classes or documentation, and has to be implemented properly and for all cases.

Obviously the application of sealed classes provides an alternative method of returning different results, and exactly one result is guaranteed in all but exceptional cases.

The problem of the asynchronous call then becomes a simple case of using a framework for marshalling data from one thread to another. Something we already have libraries for — threads and executors in Java for instance.

This could be nice! Using sealed classes and standard concurrency frameworks we can decouple the two concerns.

Let’s have a look at how we might implement this in Android.

Here’s a view interface for showing a list of contacts from a remote address book, and the associated UI presenter that handles marshalling the call and result from the long-running background task on and off the main thread.

And is used as so in this Activity:

Not bad, pretty clean. We’re marshalling data across the thread boundary in, what I think is, a good place — at the high-level UI component where we understand about the restrictions of performing long-running tasks on the main, UI thread. No other components need to deal with callback methods.

But what’s my problem with callbacks? I think it’s good at this point to remember that it’s always easier to adapt from a blocking interface to a non-blocking one (return-from-function adapted to return-on-callback) and awkward and error-prone the other way around. If you’re writing a collaborator that performs some long running task, think carefully about the API — does that function really need to take a callback? Are you making an assumption about the context it will be used in? Or could it just be a normal function with a return value, leaving the caller to decide how, when and if they want to push the call onto a background thread?

There’s probably some nice clean patterns and libraries for solving some or all of the problems I’ve discussed here. Modelling changes as events from Observables as per some of the new Android Architecture feels like it might side-step some of these issues… But, who knows?

Aside from the neat syntax, improved safety and hand-holding, it’s very interesting to me that a seemingly small additional language feature, like sealed classes, can lead to a larger architectural re-think. I’m expecting more realisations to come from exploring Kotlin’s other features (low-friction and inline lambdas, co-routines etc.).

--

--