Categories
android kotlin

Lessons Learnt with Kotlin: Using inline classes 👩‍🔬

What is an inline class? 🧐

An inline class is a special type of class defined in Kotlin that only has one property. At runtime, the compiler will “inline” the property where it was used. This is similar to inline functions, where the compiler inserts the contents of the function into where it was called.

Example without inline classes 🕵🏻‍♀️

For instance, we have two classes — Recipe and Ingredient, which both use a UUID as the type to represent the ID of the instance. When creating a new Ingredient class, it can be quite easy to pass the incorrect ID to the Ingredient class. This would still compile and there is no indication that there is an issue. 

// Without inline classes 😞
data class Recipe(id: UUID) 
data class Ingredient(id: UUID, recipeId: UUID) 

val recipeId = UUID.randomUUID() 

// compiles perfectly - but incorrect ID
val incorrectIngredient = Ingredient(recipeId, recipeId)

If you are lucky, you might have a unit test that indicates to you that you’ve used the wrong ID. But how can we make this more explicit? 

Example with inline classes 👩🏼‍🏫

// With inline classes ✅
inline class RecipeId(id: UUID)
inline class IngredientId(id: UUID) 

data class Recipe(id: RecipeId) 
data class Ingredient(id: IngredientId, recipeId: RecipeId)

val ingredientId = IngredientId(UUID.randomUUID())

// Can be quite easy to pass in the incorrect UUID without inline classes:
// wont compile! yay! 🎈
val doesntCompileIngredient = Ingredient(ingredientId, ingredientId)

val recipeId = RecipeId(UUID.randomUUID()) 

// compiles and is safer! 🔐
val safeCompilingIngredient = Ingredient(ingredientId, recipeId)

In order to use inline classes, you would need to create a class with a single value, and prefix it with the word inline. This tells the compiler to inline the type where possible but still provides compile-time safety if we try to use the incorrect type. 

In this example above, we create two inline classes for RecipeId and IngredientId, both which wrap the UUID. We then specify that those two data classes take RecipeId and IngredientId.

Now if we try to pass IngredientId into the RecipeId field, we will get a compile-time error! Which is great as this means more safety when writing code. 

Where have we used it? 👩🏻‍💻

Another example of where we have used it at Over is to represent Degrees and Radians. In our app, we work with both mathematical representations of angles. Initially, we represented both as Float types and tried to name our variables either val radians: Float or val degrees: Float. In the hope that this would be descriptive enough. But this eventually resulted in a few difficult to spot bugs. For example, we were adding degrees to radians, or sending degrees into a function that actually operated on radians. 

How did we improve this? 💃🏻

We defined Degrees and Radians as inline classes and added methods to each, to convert between the two if required. We also added some operator overloads to make working with the classes even smoother. 

inline class Degrees(val degrees: Float = 0f) {

    operator fun plus(degrees: Degrees): Degrees {
        return Degrees(this.degrees + degrees.degrees)
    }

    operator fun minus(degrees: Degrees): Degrees {
        return Degrees(this.degrees - degrees.degrees)
    }

    fun toRadians(): Radians {
        return Radians(Math.toRadians(degrees.toDouble()).toFloat())
    }
}
inline class Radians(val radians: Float = 0f) {

operator fun unaryMinus(): Radians {
return Radians(-radians)
}

fun toDegrees(): Degrees {
return Degrees(Math.toDegrees(radians.toDouble()).toFloat())
}
}

Now when we write a function that works with a specific type of angle, we can explicitly specify which type we expect:

fun Canvas.drawRotatedText(
    text: String,
    angle: Radians
) {
      // we know we need to convert the input Radians to Degrees  because Android Canvas works with degrees. 
     withRotation(angle.toDegrees()) {
        // draw the text.
     }
}

No more needing to worry that degrees will be added to radians anymore!

Why would I use them? 🤔

Inline classes can help with performance. If you use a normal wrapper class (without the inline prefix), you could introduce performance overheads as you would be allocating memory to create the wrapper class. Whereas with inline classes, there is no additional memory used since the properties are unwrapped.

Another great benefit of inline classes is compile-time safety, as we have just seen in the above examples. 

Lastly, you might be thinking “I can achieve the same thing with using a typealias”. Whilst this is somewhat true, a typealias doesn’t enforce compile-time safety, it will happily allow you to assign the underlying property without checking if it is the correct type. 

The following is an example of defining Degrees and Radians as a typealias:

typealias Degree = Float
typealias Radians = Float

var degrees : Degree = 180f
var radians : Radians = 0.543f

degrees = radians + 180f
println(degrees)

Unfortunately, the above runs perfectly without any compilation issues. Even though we are adding radians and degrees together then assigning it to a Degree class. 

Caveats 🚧

There are a couple of things to keep in mind before introducing inline classes into your codebase. One of the most important is that this is still an experimental feature which means that it could potentially be removed in future versions of Kotlin. 

Another caveat is that if you are trying to mock inline classes in your tests, say you want to use an argument matcher on one of them, things don’t work as expected (yet). There are issues open on two of the popular Kotlin mocking libraries: https://github.com/nhaarman/mockito-kotlin/issues/309 and https://github.com/mockk/mockk/issues/152

Summary

Working with inline classes has made me feel more comfortable in dealing with different properties that are represented by the same data type. The compile-time safety, coupled with the better performance has sold me on the language feature. However, I would keep in mind that it is still experimental and I would be cautious using it and have back up plans in place, should the feature be removed. 

Have you used inline classes yet? Where have you found them to be helpful? Let’s chat — find me on Twitter

References 📚