Mastodon

Preparing for K2

K2 is around the corner (RC2 at the time of writing) and if you haven't prepared your project(s) for it, this post'll help cover some of the areas to watch out for.

A purple lego K2 logo with assorted construction tooling around it.
Let's be real, we are all using AI to generate blog header images. Don't look too closely at the finer details.

Kotlin's K2 release is around the corner (RC2 at the time of writing) and if you haven't prepared your project(s) for it, this post'll help cover some of the areas to watch out for. This post won't seek to explain all the under the hood changes, JetBrains has plenty of good documentation on that in the Kotlin blog!

The below is advice based on preparing over a dozen or so projects across my work and OSS for K2. JetBrains runs an "EAP Champions" program where they work closely with a bunch of us in the community to try to proactively find and engage issues with upcoming releases.

I've been tracking all of my testing and repos tested in this spreadsheet, which also has links to all of the WIP PRs where you can see the changes that have had to go into them.

I'd advise doing the following in order, getting your build working at each stage first before moving on to the next.

  • Bump the Kotlin version to 2.0.
    • This includes corresponding releases of KSP, compiler plugins, etc as needed.
  • (If applicable) switch to KSP2.
  • (If applicable) switch lint to the latest alphas.
  • (If applicable) switch lint to use K2 UAST.

The rest of this post is broken up into distinct sections. Feel free to skip any that don't apply to you!

K2

The compiler itself is nearly ready to go. JetBrains is now in the phase of squashing final bugs or postponing to 2.0.20/2.1.0. As of RC1, you can publish new binaries that can be used by projects still targeting Kotlin 1.9 (Kotlin supports N+1 forward compatibility). You should be able to use K2 2.0.0-RC2 in your projects today. Here's some things to look out for:

  • There are some cases around nullability that K1 missed that K2 will capture and (correctly) warn or error on.
  • Past suppression mechanisms like INVISIBLE_REFERENCE may now be warnings or be wholly unsupported. KT-67920
  • INVISIBLE_REFERENCE suppressions are completely unsupported now.
  • There are some new smart casting features that K2 can do, such as propagating smart casts across boolean checks
