Targeting Android in a Kotlin/Multiplatform Mobile library

Salomon BRYS
Kodein Koders
Published in
8 min readApr 26, 2021

So far, we’ve seen how to create a Kotlin/Multiplatform Mobile library, how to access iOS Swift APIs in it, and how to push it to Maven Central. We haven’t discussed how to access Android APIs.

Kotlin is the main Android programming language, so how difficult can it be ?

Well, it turns out that the Android platform is not as simple as the JVM, so there are multiple strategies you can use to access Android APIs in your KMM libraries.

Let’s discuss them!

First and foremost, you need to make sure that you actually need an Android API, and not a JVM API. You should not have your library target Android if the API you want to access is actually provided by the JVM. Most of the file access, date time, cryptography, and many more APIs are JVM APIs, and even if your library is supposed to be used especially on mobile targets, it is way simpler to target the JVM rather than adding the entire Android toolchain to your build.

OK, so you’re positive you need Android APIs?

The way I see it, there are two ways you can add the Android APIs to the classpath of your library:

  • There’s the official way, which enables all Android capabilities to your library.
  • There’s the lighter way, a hack, really, which is more restrictive, but a lot simpler, which we’ll discuss at the end of this video.

The official way consists in adding the Android Gradle plugin to your build, as well as its corresponding Kotlin target & Android configuration.

Adding the com.android.library plugins and defining an Android target, rather than a regular JVM target, has a notable effect: what will be published is an AAR (for Android ARchive) rather than a JAR (for Jvm ARchive). This means that the library can package Android resources, such as images, XMLs, or JNI native libraries.

As you can see, most of the Android configuration is not happening in the kotlin block, but in the android block. This is because the Kotlin/Multiplatform plugin communicates with the Android plugin, and directly uses the regular Android configuration.

Note the publishLibraryVariants that is needed in the Android configuration. This is needed for your Android library artifact to be published.

Also, you need to configure the Android plugin to read the manifest in the androidMain source set, rather than the regular main source set where the Android plugin usually expects it.

Since we’re talking about the Android manifest: your Android library must declare one. It should be located in src/android/AndroidManifest.xml.

Here, for example, I am declaring that my library will access the Internet, and therefore I need to declare that the library will use the corresponding permission.

As with any regular Android library, when a KMM project will depend on this library, it will merge its manifest with the application manifest, and automagically add the permission declaration to its own.

That’s it! You can now access Android APIs in the androidMain source set!

As we’ve seen in our previous tutorial, it is easy to abstract a platform specific feature.

Let’s say we want an API that gets us various paths.

This is a very simple expression of that API: we expect an object that provides two functions to get us paths to different directories.

The actual iOS implementation of that API is pretty straightforward:

This is a simple Kotlin translation of the Objective-C API used to access this information. Nothing new here that we haven’t already seen, so let’s move on.

Things become a little bit more complex once we implement this API for Android:

OK, so you may think that this is even simpler, but looks are deceiving: this code does not compile!

The problem is that we need an Android context to access that information.

This may look simple, but it is to me the biggest hurdle we need to overcome when creating KMM APIs for libraries targeting both Android & iOS.

The notion of context object does not exist in iOS, so we need to find a way to handle this Android problem in a multiplatform way.

As usual, there are multiple solutions we can use to solve this problem.

The simplest solution is to ask that the context be passed to the object at application creation, and store it statically:

Note that we are making sure that we are storing the application context, and not “a” context, so this ensures no memory leaks.

This is of course the simplest solution, but it only works if you are not accessing resources, or if you are only accessing resources that are not dependent on the activity configuration such as theme, orientation, etc.

This also means that any application using this library will need to call setContext in their application onCreate.

Another solution that’s a bit more complex is to declare Paths as an expect class rather than an expect object:

Note that we are not expecting a constructor.

Contrary to a regular class, if an expect class expects a constructor, then we need to explicitly define it. Here, we did not define a constructor, so that means that each implementation may have their own specific constructor.

Here is the implementation for iOS:

… and for Android:

