URLSession
on iOS). If your ktor-based networking stack and your application’s “legacy” stack are making connections to the same hosts, you may end up with kept-alive connections to the same hosts in the connection pools managed by each stack (doubling your connections, depending on your platform).Depending on your project, these issues may or may not matter to you, but with these ideas in mind, we (my team at Autodesk) took a stack-agnostic approach to multiplatform networking. In our multiplatform layer, we have an interface
called Network
, and it allows us to make network calls to our backend API. Here’s a look at the interface
itself. It started as this core method and a collection of supporting classes, though it has grown over time:
Starting at the top, we have interface Network
, which defines a method for making a request to one of our API hosts, which are represented by an enum with cases for each of our internal API hosts (not shown here—each case maps to a host URI, which can change based on staging/production environment). The non-host parts of the request are represented by the Request
class, and then the completion lambda is called with the response, when the request is finished. Making this call returns a handle to the request in the form of NetworkDisposable
(definition omitted), which is an interface
with a single method that can be called to cancel the request. Below that, you can see a sampling of the request and response classes. I left some definitions out for brevity, but hopefully you get the idea: a simple collection of classes to represent a basic HTTP request and response.
With these, you have each client application, who wants to use functionality of the multiplatform layer requiring network access, provide an implementation of Network
. So instead of ktor, you just bring along a class that implements this interface
and negotiates calls to your existing networking stack. In the PlanGrid app, we got started with this interface about two years ago, and it continues to serve us well, though we have added to it over time to support more use cases (e.g. downloading files). Here is a short list of things I like about it, which we take advantage of in our code:
Network
, it’s easy to write Network
fakes in our tests that work across platforms.Network
(using this) that allows internal code to use Network
with coroutines, while on the outside (back on your native iOS/Android/Windows platform), all clients get an easy-to-support lambda-based API.Network
as this seam we have throughout our code, we can one day decide the time is right to swap out all of our platform-specific Network
implementations for a single one based on ktor.Like I said earlier, ktor may be the right choice for your project, but our choice to take this interface Network
approach allowed us to get going on building the meat of our multiplatform business logic, fast. We continue to reap its benefits years later.
At Autodesk, engineers aren’t always working alongside a Kotlin framework that was built from local sources. Often, many of us are testing our latest Swift code, which links against the pre-built binary version of the framework that was built by CI and downloaded and installed from a remote location via CocoaPods (podspec checked into a private specs repo). We do this for a few reasons:
If you follow all of the advice from Kevin and Touchlab’s available documentation and use their plugins, then you should find yourself with a working setup that allows you to debug Kotlin built from source on your machine. This is exciting! You now have a basic working “dev setup.”
Now, let’s say you’re working with the pre-built version of your Kotlin framework. All of a sudden, you find a bug! You do some initial debugging, and you come to the conclusion that, yes, this bug is in the Kotlin code somewhere. To get this sorted out, you will need to get your new “dev setup” back into place:
Now that it has been about 30 minutes, you resume debugging, fix the bug, rebuild to verify it, (answer more Slack messages in between), verify the fix, and then PR and push a patch. Now that you’re done, you can go back to what you were working on before when you encountered the bug, which was what again? Oh shoot it’s the end of the day— try again tomorrow.
If this sounds miserable, you are right! Slack relationship issues aside, there is a better way. You can debug a binary pre-built from CI. The whole reason we have to debug using Kotlin that was built from local sources in the first place is because dSYM bundles (which enable Xcode to map binary function addresses to your sources and therefore enable your breakpoints) contain references to your sources that contain absolute paths from the machine that built the framework and dSYM. You can set all of the break points you want, but Xcode won’t be able to figure out that /var/lib/jenkins/YourLibrary/src/commonMain/kotlin/MyClass.kt
referenced in the dSYM built by your CI machine is ~/Code/YourLibrary/src/commonMain/kotlin/MyClass.kt
on your local machine.
The good news though is that there is a way to make Xcode and LLDB understand just that. Apple has a page here that explains that you can embed a plist file in your dSYM bundle that tells LLDB (run by Xcode) how to map absolute paths in your dSYM to those on your machine. Here’s their example plist:
I won’t spend too much time in here, but we can eyeball the plist and see that there’s a key called DBGBuildSourcePath
and DBGSourcePath
, which together create the mapping we want (from CI sources to local sources). If you read through the Apple page a bit, you’ll find that once such plist per architecture included in your binary is needed to make this work. Each file is named <UUID>.plist
with the UUID that identifies the architecture slice in your binary (dwarfdump --uuid <path_to_framework_binary>
to see these).
Stepping back a bit, our goal is to be able to debug our pre-built Kotlin framework from Xcode, which we can’t do (without making some changes) because the dSYM we have with our framework contains absolute paths on a machine (the CI machine that built the framework) that isn’t our local machine. To make this work we need to:
When I was first digging into this problem, I came to realize that any of this was solvable at all by stumbling upon Max Raskin’s post on the subject. Max (who now happens to be one of my colleagues) wrote this post addressing the same issue but for a C++ library. As it turns out, this problem has nothing to do with Kotlin. It’s a general problem in this (Apple?) ecosystem for binary libraries. Here’s a bug discussing these plists in the Swift bug tracker, for example (another non-Kotlin context). In Max’s case, it’s used to help debug a binary C++ library. In our case, we’re using it to debug a Kotlin binary framework.
If you read Max’s article, you’ll see that Max put together a nice python script that, when given paths to your framework, dSYM, and sources, will generate and output the plists you need, and put them in the right place. Max was also kind enough to include a reference to my ruby port of the same script. On my team, everyone has a dependable ruby setup for CocoaPods, but that’s not always the case for their python setups. So, I made this port.
To use either script and get this working:
If all goes well, Xcode should be able to stop on your Kotlin breakpoints, even though the binary was built on your CI machine. At Autodesk, we have this ruby script run in a post_install
hook during pod install
, if you set an environment variable that contains the path to your local checkout of the Kotlin sources. Then, all you need to do when you want to debug is create that folder reference in Xcode. This saves us a lot of time and hassle, if we encounter a bug in our Kotlin while testing our applications. We no longer need to stop to reconfigure our setup to build from source, just to debug our Kotlin.
Give the ruby script a try, and leave a comment in the gist, if you run into issues.
Happy Kotlin debugging!
]]>Most new feature development for the PlanGrid app starts in the shared library now. It has become so much an extension of our main application that we’re planning a move to a mono-repo (iOS, Android, Windows, and the KMP shared library all in one place) later this year, which will help solve some of our scale issues (great problem to have all things considered).
Looking back, there were a few things that made Kotlin work well for us that have to do with a combination of developer experience and our team. Early on, the few of us working on the proof-of-concept and pitching KMP to the rest of the team focused on developer experience. We ensured that integrating our experiment wouldn’t get in the way of day-to-day work by distributing it to iOS as a binary framework via CocoaPods (already integrate other binary frameworks this way without issue).
While testing out KMP-based functionality alongside all of the other work going on, we made use of feature flags in case shit hit the fan in production. Once we had our foot in the door, had proved things worked well, and had come up with a plan for how the library was going to evolve, the next step was getting people to adopt the shared library for their teams’ features.
This again takes us back to developer experience. Lucky for us, KMP comes with iOS interop out-of-the-box. It’s so good that one of my colleagues thought they were using some kind of bridging layer that we must have written to make Kotlin feel Swift-y. When they command-clicked through to source, they were surprised to find the KMP-generated shared library header.
Did I mention we have a Windows team? For that side of things, we got lucky. The level of support from KMP you get for Windows does not measure up to what you get for iOS. On the Windows side, my colleagues would like to have a C# library in the style of the KMP-generated Obj-C library with the nice, generated headers that we get on iOS. Instead, they get a library that uses C-interop— quite a different experience. Fortunate for us, a few folks on our Windows team were experienced and interested enough to write and maintain their own C# code generation on top of that. We hope to open source it later this year.
To recap, these elements made it possible for KMP to work well for us:
If I could only pick one, it would be the interop on iOS that allowed this to become as successful as it has for us. It would be a much tougher sell, for example, had both the iOS and Windows teams needed to spend loads of effort on interop.
My favorite example of this great interop is one from my earlier post, where Phill and I wrote about the iOS interop:
This example highlights some idiomatic Kotlin that allows you to write idiomatic-looking Swift. It’s impressive. However, this example has a secret. It also highlights many of the areas where Kotlin/Native interop with iOS has room for improvement. With Kotlin 1.4.0 out the door, I hope now is a good time to raise these issues. Fixing them would take the sell to iOS teams to the next level, at least in terms of having excellent interop. I’ll discuss the issues in increasing in order of how long it took our team to notice and bump into them.
This is one of the earliest areas, for which I filed an issue in the Kotlin/Native GitHub. Enums are a great way to describe an input for an API, such as the above log method. However, we ran into problems with Kotlin enums as soon as we wanted to do an exhaustive switch
(equivalent to exhaustive when
in Kotlin) over them in Swift.
The problem lies in how enums are represented in Kotlin compared to Obj-C (remember Swift is irrelevant for comparison— interop is via Swift-y feeling Obj-C). Enums in Kotlin are reference types (a special class
). In Obj-C, they’re integers. Attempting to switch
over them from Swift would be like trying to switch
over any other class instance that you defined in Swift, for example. The Swift compiler doesn’t know that there happens to be a finite number of instances of that enum class
like Kotlin does.
There is a workaround though that can improve the ergonomics a bit. You can define a matching C-style enum (with a matching name), and redefine the ordinal property as having that type. At Autodesk, we do this with a bit of fragile (albeit has worked in our codebase with only one minor tweak since its creation) code generation that adds these C-style enums to the Obj-C framework header as Obj-C extensions on all of our enum types. With that, we get exhaustive switch
for all of our Kotlin enums, by doing a switch
over the ordinal property.
In the above sample code, you can see there’s no problem passing enums around. You wouldn’t have any idea about how Kotlin enums are represented, until you go to switch
on one.
In my first article, I mentioned how, as an iOS developer, I found the companion object
a bit strange at first, but I get it now. As my iOS colleagues have become better acquainted with them, we have begun to see them more often in our shared library. The problem comes if you don’t know that you can name a companion object
, like in the above example. Unnamed ones are more common, in my experience. Here is what the Swift code would look like, if we hadn’t named the above companion object
“default”:
As someone who writes a lot of Swift, this looks funny at first. Are we creating a new Logger.Companion
? If so, where can I see what this does in Kotlin? The ergonomics of the unnamed companion object
is another issue that I filed early on. To answer the question, you aren’t creating a new Logger.Companion
.
The fix here isn’t straightforward. You can convince your team to prefer naming a companion object
, but that doesn’t always make sense, if say your goal is to use a companion object
to namespace a public constant. Solutions discussed in the ticket would be breaking changes. That said, I think improving the ergonomics here would be an easy way to prevent less enthusiastic iOS developers from having this easy (and small) thing to point at. At Autodesk, we just acknowledge this quirk of the Obj-C export and move on. But, that’s easy for us to do now, as we have a critical mass.
Although we bumped into this one later on, a fix for this would make the biggest difference for our team right now because our dependence on KMP has grown. If you notice in the above Kotlin, the log function has a default argument. However, you cannot use that default argument from Swift. As a log parameter, this might not be that important. In other contexts though, this could be a default integer parameter, for example. In that case, it can be near impossible to know what argument you should pass in the “default” case. To avoid bugs, your best bet is to go back to the Kotlin source and find the answer.
Though, I’ve heard folks say: but Swift supports default arguments, why shouldn’t this work? This is often the first place folks begin to internalize that this doesn’t matter. The interop is via Obj-C, so all that matters is what Obj-C supports. You can repeat this same answer for a bevy of similar complaints. Interface
extensions? Swift supports the equivalent protocol
extensions. Obj-C does not. Optional primitive types? Obj-C doesn’t have those either. None of this is a knock on Kotlin/Native’s interop, which again is great! Obj-C interop was the correct and stable choice at the time, as that was before Swift had a stable ABI. I made comments about this in my previous interop article, so let’s go back to the issue at hand.
Default arguments have a solution in Kotlin/JVM interop, which is used in hand-written Obj-C as well: generated overloads. Generating overloads for C and Obj-C KMP libraries would be a huge improvement to Kotlin/Native’s export facilities. I hope with Kotlin 1.4.0 out the door, time can be made for this one.
I want to go back to enums for a moment. I mentioned that I would review the issues in the order that my team ran into them. After getting more comfortable using Kotlin enums in our code, we began to come up with cases where we wanted to enumerate Kotlin enums. However, the needed values()
function is not exported to Obj-C by default. If you need this, you’re left to define it yourself in your library and have it call the equivalent function. The workaround is fine, but it gets in the way, when the library you’re using is one that’s already packaged up and distributed. A fix requires another PR and waiting on another CI deploy.
Enum<T>.values()
As of Kotlin 1.4.30, your Kotlin enums now export a values()
class
method, which maps directly to the Enum<T>.values()
Kotlin method. One thing to note though is that this direct mapping exposes the unfortunate (for Obj-C/Swift) Kotlin.Array
. If you’re using Kotlin/Native, you may know that Kotlin.List
is the type that Kotlin/Native exposes as a native NSArray
/Swift.Array
. With Kotlin.Array
, you end up with a special KotlinArray
type, which exposes an iterator. To make of use of this in your Swift code, you’ll want to write something like this:
This will let you bridge KotlinArray
to Swift.Array
like so: Array(MyEnum.values())
. Not bad! I also tried adding some kind of Swift extension to enable bridging like MyEnum.values().toArray()
, but I ran into Obj-C to Swift generics compatibility issues. If you find a way to make that work though, please let me know on mastodon!
The final one I want to talk about is the most minor, but we do come across it on occasion. In the above example, we are hiding the fact that the completion closure has one parameter. It’s a Kotlin boolean
, but it is translated to Obj-C as a KotlinBoolean
class
. This is bound to happen if the type is optional. Again, Obj-C doesn’t support optional primitives. In this case however, it’s not. It should be a BOOL
instead, which will translate to a Swift
Bool
.
In most cases, Kotlin/Native does the right thing, but there are occasions like this one that come across a bit clunky. I admit though, I don’t have the expertise to understand why this happened in this case. Again, the interop you get for iOS is great. Fixing polish-type issues like this would take the interop to the next level.
Okay I lied. There’s one more quirk, but I felt that it didn’t fit as well with some of the above issues. It is also maybe a bit niche, and it depends on how you architect your code, as to whether or not you’ll bump into this one. In our codebase, we create an interface
for each of our repository classes. Let’s say you have a repository like this one:
Over time, other people on my team will also add repositories with fetchById
methods with the same name. Why not? fetchById
is a reasonable way to name that method. However, every time you add such a method, methods with the same name generated for Obj-C will get an underscore added to the end to disambiguate them from others. But why you ask? Again, the explanation has to do with Kotlin vs. Obj-C. Read this issue for the full details. At the time of this writing, one of our fetchById
methods is up to seven underscores in Obj-C 😂.
If you have a look at the GitHub issue, there doesn’t appear to be an easy solution from the Kotlin/Native side. What we’ve started doing is just naming our methods better. In this case, renaming the method to fetchFishById
should prevent this from happening (until we add another fishy repository). That said, it would be great if Kotlin/Native could emit a warning about such issues. Then, we can catch these during development before the Obj-C export happens, and a random API gets an extra underscore (making the overall change breaking). Kotlin 1.4.0 has new native-specific frontend checkers. A new one that helps us out here would be most welcome.
Kotlin/Native’s interop for iOS is great, and it has allowed us to scale a shared codebase with little effort on the iOS side. That’s a huge deal, when you consider that most of the mobile engineers that work on the PlanGrid app had to learn a new language for this to work. Again, I hope folks (including those at JB) don’t read this as a knock on Kotlin/Native. Now that Kotlin 1.4.0 is out the door, I hope these issues can be addressed to take Kotlin/Native’s interop to the next level 🚀.
For those evaluating Kotlin/Native, of course you’ll want to know what issues lie ahead as you scale up. This lays out all of the source-level issues that we’ve run into. Despite these issues, it has been well worth it.
]]>Fundamental to getting this working is understanding what the various Gradle tasks do, which make your multiplatform build work. It’s also important to dig into how Gradle tasks work in general, so you can better understand how to debug Gradle build issues, when they arise — a useful skill upgrading to a new version of the Kotlin Gradle plugin.
On top of that, I think you’ll also start to understand how powerful Gradle is and begin to see how you can use it to automate all sorts of other tasks that are part of your build. And while you can get away with not diving into details about how Gradle tasks work for awhile, I promise that if you spend a bit of time on some of the recommended reading below, you’ll wish you hadn’t put it off.
For Kotlin multiplatform, the build works via Gradle and the Kotlin multiplatform Gradle plugin. For iOS developers coming to Gradle for the first time, Gradle is like xcodebuild
. But, instead of lots of build variables and a project file that defines your targets and configuration, in Gradle it’s all defined in code. I found this a bit confusing and unfamiliar at first, but now that I understand it, I find that I enjoy Gradle’s build tooling far more than what I get with Xcode.
The best advice I can give about learning Gradle is to read some of their documentation. Early on, my coworkers and I found we were able to make the most progress on debugging build issues after we had done some reading on gradle.org and kotlinlang.org to understand some core Gradle and Kotlin multiplatform plugin concepts. To help with that, I’ve made a recommend reading list:
In your project, Gradle tasks are what make it go. They run your tests, build your debug builds, and it’s what your CI will run to verify your build. Kotlin multiplatform projects turn the Gradle task complexity up to 11. So again, if you can spare the time, do as much of the recommended reading above as you can stand. From this point on, I’ll assume some base knowledge of tasks.
Something I often encounter on my team is folks, that are newer to multiplatform, running the “wrong” Gradle tasks. For example, someone will come to me with a build issue, and I’ll find that this whole time they’ve been running the build
, check
, or assemble
tasks as part of their development workflow. On a project with fewer targets (i.e. only a JVM target), this is fine. In a multiplatform project, this is going to run the build and/or test tasks for all of the targets in your project that are supported by your machine. That can take a lot of time.
Instead, find a target that works for you — maybe one that’s for your preferred development platform. Then, let CI handle the final check
across all of your targets. To see what any of these does on your machine, run (for example for check
) ./gradlew check --dry-run
. You’ll get a list of all of the tasks that check
would invoke, and you can pick one from the list that makes sense for you. Read on for more info on decoding some of these task names.
Alright so at this point, I’m going to assume you’ve done some of the recommended reading above. You understand what tasks are, how you use them to build your project, and that there are a lot of them in a Kotlin multiplatform project. If you don’t have a project setup, you can clone my bootstrap project and run ./gradlew tasks
to see for yourself.
When getting started on CI work, I like to start by listing the high level steps that need to happen during the build. For PRs, there are two things we need to do:
If those two things run without error, the build is passing. With multiplatform, this becomes complicated by the number of platforms you support. This might mean setting up some kind of build matrix-type setup where these steps run on multiple operating systems for each platform.
In many cases, it’s easy enough to just run build
. From the docs:
Intended to build everything, including running all tests, producing the production artifacts and generating documentation…
If you’re in a hurry, build
will get the job done, and for small projects, this is enough.
build
However, be aware that in cases where you’re running your build on multiple operating systems, the build
task on its own can be wasteful. Let’s say your multiplatform project has a JVM target. Well, if your macOS and linux environments both support JVM, then running build
in all environments during CI is going to run your JVM build twice. If you don’t need that, consider running the macosTest
and jvmTest
tasks explicitly in separate environments to reduce your build times. Again, use --dry-run
to dig into what your build
actually does, so you can figure out a separation of environment-specific tasks that works for you.
Figuring out the right tasks to run in the right environments takes some tinkering and opinions on what you want your build to validate. This is true for both CI and local development. There are a lot of tasks to choose from, but you can boil them down to just a few “task templates,” if you will, once you understand how they work.
First, there are two variables to consider: configuration (sometimes called variant I think?) and target. Configuration can be either “Debug” or “Release”. Target is the name of one of your targets (e.g. JVM). These variables are mixed with verbs to create the task names that make up your multiplatform project. For example, <target>Test
is the format of all of the test tasks, so if you know the names of the targets in your project, you now also know the names of all of the test tasks. With that in mind, here is a bit of a cheat sheet that maps out the kinds of tasks you can run (follow along by running ./gradlew tasks
in your project):
<target>Test
compileKotlin<Target>
link<Configuration>Framework<target>
(replace Framework
with other binary types)With that, you should be able to figure out which Gradle tasks make sense for your build. Be warned, the Android Gradle Plugin (if that’s part of your project) adds loads more tasks that look kind of like these, but they may not always fit into the above formulas.
When you’re sharing your multiplatform build with an iOS team, you may end up coming across this “fat” framework concept. In KotlinMobileBootstrap, I have a special task that I created that inherits from FatFrameworkTask. Back over in Xcode-land, we’re expecting a framework that will work with our build on both iOS simulator and device. Those link tasks I mentioned above will only produce frameworks for one or the other. To get a framework that works for both, which is called a “fat” or “universal” framework, we have the debugFatFramework
task (another could also be created for the release configuration).
You can tell which kind of framework you have and which architectures are embedded by running the file
command on the binary in the framework. For example, running ./gradlew linkDebugFrameworkIosX64
in and then file ./build/bin/iosX64/debugFramework/KotlinMobileBootstrap.framework/KotlinMobileBootstrap
(on the output) yields
./build/bin/iosX64/debugFramework/KotlinMobileBootstrap.framework/KotlinMobileBootstrap: Mach-O 64-bit dynamically linked shared library x86_64
You can see that it only has the slice for the iOS simulator. But, if we do ./gradlew debugFatFramework
and then file build/fat-framework/debug/KotlinMobileBootstrap.framework/KotlinMobileBootstrap
, you’ll see that it has the slices for both iOS simulator and device.
Okay so getting back to CI, this fat framework thing is something we want to think about. If your team is expecting a working fat framework, it’s a good idea to run these tasks during your build to add to what “passing” means for you and your team.
The hardest part of getting all of this working is that there is no one best way to setup your build tooling to accomplish the goals of CI and a local development workflow. You may not find the answer by doing a quick web search. Instead, you have to figure out how best to use the tools you have for the different parts of the build that are a part of your team’s build setup.
At Autodesk, we use a homegrown CocoaPods setup that works for local development and distribution. We distribute our library as a private pod, which is configured to present the multiplatform framework as a vendored framework. I may do another post all about that. However, I hope you can now use some of the information here about Gradle tasks to produce a framework that will work as a CocoaPods vendored framework, if that’s something you’re interested in. For the local setup, the Kotlin/Native CocoaPods plugin is worth a look. Happy building!
]]>After you get a feel for the language and do some Koans, the next step in your journey to writing Kotlin for iOS is understanding what that Kotlin is going to look like from the Swift in your iOS app. The way Kotlin reverse interop (Swift talking to Kotlin) works is via Objective-C. For some, discovering that they get an Obj-C header from their Kotlin library, instead of a Swift one, is disappointing. That’s an understandable reaction. In a all (or majority) Swift code base, you and your team may have spent a lot of time building out your project using all that Swift has to offer — even the stuff that’s not compatible with Obj-C.
The first thing to understand here is that Kotlin/Native — the member of the Kotlin multiplatform family responsible for this part — predates the Swift language features (Swift ABI, and module ABI, stability) that would make Swift-only reverse interop for Kotlin possible. We know, and JetBrains knows, that Apple has (some) Swift-only system frameworks now, and Apple’s ecosystem is moving in that direction. If JetBrains wants developers to build iOS (and Android) apps in Android Studio this year, I think we can expect Swift reverse interop in the future 🙏.
For now though, what you get when you build a Kotlin library into an Obj-C framework is one with a header that is well-annotated for Swift. Phill and others on our team using our Kotlin-based framework for the first time didn’t realize — until they looked under the hood — that the classes they were interacting with were Obj-C. And for what it’s worth, this is the status quo with the majority of Apple’s system frameworks that you interact with in Swift right now. You get an Obj-C framework that is annotated to feel “Swifty”.
The next step to writing your own Kotlin library is understanding visibility. Like Swift, Kotlin has visibility modifiers for top-level declarations: public
, internal
, and private
. By default, everything is public
😱. It’s therefore good to get into the habit of marking your classes as internal
until you’re sure you want your API to be used by your downstream application. There’s no cost to keeping an API hidden. But, I assure you that your colleagues in other timezones (👋 Tel Aviv) will not be happy when they wake up, pull, and find breaking changes because you changed your mind about an API that you didn’t mark as internal
😇.
For reasons like that, JetBrains recommends in their Coding Conventions that library authors “always explicitly specify member visibility.” Be on the lookout in a future Kotlin release for “API mode”, which will allow you to run the compiler in a mode that generates warnings when visibility isn’t explicit 🙌.
A good starting place to understand what you’re going to get when you export your library to an Obj-C framework is this one-pager on Kotlin and Obj-C/Swift interop. We’re starting from a good place: List<String>
in Kotlin gets you NSArray<NSString>
in Obj-C and [String]
in Swift, for example. class
in Kotlin is class
in Swift. interface
is protocol
. If you don’t read the whole guide, scan over the table at the top to get a quick understanding of what you can expect to get when exporting your Kotlin.
To make the interop work, Kotlin/Native generates some base-layer (for lack of a better term) code in your framework to make this translation work well between Swift and Kotlin. Let’s take a look at KotlinMobileBootstrap’s framework output. Clone the repository, and run ./gradlew debugFatFramework
. If all goes well, you should end up with a framework at build/fat-framework/debug/KotlinMobileBootstrap.framework
(relative to the repository root). If you then crack open the framework’s header, you’ll see a healthy list of base classes. All of your types will inherit from these, and you may see more depending on what all you export in your framework (e.g. an added base class for Kotlin’s enum type). Here are some of the types in KotlinMobileBootstrap:
@interface KMBBase : NSObject
@interface KMBMutableSet<ObjectType> : NSMutableSet<ObjectType>
@interface KMBMutableDictionary<KeyType, ObjectType> : NSMutableDictionary<KeyType, ObjectType>
@interface KMBNumber : NSNumber
@interface KMBBoolean : KMBNumber
You won’t find these in your actual Kotlin code. They’re generated when creating the Obj-C framework. The overarching reason we need all of this is to ensure smooth interop between the Swift/Obj-C world and the Kotlin one. To pick an example, one of Kotlin/Native’s promises is Kotlin working in your application without the help of the JVM, yet Kotlin/Native has to have working automatic memory management. Among other things, KMBBase
allows the memory management environments of Kotlin/Native and Swift to work together without help from me and you. I won’t get into how their automated memory management works in detail (could be a post in itself if I had the expertise), but it’s one of the concerns Alec Strong addresses in our talk from KotlinConf 2019.
You also might notice lots of __attribute__((swift_name("SwiftyNameHere")))
sprinkled throughout the header. If you haven’t worked with making Obj-C APIs feel “Swifty” before, this is the mechanism for making a Obj-C-sounding declarations look and feel “Swifty” when interacting with them in Swift. Obj-C will see the classes and methods as is, and Swift will see the string inside the swift_name
part.
Now that we have some of those basics covered, it’s time to write some Kotlin. Phill is going to walk through how some of the basic Kotlin constructs come out on the other end and appear to Swift.
From here on out, you can try out the below samples from this sample project.
One of the most common features of the Kotlin language you’ll work with is a class, which for the most part works exactly as you would expect it to in Swift and Obj-C. Define a class
(or a data class
) Sample
in Kotlin and a corresponding class will be defined in Obj-C.
Notice that generated Obj-C classes inherit from the KotlinIos2Base
super class, which itself inherits from NSObject
. Generated classes are prefixed with a prefix that is derived from the framework name, in this case “KotlinIos2”. Attributes are used to ensure this prefix is omitted from Swift, and that methods and initializers look and behave natively to Swift language conventions.
Since classes in Kotlin are final by default, their native counterpart is annotated to restrict subclassing with the __attribute__((objc_subclassing_restricted))
attribute. By specifying open
on your Kotlin class, the generated Obj-C class will also support subclassing.
Conforming your class to an interface in Kotlin defines the interface as a protocol and adds conformance in the public header of the generated Obj-C class.
Using a primary constructor in Kotlin to define a set of parameters directly after the class name will generate a designated initializer in Obj-C. It will also generate @property
members on the public interface of the class, marked with readonly
if defined as a val
, or not if the property is a var
.
Initializers and functions will concatenate parameter names into an Obj-C-friendly camel case method name, such as initWithStr1:str2:
and provide a more concise Swift-friendly function name annotation.
Coming from the world of Swift, you’re probably used to defining structs but you’ll quickly notice that the Kotlin language doesn’t have value types. Instead, the next best thing is a data class
, which is simply a class that allows the compiler to derive some out-of-the-box members such as equals()
, hashCode()
, toString()
and copy()
from the properties defined on the object.
If you inspect the Obj-C output of a Kotlin data class, you’ll see that the compiler has mapped these derived methods into their Obj-C equivalents on NSObject
such as isEqual
, hash
and description
.
Enums are actually a special type of class in Kotlin, but they work in much the same way as they do in Swift. When you define an enum in Kotlin and peek at the Obj-C generated header, you may not see what you thought you’d see.
Since enums are just classes in Kotlin, the compiler generates an Obj-C class with each enum case defined as a readonly
class property on the type. This generated class inherits from a KotlinIos2KotlinEnum
superclass, which is another generated class that utilizes Obj-C lightweight generics to provide base-layer enum functionality including case comparison, equality and initialization.
Functions defined in Kotlin along with their named arguments, default arguments, and return types map to Obj-C and Swift almost seamlessly. As an API consumer, interacting with these methods feels exactly the same as if they were originally defined in Swift. Attributes maintain a Swift-y method signature, parameter types are mapped to native types and even Lambda’s defined in Kotlin are generated as closures with the same parameters and return types.
Since Obj-C doesn’t support default arguments, these are unfortunately not mapped through to Swift. If you’re familiar with static and class methods in Swift, Kotlin’s companion objects provide similar functionality. Companion objects defined in Kotlin will actually generate separate Obj-C classes but use the swift_name
annotation to maintain a consistent API interface, so while the generated Obj-C looks different you can interact with the method in the same way you would a static method.
Kotlin’s Kotlin/Native backend does a good job of setting you up with the basics you need to integrate Kotlin on iOS. There are some rough edges, and I’ll dig into some of those in a future post. For now, I wanted to focus on the basics of what you can expect from Kotlin/Native, so that you can get started. And with that, my next and final post in this “getting started” series will focus on the build and integrating your Kotlin-based library, which I think is one of the more intimidating parts of making Kotlin a part of your team’s workflow.
]]>We started exploring how to share code between our native mobile platforms at the end of 2018, and we wrote a bit about that in a recent post here. After a year of learning how all of the various parts of Kotlin multiplatform fit together to make that project work, I felt motivated to make this blog to write about it.
To get started, I’m going to write a bit about Kotlin and Kotlin multiplatform from the perspective of someone (me) who came at this from iOS development. I’ll start with Kotlin and work my way through multiplatform.
If I were starting an app from scratch today, it’s hard to say what language I would write it in. Swift is an impressive language with lots of useful modern features. But, if I also want to write this app for Android and am limited in the number of engineers (just me in this scenario), why would I set myself up to write the same app in two languages? JetBrains announced at KotlinConf 2019 that you’ll be able to build and iterate on an iOS and Android app simultaneously in Kotlin using Android Studio. If nothing else, it highlights what you can build on a foundation of great tooling. In this case, that foundation is Kotlin multiplatform.
A year ago, I hadn’t written any Kotlin. When Kotlin multiplatform became a possibility for us, I got my start with Kotlin Koans. This got me enough Kotlin knowledge to where I could write working code and test things out with iOS. Let’s walk through a quick comparative example between Kotlin and Swift.
Let’s say I want to represent a dog. In Swift, you might represent it with a struct
like this:
In Kotlin, the tool we reach for here is a data class
, which looks like this:
In one line, we’ve explicitly defined the type, its properties, and its constructor. With a struct
, you get this too (though such a one-liner is discouraged in Swift), but the constructor is implicit (generated by the compiler). With a data class
, equals
and hashCode
are generated for you as well. To get this with a struct
, you make sure to declare it as Equatable
(also gets you Hashable
).
Let’s expand on this with a protocol
:
In Kotlin, this is an interface
:
Let’s implement it. First, in Swift:
And in Kotlin:
In Kotlin, we’re still at one line. I don’t know how others feel about ability to do this in one line, but as I’ve been writing more Kotlin, I’ve found it to be practical and concise. It’s one of the “Kotlin-y” things I’ve come to enjoy most, and I’ve found more and more of these “little things” as I’ve explored the language.
At this point, you may have noticed some similarities between the two languages, and there are many that you can lean on to help get started writing some Kotlin:
val
/var
and let
/var
for declaring variablesinterface
and protocol
for defining methods and properties that a type must implementfun foo(arg: String)
and func foo(arg: String)
for defining a function called “foo”class
and class
Coming from Swift, there were also tools I wanted to reach for when writing Kotlin that didn’t have such an exact match in Kotlin. For me, these all fell into the category of working with statics. The first thing to know is that there is no static
keyword like in Swift. Think about all of the places you might use static
in Swift. How do we do those things in Kotlin?
In Kotlin, singletons are declared using the object
keyword, and all members declared within are accessed on the type directly.
In this example, I’ve declared an object
called Platform
. This acts as a singleton, and we access its sole property via Platform.name
and call its sole function via Platform.printName()
.
In Swift, we’re used to using static func
and static let
for type-scoped members. In Kotlin, we do this by extending the object
concept and declaring one within the class
using the companion
🤝 keyword.
When I first saw this, my mind went to the C++ friend
keyword 🙈, but all it does is provide us an area to define static
-level members for the Dog
class. In this case, we can now access Dog.isGood
, which will always return true
🐶.
One thing to note about object
s when working with them in multiplatform contexts is that they behave in slightly different ways on native platforms like iOS (i.e. built using Kotlin/Native) compared to the JVM. On native platforms, globals (including object
s) are initialized during Kotlin/Native runtime initialization, which is typically when you launch your application. On the JVM, they’re initialized on access (lazily).
Their behavior here turns out to be the same 🎉. I made a mistake understanding the concurrency rules and reading too much into this line in the immutability docs:
Top level/global variables of non-primitive types are by default accessible in the main thread (i.e., the thread which initialized Kotlin/Native runtime first) only.
I’ll discuss this a bit more in a future post.
I hope that if you’re coming from Swift, this gives you a sense of Kotlin as you’re getting started. Next, I’ll get into Kotlin multiplatform and how these Kotlin concepts bridge back to Swift/Obj-C.
]]>