Skip to Content

Enhanced Deprecation in Kotlin

Kotlin has rethought the deprecation lifecycle and provided tools to make it seamless

Posted on
Photo by Eric Karim Cornelis  on Unsplash
Photo by Eric Karim Cornelis on Unsplash

Kotlin’s Deprecation annotation makes life a lot easier for everybody when deprecating code. In this post, I’ll show you what @Deprecated can do for us, and how to use it effectively. Your users will thank you!

Kotlin’s @Deprecated Annotation

Let’s suppose we maintain a Kotlin library, it doesn’t really matter what it does specifically. After all our hard work of designing and testing our library, we end up with a function like validateNameAndId below:

fun validateNameAndId(id: Int, name: String) = ...

After we ship it, we’re so happy until one of our users mentions “the function is called validateNameAndId but the argument order is id and name”. And they’re right, the function should be consistent. We could either change the function name to validateIdAndName or change the order of the parameters.

In the past, we’d slap a @Deprecated annotation on that method and maybe if we’re feeling generous, add some kind of description to the function’s inline documentation. It’s up to the consuming developer to change their code to accommodate our change.

Enhanced Deprecation

Kotlin’s @Deprecation annotation is designed to work with your IDE to let our users know, in a graceful manner, that not only have we deprecated our function, but to automates the upgrade process as much as possible! Kotlin’s @Deprecation annotation lets us specify three critical pieces of information, missing from the Java version of this annotation:

  1. A reason for the deprecation.
  2. What should be used instead.
  3. How serious do we want to make this? Just a warning or a compiler error?

Going back to our original function, let’s suppose we change the order of the parameters because the existing method name is fine, and anybody who calls it with named parameters is probably not bothered by the inconsistency.

@Deprecated(
    message = "Oops, we got the arguments backwards",
    replaceWith = ReplaceWith("validateNameAndId(name, id)"),
    level = DeprecationLevel.WARNING
)
fun validateNameAndId(id: Int, name: String) = 
    validateNameAndId(name, id)

fun validateNameAndId(name: String, id: Int) = ...

In terms of deprecations, this one isn’t bad because we can call the new function from the old function and swap the parameters around. But since we don’t want to maintain this version in our code forever, we can push our users to make this change in their code now.

Here’s what calling the old function looks like in my IDE (IntelliJ) when I hover my mouse over it, now that we’ve deprecated it:

Actionable Deprecation Warning

This is a nice improvement, it shows why this function is deprecated and what to do about it.

RepaceWith - Actionable Deprecation

So what? Great - we get a description in the IDE, that’s not such a big deal. Hold on! Because we’ve specified a ReplaceWith, the IDE can do the work for us.

Instead of hovering my mouse over the now deprecated function, what if I do Show Context Actions (Win: Alt-Enter, Mac: Opt-Enter) instead?

Replacement

IntelliJ is offering to do the work for us because we specified what to do in our deprecation annotation.

This is great because library authors can deprecate things and provide a seamless upgrade path to their users!

Note: While all of this work is being shown in IntelliJ IDEA, there’s no special reason why other IDEs or editors cannot provide the same feature.

A More Complicated ReplaceWith Example

Let’s see how ReplaceWith can help us beyond the basic example we’ve seen above. Instead of simply moving the argument order around, what if we changed the type of our name argument from a simple String to an Name class (which we’ll define as an inline class, just for fun). We’ll also define our Name in another package, to complicate things a bit more.

After those changes, here’s where we end up:

// In package com.ginsberg.inline

inline class Name(val name: String)
// In package com.ginsberg.common


@Deprecated(
    message = "Inline name and flip argument order",
    replaceWith = ReplaceWith(
        "validateNameAndId(name), id)",
        "com.ginsberg.inline.Name"
    ),
    level = DeprecationLevel.WARNING
)
fun validateNameAndId(id: Int, name: String) =
    validateNameAndId(Name(name), id)

fun validateNameAndId(name: Name, id: Int) = ...

There are a couple of different things I want to direct your attention to. First, we can refer to our Name class when wrapping our existing String name parameter, and Kotlin is smart enough to do the right thing. And second, because our Name class is in another package, we can tell ReplaceWith to add that class to the list of imports.

When we tell our IDE to do the conversion for us automatically, we end up with this:

import com.ginsberg.inline.Name

fun main() {
    validateNameAndId(Name("Todd"), 42)
}

The expression we’ve provided has been interpreted properly, and the import we provided has been added to our class!

The Deprecation Lifecycle

In addition to a description and a replacement expression, the @Deprecation annotation also lets us specify a level. This level lets us specify where in the deprecation lifecycle we are with removing our old code.

Warning, Error, Hidden, Removed

The Deprecation Lifecycle

1: WARNNIG

As we’ve seen above, the WARNING stage keeps our deprecated code in the compiled output, but using it shows up as a warning in the IDE. Callers are still free to ignore this warning and use the old code.

2: ERROR

Anybody referencing the old code will be met with an error when trying to compile against it. However, the old code is still generated and can be called if there are other compiled classes still referencing it. This is useful to maintain binary compatibility. Meaning that we can ship a new version of our library which prevents people from using our old function when they compile, but doesn’t prohibit already compiled code from using it because we maintain binary compatibility.

3: HIDDEN

This is the next to last step before complete removal. When in the IDE, our deprecated code will seem to be completely removed. However, the Kotlin compiler will still generate the old code in case other compiled code is still calling it. The use case for this is the same as above - maintaining binary compatibility without letting new code use the old function.

4: REMOVED

This is the final stage of the Deprecation Lifecycle - complete and actual removal from the codebase.

Summary

While nobody really wants to deal with deprecation, Kotlin sure makes it a lot easier than it has been in the past. By providing some additional information when we deprecate something, our users will have a much easier time changing their code the way we want them to. No more guessing about the right thing to do, or hoping our instructions are clear enough.