Kotlin extension functions for self-documenting code

Lukas Vyletel
4 min readApr 3, 2020

Extension functions are without doubt one of Kotlin’s most loved features. Most articles on the internet and even many libraries use them, however, only for simple utility methods, that might be useful across whole project.

In fact, you’ll even find articles like this one that even warns you about misusing extending functions for anything else.

I am, on the other hand actively using extension functions to write easy-to-read self-documenting code. And if a comment can be replaced by extension function, you bet I’ll use one!

Example using SharedPreferences

In Android development SharedPreferences are often used to store simple data to persistent storage, such as user preference whether he wants to see a usage tip during launch. Perhaps we also allow users in our app to specify whether they want to see the tip in the morning or in the evening? And perhaps this setting is stored in SharedPreferences? Let’s say we have a class that needs to figure out if we should or should not display usage tip during the day. It could look like this:

class LaunchScreen(private val sharedPreferences: SharedPreferences) {
fun launch() {
if (sharedPreferences.getBoolean("SHOW_LAUNCH_TIP", true)) {
val timeToShowPreference = sharedPreferences.getInt("TIME_TO_SHOW", -1)
val isCorrectTimeOfDay = when (timeToShowPreference) {
// wants to see it in the morning
0 -> LocalDateTime.now().hour in 6..10
// wants to see it in the evening
1 -> LocalDateTime.now().hour in 19..21
else -> false
}
if (isCorrectTimeOfDay) {
showLaunchTip()
}
}
}

fun showLaunchTip() {
TODO("Show launch tip")
}
}

Surely, there are worse pieces of code written on the internet than the above example.

And yet, it is a little bit tricky to understand and interpret the value TIME_TO_SHOW from SharedPreferences . It’s just some integer. We don’t know what the integer means. Luckily, we do have some helpful comment that kindly explains it to us!

Declarative reading from SharedPreferences

That said, perhaps we could improve this a little bit, by adding an enum:

enum class TimeToShowTipPreference {
MORNING, EVENING
}

Now we can do something interesting:

private fun SharedPreferences.timeToShowTipRaw(): Int = 
getInt("TIME_TO_SHOW", -1)

private fun SharedPreferences.timeToShowTip(): TimeToShowTipPreference? =
TimeToShowTipPreference.values()
.find { it.ordinal == timeToShowTipRaw() }
private fun SharedPreferences.wantsToSeeLaunchTip(): Boolean = getBoolean("SHOW_LAUNCH_TIP", true)

This will make the original class easier to read:

class LaunchScreen(private val sharedPreferences: SharedPreferences) {
fun launch() {
if (sharedPreferences.wantsToSeeLaunchTip()) {
val isCorrectTimeOfDay = when (sharedPreferences.timeToShowTip()) {
MORNING -> LocalDateTime.now().hour in 6..10
EVENING -> LocalDateTime.now().hour in 19..21
else -> false
}
if (isCorrectTimeOfDay) {
showLaunchTip()
}
}
}

fun showLaunchTip() {
TODO("Show launch tip")
}
}

Did you notice how we dropped the comments explaining what branches of the when statement mean? They’re no longer necessary!

Now we know what we’re reading from SharedPreferences. We gave a sensible name to a piece of code that reads who knows what kind of a raw value from SharedPreferences , and even return it in a sensible type.

We’re reading time to show tip from SharedPreferences and do different check if the time to show tip is morning or evening .

Extension functions to explain conditions

But what check are we exactly doing? Am I the only one, who is lazy and doesn’t want to think about what this condition LocalDateTime.now().hour in 6..10 actually means?

Perhaps if we just…

private fun LocalDateTime.isMorning(): Boolean = LocalDateTime.now().hour in 6..10


private fun LocalDateTime.isEvening(): Boolean = LocalDateTime.now().hour in 19..21

and that would change our class to:

class LaunchScreen(private val sharedPreferences: SharedPreferences) {
fun launch() {
if (sharedPreferences.wantsToSeeLaunchTip()) {
val isCorrectTimeOfDay = when (sharedPreferences.timeToShowTip()) {
MORNING -> LocalDateTime.now().isMorning()
EVENING -> LocalDateTime.now().isEvening()
else -> false
}
if (isCorrectTimeOfDay) {
showLaunchTip()
}
}
}

fun showLaunchTip() {
TODO("Show launch tip")
}
}

Ok, that makes sense. When time to show tip is morning, we want to know if now is morning. If time to show tip is evening, we want to know if now is evening. If any of these are true, it is correct time of day. This is so easy to read!

To wrap it up, we can move this whole functionality to extension function as well:

private fun SharedPreferences.isCorrectTimeOfDayToDisplayTip(dateTime: LocalDateTime): Boolean =
when (timeToShowTip()) {
MORNING -> LocalDateTime.now().isMorning()
EVENING -> LocalDateTime.now().isEvening()
else -> false
}

private fun SharedPreferences.shouldDisplayTipAtTime(dateTime: LocalDateTime): Boolean =
wantsToSeeLaunchTip() && isCorrectTimeOfDayToDisplayTip(dateTime)

shouldDisplayTipAtTime and isCorrectTimeOfDayToDisplayTip are easily readable methods. Thanks to them, our original class finally shrinks to just this:

class LaunchScreen(private val sharedPreferences: SharedPreferences) {
fun launch() {
if (sharedPreferences.shouldDisplayTipAtTime(LocalDateTime.now())) {
showLaunchTip()
}
}

fun showLaunchTip() {
TODO("Show launch tip")
}
}

Now this class is perfectly readable. Not only that, but also methods behind the scenes, that determine whether we should display the tip or not to the user, are easily readable too.

Conclusion

In my view, extension functions are not simple utility methods. In my view, extension functions can be used to write code, that can be read almost like a sentence. Extension functions can be used to wrap a piece of code and put a name on it, effectively documenting it. And with such an easy-to-read code, who needs comments?

Bonus tip: You may have noticed I’m using private in the method signature of the extension functions that I posted here. This is a personal preference, but I usually keep extension functions in the same file as the class in question and I keep them private, so that they’re easy to find when reading through the class and they don’t pollute global namespace.

--

--