Using KMP Compose libraries in Android Apps

Android Developers around the world have been very excited lately since Jetpack Compose has reached v1 stability in July, 2021. Right after this, Jetbrains also announced that their Compose for Desktop project was going to start its alpha release phase. This is great news for UI development because Desktop, Web, Android platforms (iOS and Mac worlds enjoy SwiftUI) are going to be unified under one framework. Learn once, apply everywhere!

Jetpack vs Jetbrains Compose?

Most people refer to Jetbrains Compose as Desktop Compose or Compose for Desktop. This article uses these terms interchangeably.

Jetpack Compose and Jetbrains Compose are non-identical twin projects. Jetpack variant is the main product which was intended to be replacement for Android’s native UI framework. Non-surprisingly Google put a brilliant team behind this project. The team went above and beyond the expectations of Android Developers to deliver a well-structured, multi-layered, multi-purposed framework which happens to be great for programming declarative UIs. Most layers of Jetpack Compose has nothing to do with Android.

In comes Jetbrains, authors of Kotlin programming language which powers Compose from foundation to peak. Jetbrains took advantage of how Compose decoupled itself from Android and also used skia for rendering UI elements. Compose for Desktop adopts many aspects of Jetpack Compose and brings it to Desktop and Web environments. Anyone who learns Jetpack Compose for Android can instantly become a Desktop App developer thanks to almost identical API of these 2 libraries.

How different?

There should be a catch though. An Android Library and Desktop Library cannot be seemingly the same. Don’t worry, they are not. Desktop Compose uses different artifacts which extend from Jetpack Compose artifacts. The main difference is that Jetbrains’ artifacts start with org.jetbrains.compose while Jetpack Compose uses androidx.compose . The divergence seems to almost stop here. Funny enough if you use Compose for Desktop, you will see that import statements indicate that packages are still prefixed with androidx.compose .

This code snippet is from a Desktop app

KMP possible?

Due to the fact that these artifacts are almost identical, Kotlin Multiplatform enables the use of Compose in common modules. We can create a KMP library with org.jetbrains.compose plugin and include this library in an Android or a Desktop app. The ability to use shared UI code written in Kotlin between Android and Desktop is incredible. Moreover, KMP provides us with expect/actual paradigm which easily facilitates an opportunity to implement different designs for Android(androidMain) and Desktop(jvmMain).

However, now there is a problem. If you develop a KMP library with Compose, you sure must include Compose artifacts from Desktop Compose group such as org.jetbrains.compose.foundation|runtime|
animation|etc.
These dependencies are transitively passed to your Android App, which depends on your new library. Naturally you’ll be using Jetpack Compose in your Android App with Jetpack Compose dependencies such as androidx.compose.foundation|runtime|animation|etc. Do you remember that these two groups of modules are actually identical? When you try to build your Android app without further configuration, you’ll probably end up with the following Duplicate Class errors.

The real list of errors is quite long (>4122 lines).

How to fix?

Before we directly start with the fix, let’s take a minute and try to understand why this is happening. Truly understanding the core problem behind an issue is the most crucial step in reaching a resolution.

Our Android app already includes Jetpack Compose artifacts for a very basic reason; we were writing a Compose app.

Next, we wanted to have a nice functionality in Compose which is provided by a library. Developers of this library, or us, decided that it would be a good idea to share the library as KMP, so that Desktop developers too can benefit.

KMP library must have depended on Compose artifacts that are published by Jetbrains. These dependencies are transitively passed to our Android app.

Gradle takes a build command and starts dependency resolution. There is nothing wrong at first sight because Jetpack Compose and Jetbrains Compose modules are non-conflicting. They have different group, module names and also different versions. When the time comes for classPath analysis, compiler realizes a grim error. There are duplicate classes in the class path, too many of them. Compiler simply cannot understand which one to use, because there is no rule to help make the decision.

Now, we can continue with our options on how to fix this problem.

1) Use apply plugin: org.jetbrains.compose

First thing we realize is that many sample projects that use KMP Compose do not suffer from this error. It is because all of them apply org.jetbrains.compose plugin to their Android build.gradle as well. This seems to fix the issue altogether.

On the other hand, it feels a bit powerful as a solution when our only concern is using a KMP library in Android. This plugin might bring undesired configurations by just applying. We should take a look at the source code and see how the plugin fixes our error.

2) Module Replacement (Recommended)

Below link targets the exact place that implements the fix in plugin’s source code.

Desktop Compose plugin uses a process called Module Replacement in Gradle to get rid of colliding artifacts. You can learn more about this concept from official Gradle docs. Our takeaway is

What happens when we declare that google-collections is replaced by guava? Gradle can use this information for conflict resolution. Gradle will consider every version of guava newer/better than any version of google-collections. Also, Gradle will ensure that only guava jar is present in the classpath / resolved file list.

Gradle tells us that when a module is replaced by another module, conflict resolution is extended beyond version check. New module in replacement config trumps over the old module in every dependency resolution during the build.

So, we might choose to only adopt this part of the plugin and leave the rest. We only need to change the order of replacement because we would rather have Jetpack Compose artifacts in an Android app, compared to Desktop Compose artifacts.

Add replacements under dependencies closure

This should be an exhaustive list of modules that are provided by Jetbrains Compose team because we might never know which modules are transitively passed to our app from a library.

I personally recommend this approach not because the official plugin does it this way, but because the other solution is not as exhaustive and has an additional requirement to remember.

3) Dependency Substitution

We learned about Module Replacements but what is dependency substitution?

Again, consulting the official Gradle docs

A dependency resolve rule is executed for each resolved dependency, and offers a powerful api for manipulating a requested dependency prior to that dependency being resolved. The feature currently offers the ability to change the group, name and/or version of a requested dependency, allowing a dependency to be substituted with a completely different module during resolution.

This sounds awfully similar to Module replacement. Even more, Gradle allows us to conditionally apply Dependency Resolution rules. So, we can get rid of all incoming org.jetbrains.compose dependencies in favor of their Android counterparts. We only need a mapping between Jetbrains Compose and Jetpack Compose artifacts including version matching. Unfortunately, it would be infeasible to include this artifact mapping between two Compose variants into our build.gradle. Also, new updates would force us to add new mappings.

Instead, we can leave everything to Gradle’s version resolution. Let’s say incoming transitive dependency is org.jetbrains.compose.runtime:runtime:1.0.0-alpha3.

Its counter part in Jetpack Compose would be androidx.compose.runtime:runtime:1.0.1 .

If a Gradle Dependency Resolution rule is only programmed to replace the prefix with androidx.compose , new dependency would be androidx.compose.runtime:runtime:1.0.0-alpha3 .

This artifact simply doesn’t exist. However, Gradle doesn’t care about that. Gradle is going to be busy with version conflict because our dependency list already includes androidx.compose.runtime:runtime:1.0.1 .

Gradle will ignore the non-existent dependency in favor of higher and newer version that we already have in our dependency list.

Don’t forget that for this to work, Jetpack Compose counterpart of any replaced dependency from Compose Desktop should be included in dependency list of our Android app. This rule can be added as follows

Conclusion

If you are new to Compose and KMP, chances are high that you are going to experience Duplicate class error once in your experiments. There is no good resource currently explaining this issue, so I decided to write one about my experience. I recommend the exhaustive approach of module replacement until Jetbrains completely resolves this issue. Still, dependency resolution rules are super effective and useful as long as we know exactly what we are doing. I hope this article was helpful to the readers.

Opsgenie@Atlassian / Android Engineer