TL;DR
- One of the most essential skills for Kotlin Multiplatform Mobile cross-platform development is sensitivity to what code is platform-dependent or not.
- Platform-dependent code can be written entirely in Kotlin using KMM’s
expect
andactual
syntax or by defining an interface in the KMMcommon
module and implementing it natively in Android (using Kotiln) and iOS (using Swift). - Platform-independent code is written inside the KMM shared framework and can be used for any business logic for your application that does not directly depend upon any platform-specific code.
- Given the complexities of writing multi-platform code, this post provides an overview, and future posts will dive deeper into these topics.
Why Multi-Platform Development
After I released Gap Click on iOS, Android users were not shy about sharing their feedback that they, too, were excited about it. As an iOS engineer with almost no Android experience, I investigated various ways to expand to Android.
With the Kotlin Multiplatform version of GapClick in production for over 18 months and having released another app utilizing KMM, I have valuable lessons to share for the technology I’ve chosen.
This post discusses what platform-dependent code is versus platform-independent code and overviews the three primary approaches for sharing code between iOS and Android applications using Kotlin Multiplatform, explicitly targeted at iOS engineers. In subsequent blogs, I’ll dig into the trade-offs with representative code samples and my personal recommendations.
This post is part of a series on Kotlin Multiplatform:
- The iOS Engineer’s Guide to Beginning Kotlin Multiplatform Development
- Why iOS Engineers Should Avoid This Glorified KMM Technique
Clarifying KMM vs. K/N
Before we jump in, let’s make sure we’re on the same page by differentiating between Kotlin Multiplatform and Kotlin Native:
Kotlin Multiplatform
/kɒt lɪn muhl-tee plat-fawrm/
Kotlin Multiplatform Mobile (KMM) is an SDK designed to simplify the development of cross-platform mobile applications. You can share common code between iOS and Android apps and write platform-specific code only where necessary. Common use cases for Kotlin Multiplatform Mobile include implementing a native UI or working with platform-specific APIs.
Kotlin/Native
/kɒt lɪn ney-tiv/
a technology for compiling Kotlin code to native binaries, which can run without a virtual machine.
I will refer to the technology or SDK of Kotlin Multiplatform (which utilizes Kotlin Native) as “Kotlin Multiplatform” (or KMM, short for Kotlin Multiplatform Mobile). I will use “native platform” (the lowercase ‘n’ is on purpose) to refer to actual native implementations on either iOS (Swift) or Android (Kotlin).
(Side note: Although Kotlin/Native also supports other platforms, such as JavaScript, these articles will focus specifically on iOS and Android only.)
Step #1: Is Your Code Platform-Dependent?
The first step in determining where to put your code is to understand if your code is platform-dependent or not. Sometimes this is easy to discern, and other times it can be confusing. Let’s look at some examples to start.
Platform-Dependent Code
As an iOS engineer, you’ll know your code depends upon a platform framework if you’ve either added it to your project under “Frameworks, Libraries, and Embedded Content”:
… or you have imported it at the top of a Swift file:
The most obvious ones might be Foundation or UIKit. Some other obvious ones could be StoreKit, CoreGraphics, AVFoundation, or CoreLocation.
There are also some gray areas you might not realize are platform-dependent, such as networking APIs, accessing the application bundle, or file URLs, to name a few.
Platform-Independent Code
This is any code that you can write without importing any iOS frameworks or libraries. Model objects (or value objects) fall under this category. Of course, your application’s business logic would also be included. I also like to include the code required to implement the mobile architecture approach you’ve chosen, whether it be MVC, MVP, MVVM, or something else. Depending on how sensitive you become to platform dependencies, you’ll find there’s a lot more code that can be shared than you might initially think is possible.
How Code Is Organized Within a KMM Project
It might be challenging for those new to KMM to understand how code can be organized inside the project.
Platform-dependent code can be located either:
- Within the native Android mobile application (written in Kotlin), or within the native iOS mobile application (written in Swift), or
- Within the KMM shared framework written in Kotlin, placed in either:
- “androidMain” for Android-specific implementation code
- “iosMain” for iOS-specific implementation code
Platform-independent code can be placed:
- Within the KMM shared framework written in Kotlin, placed in the “common” module
Step #2: Writing Platform-Dependent Code
Option #1. Using KMM’s expect
and actual
Syntax
The Kotlin Multiplatform SDK includes a syntactical construct integrated into Android Studio, which can be used to define classes and functions that are expect -ed (pun intended) to have platform-specific implementations.
This approach is included in the Kotlin Multiplatform for iOS and Android tutorial, and the Kotlin documentation also references this as the way to access platform-specific APIs. Given this, I assume that this is the recommended approach from the KMM team.
In code: define a class or a function as expected, then provide the actual implementations for each supported platform - all written in Kotlin.
Code Example
Both the Platform class example in the KMM Tutorial, as well as the UUID function example in the KMM documentation, are straightforward and simple examples of this approach. In the spirit of simple and practical examples, here’s another example of an expected function declaration and the actual implementations for generating the epoch date/time number of seconds since 1970 for UTC:
Common Module expect
declaration:
1
expect fun currentDateTimeInSecondsUTC(): Long
androidMain Module actual
implementation:
1
2
3
4
5
6
import java.time.LocalDateTime
import java.time.ZoneOffset
actual fun currentDateTimeInSecondsUTC(): Long {
return LocalDateTime.now(ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC)
}
iosMain Module actual
Implementation:
1
2
3
4
5
import platform.Foundation.*
actual fun currentDateTimeInSecondsUTC(): Long {
return NSDate.date().timeIntervalSince1970().toLong()
}
Suppose you’re not already programming in Swift for iOS. In that case, it might not be apparent that the above Kotlin source code for the iOS implementation is based on Objective-C APIs, not Swift APIs. (“NSDate” might give this away.) This means that the Kotlin code you’re writing for iOS is effectively “Objective-C-ified Kotlin” and lacks many pleasantries that the Swift language provides. I’ll explain this further when deep-diving into this topic.
Option #2. Swift Implementation For a “Common” Kotlin Interface
As an iOS engineer, I quite enjoy writing code in Swift. Therefore, it’s no surprise that I’d prefer to write any iOS platform-specific implementation code directly in that language. This approach allows you to do exactly that! This is the primary benefit for iOS engineers when utilizing this method: writing your code in Swift instead of Objective-C-ified Kotlin.
For the platform-specific Android implementation, the code is still written in Kotlin; the only difference is that the code is placed within the Android app, not the shared framework.
So long as you have defined the interface in the shared framework “common” module, you can implement that interface within your iOS app using Swift (or Objective-C if you like) and within your Android app.
Code Example
For simplicity, I’ll continue to utilize the same example of retrieving the current epoch date/time in milliseconds.
Kotlin common
Module Interface:
This is just an interface that we can program to. Nothing fancy here - just the function definition. (iOS friends, you can compare this to a protocol
in Swift.)
* Note the package name defined on line #1.
1
2
3
4
5
package com.company.product.shared.dateprovider.EpochDateProvider
interface EpochDateProvider {
fun currentDateTimeInSecondsUTC(): Long
}
Native Android Implementation:
On the Android side, we simply implement this interface. Since we already did something similar above, the implementation here isn’t any different.
* Note that the interface is being imported from the shared framework on line #1.
1
2
3
4
5
6
7
8
9
import com.company.product.shared.dateprovider.EpochDateProvider
import java.time.LocalDateTime
import java.time.ZoneOffset
class AndroidEpochDateProvider : EpochDateProvider {
override fun currentDateTimeInSecondsUTC(): Long {
return LocalDateTime.now(ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC)
}
}
Native iOS Implementation:
On the iOS side, we also simply implement the interface in Swift.
You’ll notice that the function definition in Swift has changed slightly. For example, instead of returning a Long
, the return type is Int64
. There are several type differences when converting between Swift/Objective-C and Kotlin code that I’ll need to discuss when deep-diving into this technique.
1
2
3
4
5
6
7
import SharedFramework
final class DarwinEpochDateProvider: EpochDateProvider {
func currentDateTimeInSecondsUTC() -> Int64 {
return Int64(Date().timeIntervalSince1970)
}
}
Regardless of our platform (Android or iOS), we still get compile-time safety in all of our code because we’re implementing an interface.
What are the differences between this and using expect/actual
?
From a code perspective, there isn’t much of a difference. With this technique, the implementation naturally needs to conform to the interface. If you omit the implementation, the compiler will still notify you to implement it, albeit in a different manner than expect/actual
.
For example…
A repository retrieves flight data for a specific date in your shared framework. To create this repository, you need to pass in an EpochDateProvider
so it knows what the current date and time are:
1
2
3
4
class NetworkFlightStatusRepository(
private val dateProvider: EpochDateProvider,
...
) : FlightStatusRepository { ... }
Therefore, in your Swift code, when you new up an instance of the NetworkFlightStatusRepository
object, an instance of the EpochDateProvider
dependency must be passed in, and the compiler will complain until this is done. This gives us compile-time safety at the platform level even without expect/actual
:
Uh oh! This sounds like dependency injection! But… isn’t that hard?
If you’re unaccustomed to it, dependency injection might initially be stifling. Once you get some practice using it (and experience the benefits while testing!), it should become natural to use.
Hopefully, it’s clear from this example that there are ancillary topics, such as dependency injection, that are key to understanding how to make the most of this approach. I know that a proper discussion of this wouldn’t be complete without explaining these other topics, and to keep this post concise, I’ll cover these later in the series.
Step #3: Writing Platform-Independent Code
KMM Shared Framework Code
Platform-independent code is written inside the KMM Shared Framework 100% in Kotlin and utilized by Android and iOS applications via the shared framework.
Kotlin Implementation For Shared Framework Code
1
2
3
4
5
6
7
8
9
10
11
import com.soywiz.klock.DateTime
interface EpochDateProvider {
fun currentDateTimeInSecondsUTC(): Long
}
class DefaultEpochDateProvider : EpochDateProvider {
override fun currentDateTimeInSecondsUTC(): Long {
return DateTime.nowUnixLong()
}
}
Given that this code is written entirely in the shared framework, it can be used by other classes within the shared framework as well as the Android or iOS code.
Those readers with a keen eye likely noticed the import com.soywiz.klock.DateTime
on line #1. Remember how I mentioned that there are some examples of code that appear platform-independent but are actually platform-dependent? Date and times fall under this category. Anyone who has worked with dates and times likely has felt the pain associated with them.
As such, at this time, KMM does not include support for dates and times out of the box, and there are a couple of open-source libraries available for multi-platform development, including Klock and kotlinx-datetime.
Given these different approaches, you’re probably wondering:
- How do I determine what code is platform-dependent or independent?
- What are the trade-offs of each approach?
- When should I use one over the other?
- What are some practical code examples of how to use each one?
These will be the main points that I’ll dive into for each one of these approaches next. If you’re considering using KMM in a project and have additional concerns, please let me know so I can include those as well!