val isString = somevar is String
if (isString) {
  // Compiler understands you can do String APIs on somevar here.
}
  • If you use mockito in your project (please don't), un-stubbed coroutine functions may explode in exotic ways due to subtle changes in how the compiled bytecode works.
    • Don't try to mock types you don't own. For example – coroutines' internals make some assumptions about library internals that mockito breaks.
    • Definitely don't try to mock language features like suspend functions.
  • I've found some cases where kotlinc is more strict about generic type inference that you may need to explicitly declare in source.
  • If using Kotlin in a Gradle plugin, lambdas in 2.0 will no longer automatically be Serializable unless you annotate them with @JvmSerializableLambda or pass -Xlambdas=class to force the older behavior. See KT-45375 for more details.

At the time of writing, the above are the only areas I've still been needing to make source changes to prepare for. Where possible, you should backport these fixes into your main branch to minimize changes in the actual K2 branch cut-over.

Gradle

Gradle is a build system loaded with footguns and where you're most likely to run into integration issues with K2, especially in nontrivial or multiplatform projects. At the time of writing, KGP (kotlin gradle plugin) 2.0 doesn't have any issues left in the projects I test. The Compose Multiplatform and KMP plugins still have some rough edges, namely use you have a Desktop application with compose resources generation enabled.

K2 IDE Plugin Mode

It appears the K2 IDE plugin will not release with K2 itself. It's still in alpha right now, and not available in the latest stable Android Studio stable release. At the time of writing:

  • It does not support script files at all (particularly problematic if you use Kotlin Gradle DSL)
  • In my experience, struggles with any large files (1k+ lines).
  • Does not show diagnostic errors from third party FIR plugins.

You should try it out, but don't expect much right now or at the release.

Compiler Plugins

While not a stable API yet, many projects use some form of compiler plugin. Popular examples include Compose, kotlinx-serialization, zipline, etc. I also publish a couple - redacted-compiler-plugin and MoshiX. For almost all of these, you will need to go find a 2.0-compatible release to test K2, as compiler APIs change often between releases and require new builds of plugins built on top of them. Any first-party plugin (i.e. hosted in the Kotlin monorepo) does not need this as they are versioned with Kotlin itself. Compose has more to it, see below.

If a plugin does not have a corresponding release and your build fails with it, you will be stuck. See if the author is open to a PR to start a branch supporting K2 ASAP.

If you're a plugin author, please publish preview releases built against K2 to help folks test your plugin! For the most part, it's no different than any other update. There's breaking API changes, you need to update for them you know the drill. Be particularly mindful of any APIs using Descriptors, as these will not work anymore in K2 but are also not completely annotated with obsolete annotations.

Compose

Compose's compiler is moving to the Kotlin monorepo for K2, allowing it to simultaneously release with Kotlin. JetBrains and Google have written up great guides for setting this up. Do this in your prep branch

Compose compiler | Kotlin Multiplatform Development

JetBrains' doc.

Jetpack Compose compiler moving to the Kotlin repository
With the upcoming release of Kotlin 2.0, the Jetpack Compose a matching Compose compiler will release alongside each release of Kotlin.

Google's doc.

If you are using Compose Multiplatform, be sure to update to the latest 1.6.10 release. At the time of writing, that is 1.6.10-rc01: https://github.com/JetBrains/compose-multiplatform/releases/tag/v1.6.10-rc01

Parcelize on Multiplatform

Historically, if you wanted to use Parcelable in KMP, you had to do a trick by creating an @CommonParcelize annotation that you could put in your commonMain sources and then expect/actual that accordingly. In androidMain, you'd then actualize that as a typealias to the real Parcelize annotation. This would be in addition to an expect/actual Parcelable supertype.

In K2 this is no longer necessary. Not only does K2 now allow you to actualize with a superset of members and supertypes, but the Parcelize plugin has been updated to allow specifying a custom marker annotation. This means you can just write @CommonParcelize in your commonMain and that's it, no expect/actual needed for the annotation. You'll still need one for the Parcelable supertype, but that's simple. There's no Gradle DSL yet to specify this annotation, but you can pass the appropriate compiler option.

// In src/commonMain/kotlin
package com.example

@Target(CLASS)
annotation class CommonParcelize

expect interface CommonParcelable

// In src/androidMain/kotlin
actual typealias CommonParcelable = Parcelable

// In build.gradle.kts
kotlin {
  compilerOptions.freeCompilerArgs.addAll(
    "-P",
        "plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=com.example.CommonParcelize",
  )
}

KAPT

KAPT is Kotlin's tool for Java annotation processing. Popular processors like Dagger, AutoValue, and more still rely on it to work in Kotlin projects. Something you may not know is that KAPT is versioned. For most of its life, we've used KAPT 3. It sits on top of an API in the K1 compiler frontend called AnalysisHandlerExtension. This API goes away in K2, so KAPT had to be reimplemented to work in K2. As such, we now have KAPT 4. Canonically in KGP, it's referred to as "K2 KAPT".

KAPT 4 can be enabled via the kapt.use.k2=true gradle property. If all goes well, you shouldn't notice any functional changes. One caveat though is that it will no longer run the compiler IR backend during stub generation, so any compiler plugins that depend on that (i.e. Anvil) will require changes.

KAPT 4 is still in preview and may not be enabled by default at K2's release, but you should still try it out anyway.

If you are an annotation processor author and test your processor, I maintain a modernized fork of kotlin-compile-testing and added KAPT4 support in the WIP K2 branch (with releases available).

KSP2

KSP is a Kotlin-first annotation processing tool intended to replace KAPT for most use cases. Like KAPT, KSP was built on top of AnalysisHandlerExtension, and thus needed to be reimplemented to support K2. This is called KSP2. It plans to be API compatible with processors written for KSP 1.

This change is a little more involved, as the new implementation is built on top of a new Kotlin Analysis API. This API is the same API the K2 IDE plugin is based on, and is similarly experimental and still under active development. KSP2 is released as a part of the standard KSP release and controlled via ksp.useKSP2=true Gradle property. Note that this is false by default, even in 2.0.0-x builds.

KSP2 also requires significantly more memory than KSP 1. If you use KSP ubiquitously in your project, you may need to consider increasing your Gradle daemon memory. This should be resolved in 2.0.0-1.0.21 (context).

Similarly to KAPT4, I've added support for this in kotlin-compile-testing if you're a plugin author and want to test it.

It still has a lot of open issues though, so I don't think it's likely to be fully available by the K2 release. The version at the time of writing is still 2.0.0-RC2-2.0.20. You should still try it out. Below are a list of issues I've encountered in projects, in case any look likely to affect yours.

Kotlin Metadata

kotlinx-metadata-jvm is an API for performantly reading and writing Kotlin @Metadata annotations. A number of Java annotation processing and static analysis tools use it to understand Kotlin language features about bytecode they are processing/analyzing. With K2, this API has been stabilized and will be published with every Kotlin release under the org.jetbrains.kotlin:kotlin-metadata-jvm artifact.

JetBrains has published a migration doc here: https://github.com/JetBrains/kotlin/blob/master/libraries/kotlinx-metadata/jvm/Migration.md

More info: KT-48011

Android Lint

Android Lint's analysis core has a new K2 UAST implementation, but it's still experimental and probably will not be enabled by default for some time after the K2 release. However, you should try it out and use it if it works for you. The Android team has been super responsive to issues filed, often turning around fixes within a release. Like KSP2, it's also built on top of the new Analysis APIs in K2.

To enable it, set the android.lint.useK2Uast=true. You should also use the latest lint canary. If you didn't realize before, you can use newer lint versions with older/stable versions of AGP/Studio by setting the android.experimental.lint.version to the lint version you want to use. At the time of writing, the latest version is android.experimental.lint.version=8.5.0-alpha08.

At the time of writing, the only remaining issue we've encountered is this: https://issuetracker.google.com/issues/338232684

Anvil

If you use Anvil, there's a few caveats depending on your repo situation. In short, you need to hop on to the latest 2.5.0 betas we've been publishing and switch over to the new KSP support for any subproject that doesn't contain components or subcomponents.

// in build.gradle.kts
anvil {
+  useKsp(contributesAndFactoryGeneration = <true|false>)
}

Then in projects that contain components, you'll need to do the following.

  • Set the language level of the KAPT stub gen task to 1.9. This is necessary because in K2, Anvil's IR plugin that merges contributions will no longer run in KAPT 4 (aka K2 KAPT).
tasks.withType<KaptGenerateStubsTask>().configureEach {
  // TODO necessary until anvil supports something for K2 contribution merging
  compilerOptions {
    progressiveMode.set(false)
    languageVersion.set(KotlinVersion.KOTLIN_1_9)
  }
}

If you use interface merging/contribute interfaces to components, you will need to do the same for the regular KotlinCompile task. This is because the API that Anvil's IR plugin uses to add new superinterfaces is no longer supported in K2. If you don't use interface merging though, you don't need to do this.

tasks.withType<KotlinCompile>().configureEach {
  // TODO necessary until anvil supports something for K2 contribution merging
  compilerOptions {
    progressiveMode.set(false)
    languageVersion.set(KotlinVersion.KOTLIN_1_9)
  }
}

In the medium term, we're working on supporting a workaround for interface merging to work in FIR instead of FIR. This would allow avoiding the second workaround above.

In the long term, we're working on fully supporting Dagger KSP, which would obviate the need for using FIR/IR plugins entirely.

Performance

K2 promises a significant improvement to compiler and IDE plugin performance. At the time of writing however, the results on projects I've tested have been mixed. Anecdotally, others in the community have reported modest improvements in the compiler, but usually only around ~10%. Far from the 2x+ improvements that were advertised up to this point.

At Slack, our benchmarks actually show a ~17% slowdown (still using KSP 1 though). In CatchUp on the other hand, I've found significant improvements. In Circuit, I found similar slowdowns again. It seems to vary widely depending on the repo, and I would highly recommend doing your own measurements using the helpful post/repo JetBrains has put together.

Note that at the time of writing, I've not successfully been able to run the Kotlin Notebook in that repo.

Call to Action

Please test your projects now! Especially if you're a library developer, compiler plugin author, or Gradle plugin author.


Special thanks to James Barr for reviewing this post.