Enhanced Deprecation in Kotlin
Kotlin has rethought the deprecation lifecycle and provided tools to make it seamless
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:
- A reason for the deprecation.
- What should be used instead.
- 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:
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?
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.
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.