This also means that it will not be possible to construct a Paths object in the shared multiplatform code. It’ll have to be done in the platform specific code. Therefore, any shared API that needs to access a path provided by the Paths object will have to take a Paths parameter.

We could go a step further and abstract the context itself. We could declare, in the Paths object, a constructor that takes an abstract context as parameter:

Note that this time, we are expecting a constructor that all actual implementations must provide. The actual Android implementation of this object would quite simple:

Here we can see a very nice trick: instead of declaring actual classes, you can declare actual type aliases, using existing classes, as long as the aliased class conforms to what’s expected. This is why we had to expect an abstract Context class: because the Android Context class is abstract, if we want to use a type alias, we need to expect an abstract class as well.

The actual iOS implementation therefore needs to define an abstract Context class as well:

Because Context is an abstract class, we need a way for the iOS application to create one, hence the IosContext class. This context will anyway be ignored since there is really no context notion in iOS. With this, you still need to pass a parameter that only exists in the target platform, but that context parameter can be used for all APIs that need a Context for their Android implementation, not just Paths.

The last solution (so many tasty choices, right?) is to use Dependency Injection. My friend Romain has told you in an earlier tutorial how to use Kodein-DI, our very own Dependency Injection container. What you can do is have the Android Context be bound in the container when you create the application:

Now you can have the Paths class request a DI object too:

And easily implement it for Android:

…and for iOS:

So what’s the difference between this and the very first solution where we used a static variable, you may ask? This feels the exact same idea: having the context be set at application startup. The difference is that we do not store it statically and access it through a DI container that can have, for example, contextual or hierarchical overloads: you will access the activity context if there is one, and fallback to the application one if there is none.

Romain will walk you through these advanced usage of Kodein-DI in a later tutorial.

What if you need to target both Android and the JVM?

Turns out the Kotlin/Multiplatform plugin does not support commonization of JVM & Android code. In the official Kotlin roadmap, the item “Sharing code between JVM and Android” is sadly “Postponed for later”.

This does not mean that you can’t target both Android & JVM code, just that IntelliJ IDEA won’t recognize shared JVM & Android code as JVM code. Any JVM API will therefore not autocomplete. However, while the IDE will report errors, the code will compile fine. This is obviously sub-optimal, and we need a better solution than programming with all of our IDE power turned off.

If you do need the full Android capabilities, then you can use this workaround:

When applyDevelopmentTrick is set to true, then we do not configure an intermediate source set, but add the directory as a source directory for JVM. Obviously, that means that the Android target is completely broken while this flag is set, but at least the IDE is tricked to work as expected.

If you do not need the full power of Android, then, you can use reflection to discriminate between Android and a classic JVM. You’d have to manually add the Android JAR to the classpath, but set it to compileOnly so that it is not declared as a real dependency of your library. Let me show you an example.

Kodein-Log is a multiplatform library for logging. It forwards logs to, depending on the platform and what’s available, Android Log, or iOS OsLog, or JS console log, or JVM Slf4J, or good old println.

Instead of declaring both the JVM and the Android platform, we made the choice to target only the JVM and detect Android at runtime through reflection.

First, we added the Android SDK Jar as compile only JVM dependency:

This provides the Android SDK to the classpath at compile time, but not necessarily at runtime, so that regular JVMs won’t need it. Then, we can easily detect at runtime if the Android SDK is in the classpath:

As you can see, we detect both Slf4J and Android availability from reflection, and have Kodein-Log behave accordingly.

By the way, you don’t have to depend on the entire Android SDK. You can shrink the Jar to have it contain only what you are interested into. This makes a very small Jar that you can distribute with your sources.

In Kodein-Log, we stripped everything except the nullability annotation, and the Log class, ’cause that’s really all we needed!

There you have it: multiple ways to add the Android target to your library, multiple ways to handle the Android context, everything you need to start contributing to the Kotlin/Multiplatform ecosystem!

I hope you learned something today! If you did, please consider subscribing to the channel, liking the video, and maybe even sharing it to your programmer friends ;)

This article is brought to you by Kodein Koders, a European service & training provider focused on Kotlin & Kotlin/Multiplatform. Do not hesitate to visit our blog, or our Youtube channel for more contents.

--

--