When shipping Kotlin Multiplatform to iOS, developers face three critical challenges: (1) Native state observation requires bridging platform-specific events (like push notifications and Remote Config) to shared Compose UI using StateFlow, where Swift callbacks must be converted to Kotlin StateFlow updates to trigger UI recomposition; (2) Non-KMP SDK integration (like Firebase) requires a delegate pattern because Kotlin Native singletons are per-compiled binary, not per-process, causing silent failures when multiple frameworks each have their own singleton instances; (3) Dependency management has evolved from CocoaPods to SwiftPM import in Kotlin 2.4.0, which eliminates Ruby dependencies but is still experimental and requires static linking to avoid linker errors.
Deep Dive
Prerequisite Knowledge
- No data available.
Where to go next
- No data available.
Deep Dive
What Nobody Told Us About Shipping Kotlin to iOS | Suhyeon (Leah) Kim
Added:Guten Tag.
Sorry, my friends told me not to do it, but it was it will brace the crowd and Thank you so much. Thank you.
Hello everyone. My name is C Young Kim and the title of today's talk is what nobody told us about shipping Kotlin to iOS. And by the end, hopefully you'll know.
First a little about me. I've been doing Android for about 7 years and Kotlin multi-platform for the last two. I met Woowa Brothers. We make Baedal Minjok, which is a Korea's largest food delivery app, part of Delivery Hero.
Recently, I moved into a technical advocate role, which means I now spend most of my time running our developer training program, organizing hackathons, and putting together internal tech conferences. Which also means if I want to code at work, I have to give up work-life balance. But my manager is here in the audience, so let's skip the long story. I'm I'm also a community organizer for GDG Korea Android and I flew 19 hours to from Seoul to Munich. So, if I start speaking Korean halfway through, please just nod. I'll get back to you eventually.
Before we start, let me tell you who this talk is for. I'm an Android developer, so when I first opened an iOS project on KMP, I had no idea which parts of the docs actually mattered and which ones I could ignore.
We're not here to walk through the docs.
They're good, but what they don't show is what actually breaks after you ship.
So, that's what this is. What we built in 2 months, what we fought over a year of maintenance, and the things we wish we had known before we got started. So, we've been shipping KMP to iOS for years. Some of this might already be familiar to you, but I think you'll still find the binary level debugging part interesting.
And if you have better ideas, and I know you probably will, please come find me after. I generally love to hear them.
So, here's where we all started.
Deliveroo Hero Brand cares about the internal tech course to train the and hire new developers, and we needed an app to manage those trainees.
Both Android and iOS.
And the team There were three Android developers. We had to ship in two months, no iOS developer, and our backend developer also writes Kotlin.
At that point, though, Universe was basically holding up a sign that said, "Just use KMP." So, yeah, we didn't really choose Compose Multiplatform, it chose us.
And fast forward to today, the app has been in production for about a year. Uh 100% Compose UI, cheers Compose Multiplatform, and across the whole code base, 98.2% Kotlin. The other 1.8% is Swift, half of it written by me in tears, and the other half written by cloud code.
Also probably in tears.
And Compose Multiplatform 1.80 shipped in just about exactly a year ago, and that release promoted iOS to stable.
And the timing worked out for us. On the App Store side, we have never been rejected for using CMP. Kotlin Native is ahead-of-time compiled into native binary, so as far as App Store review is concerned, it goes through the same path as Swift or Objective-C app.
The one thing to keep in mind is privacy manifest for your third-party SDKs, but that's an Apple requirement for any iOS app, not something CMP introduces.
Uh now, the app works well on production, but the interesting part isn't what worked, it's what broke. And everything broke at the same place, the boundary between Swift and Kotlin.
And here's what we are covering today.
Three case studies. The first is native state observation. The pattern we use to bridge native events into shared Compose UI. The second is integrating non-KMP SDKs. Building around Firebase took us something unexpected about how Kotlin native singletons work on iOS.
The third is dependency management.
Going from CocoaPods today to the new Swift PM import in Kotlin 2.40.
Along the way, I'll share an interop people's cheat sheet and what Japanese engineers told me when I brought our architecture to them at last year's KotlinConf.
Now, let's start with the case study one, native state observation. This is the fundamental pattern to bridge platform specific changes to shared Kotlin logic.
Let's start easy. I want you to picture something, just for a second.
A push notification arrives on user's iPhone. They tap it. Your app opens and now your shared Compose UI needs to do something. Show a dialogue, refresh refresh some state. Pretty common case.
So, how would you do it do that in CMP where the Compose UI is 100% shared?
If you're thinking I just write an expect function in common main, the notification doesn't originate in common main. It originates in iOS in a delegate callback on a thread Kotlin doesn't control with a payload that is an NS dictionary, not a map. And that brings us to three typical cases where expect actual fall.
First, native UI events. Things like push notifications. The notification arrives in your iOS app's notification center. Native code receives it, but the logic that handles it is in your shared module.
Second, async platform logic. Things that return through callbacks on whatever thread the platform feels like.
Your common main code just can't await those.
And third, native system APIs that have no KMP equivalent at all.
To make it concrete, these are the real iOS APIs you'd likely hit on any production app. Notification Center, Location Manager, Firebase Remote Config, App Delegate Lifecycle, Bluetooth, Audio, etc. All of these events happen on the iOS side, but the logic that needs to respond to it lives in Kotlin.
Today's first case study uses one of those, Remote Config, to drive a force update flow. So, let's look at that.
Um this was the actual requirement one from production. When the server sets a minimum version via Firebase Remote Config, users on older versions should see a dialogue and be unable to use the app.
The key challenge is that the Remote Config event originates in Swift code, but Compose UI that shows the dialogue lived in the shared Kotlin code.
Here is the module structure of our app.
The iOS app contains ContentView.swift.
Uh Compose app has Android main, common main, and iOS main. And below that, the shared module with domain logic, which shares with server code.
The important thing to notice it that is that our UI is 100% Compose. The entire UI lives in common main. There's no Swift UI mixed in. So, any native event that needs to affect the UI must flow through the Kotlin layer.
And this is the core question. Can events from the iOS module, specifically from ContentView.swift, be received in the Kotlin module?
The answer is yes, but you need a bridge pattern.
We are developers, so let's uh skip the long story and dive into the code.
This is what the default setup looks like in default KMP structure.
A simple main view controller function that returns a compose UI view controller.
Swift calls it, gets a UI view controller, and that was it. And we need to change this.
Here's our solution. We created a shared view controllers object that holds a mutable state flow of state view state.
The main view controller function creates a compose UI view controller that collects from the state flow. When the state state flow updates, compose recomposes automatically.
Take a look at update required app version code, and this is what Swift will call. It updates the state flow, which triggers recomposition, and which shows the force update dialog. State flow serves as a single source of truth for the UI state.
Main view controller is the entry point.
It's the function Swift calls to get UI view controller it can drop into the iOS app.
This is how Kotlin hands a screen across the boundary.
Now, to keep that screen alive alive and reactive, we use collect as state with life cycle, which subscribes to a state flow and turns its emissions into a compose state.
So, whenever the flow emits, the UI recomposes with the latest value, and that's the whole trick. Anything on the native side of force update dialog, a feature flag, a remote config change just needs to push a new value into that flow, and the compose UI recomposes itself.
And for the Swift side, ContentView.swift imports compose app and creates a compose view. It calls main view controller to get the compose powered UI view controller. And it has an update required app version code function that calls back into the Kotlin.
Notice it's calling shared view controllers, the Kotlin singleton exposed to Swift. You might I wonder why do we hold state in a Kotlin object instead of per screen view model?
Because force update it's wide state not a per screen state. For per screen view models, the cleaner pattern is to hold the Kotlin view model inside Swift UI's coordinator. That gives you a proper life cycle and instance isolation. We didn't need that here, but it's worth knowing.
And this is where Firebase remote config gets patched. In the callback, we extract the required app version code, convert it into int 64, not int value.
I'll explain why later in the pitfall section, and then call update required app version code.
That updates the state flow, compose recomposes, force force update dialog appears.
Let me connect the flow visually because we started easily.
Step one, remote config triggers in the iOS layer.
Step two, the data flows into the compose apps iOS main where the state flow gets updated.
Step three, compose UI recomposes based on the new state. Native SDK does its thing, calls a Kotlin function, state flow updates, compose recomposes. The key The key point across all of this work is that we didn't touch app.kt in common main.
We kept the dependency direction intact while simply trickling the exposure of compose UI.
In fact, with this dependency structure, it might have been simpler to implement if we weren't using declarative UI approach.
We know that this could have been done more easier and if we had just implemented a dialog with Swift UI, but we wanted to take the advantage of 100% compose UI.
So, that was case study one. Hopefully, a decent warm up. And because the second case starts as a straightforward non-KMP SDK integration story, but it turns into the deepest rabbit hole we went down on this project.
This is a simple module structure for logging in our app.
The logger module defines a log object in common main with platform specific implementations in Android main and iOS main. The compose app modules base view model just references this log directory.
Now, yes, we could have wrapped it in a proper DI library like Koin, but we wanted to start with the lightest possible structure and just see if it worked.
On the JVM, this works exactly as you'd expected. One log instance shared across modules, accessed wherever you need it.
But the moment we shift this into iOS, something strange started to happen.
Here is the actual code. In common main, we define an expect object log with functions for error, debug, and info logging.
In Android main, we use Android's native log, and in iOS main, we use NS log.
This works fine for basic logging, but we needed to integrate the internal crash reporting tool for a better view.
And this is the timing for the second case study. We needed to implement Firebase Crashlytics. On Android side, this is easy. Just add the Gradle dependency, import it directly in Android main. Done.
But on iOS, the Firebase SDK is Swift and Objective-C. You can't import it into iOS main directly. Kotlin native simply cannot access it.
So, we needed a bridge, and that's exactly why we hit the wall on the next slide. This isn't just about the Firebase problem. You'll hit this with any iOS SDK that doesn't have a KMP support. Analytics, payment payment services, push notification libraries.
The first approach we had was the inversion of control. In the logger module's iOS main, we define a Crashlytics delegate interface. The iOS app provides the actual implementation using the real Firebase Crashlytics iOS SDK.
The log on module doesn't know about Firebase. It only knows about the interface.
It's a clean dependency inversion.
And let's just skip the last slide again and dive into the code.
Step one, we define a simple interface Firebase Crashlytics delegate in our logger module's iOS main.
And the iOS log object holds a nullable reference to this delegate with an init Crashlytics delegate function to receive it.
I know a nullable delegate sitting on a singleton makes some of you nervous, but we'll come back to whether this was a good idea.
And for the step two, in the iOS app, we create a default Firebase Crashlytics delegate that implements the Kotlin interface and calls the real Firebase Crashlytics SDK. And this is the bridge.
Kotlin interface on the one side, native SDK on the other.
Note that even though we declared NSError on Kotlin side, Swift shows it as any error. And that's Apple's automatic NSError to error bridging. The compiler handles the conversion both ways and we'll come back to this on the cheat sheet at the end.
And finally, in app delegate, when the app launches, we configure Firebase and inject the delegate into the Kotlin singleton.
Two quick things to point out in this Swift code before they trip you up later.
First, log.shared in Kotlin, uh log is just an object, but when the Kotlin compiler exports that object to Objective-C, it generates a shared property to access the singleton instance. So, whenever you see that shared in our Swift code, that's how you reach our Kotlin object.
Second, that do prefix on do we need Crashlytics delegate? The original Kotlin function we created was just any Crashlytics delegate.
In Objective-C, methods that start with init are treated as initializers, constructor constructors, basically.
That's a convention ARC depends on for memory management. So, when a Kotlin function name starts with init, there's a risk Objective-C reads it as an initializer when it's not.
To avoid that ambiguity, the Kotlin native compiler renames it with a do prefix when generating the Objective-C header, and there's a small set of automatic mappings, also.
So, we just implemented the clean textbook dependency injection, also possible with the ITI libraries like Koin, and it worked perfectly before it didn't.
But, in production, the logger we just implemented silently did nothing.
And debugging this took longer than my flight to Munich. We added logging. The delegate was definitely being injected.
We could see the log in the app delegate, but when the view model tried to use it, the delegate was als- always null, every single time.
I spent a full day thinking it was a timing issue.
Let me walk you through our misdiagnosis.
First, we thought it was a threading issue, added dispatch queue.main.async.
It was still null.
Then, the initialization order. Maybe the view model loads with or a delegate finishes, added delays, check the life cycle, injection happens first, and it was still null.
Then, I thought ARC was releasing the delegate, made it a strong property, still null.
At this point, I started questioning my career choices, and what made it worse was Xcode stack traces for Kotlin code are nearly useless for this kind of problem. When Kotlin native code is involved, Xcode shows you shows you KFun prefix mangled symbols.
You can't tell which module or which function it came from at a glance. So, we were doing binary search debugging with the NS log statements anywhere.
And what we get and what we figured was that we were debugging the whole wrong dimension. The answer was something we never even considered.
I ran NM on terminal, uh Unix's symbol dump tool on logger framework, and that's our log class.
And then the same query on composeapp.framework.
It would be a same class name, but different address, 20 megabytes away.
These are the symbol addresses, static layout inside the binary. Two different addresses means two different class definition got compiled in.
But, strictly speaking, this is not the direct proof that two singleton instance exist at runtime.
What is the direct proof, at least at the symbol level, is this.
Yeah, this. We ran NM one more time looking for Objective-C bridge symbols in ComposeApp, but no result printed out.
And that means that ComposeApp's log existed in the binary, but Swift has no way to walk into it. Swift could only see logger's log, and meanwhile, Kotlin code inside ComposeApp was happily using its own internal log with different instance.
We also ran the runtime application to print hash code of the two instance, and it, as expected, two different hash codes printed out.
And this is the part where we need to dive into what's under the hood.
Kotlin's object is a singleton per compiled binary, not per iOS process.
When two KMP module as separate xcframework files, each get its own static global for every single tone. And this is by design. It's how native binary linking works, but if you come from the JVM world where a single tone is a single tone across the whole JVM, this is a complete surprise. Hence, the title of this talk.
I show you the I showed you the symbol table not because every Kotlin developer needs to know NN, but when something fails silently across modules and your usual tool says nothing wrong, just remember that this layer exists.
For the immediate fix is brute force.
Move everything into one module, one xcframework, one single tone, and the problem would be gone. But we have to get up give up modularity, of course.
And build time started creeping up. For a small app, this is fine, but for everything larger, you need a real solution.
For the real proper fix to keep your module separate, you export them through your composed apps framework configuration. Export tells the Kotlin compiler pull this module symbols into the same framework binary. Uh this pattern is commonly called an umbrella module, where you have a single module that re-exports the others into one framework.
And setting the transitive export options to true makes sure their dependencies come along, too.
And the aesthetic true options, uh static linking is what Apple recommends on iOS. The reason is that every dynamically linked framework adds a little bit up to your app's launch time, since the dynamic linker has more work to do at startup.
More frameworks, more work at launch.
Static linking keeps everything in one binary, loaded once. The trade-off is that static linking can increase your binary size, and you have to be careful about symbol duplication if multiple prime frameworks pull in the same code.
But for most setups, static is the right default.
But this isn't even the end of the story. JetBrains is also working on something more structural and that brings us to a very, very relevant update.
JetBrains is actively working on a new default project structure for KMP.
Martin Brau, developer advocate at JetBrains, recently posted about replacing the all-in-one composable module with per platform app modules and separate shared modules.
The post was just released last week and I needed to update my talk slide. So, now the new default KMP projects module have clearer responsibilities, better aligned with the conventions used by other build system.
And I'm very excited to create a new KMP project now.
Now, I've shown you what we did, but I owe you the alternatives because what we did isn't the only path.
And if you're starting a KMP project today and you don't want to write delegate bridges yourself, you have three real options.
First, go to the source.
Firebase has a public feedback portal and there's already an open request for KMP support. It has been made in 12 2 years ago, I guess.
Uh the more vote that thread gets, the more likely Google takes this seriously.
So, if you've ever wanted Firebase to ship a Kotlin multiplatform SDK officially, go vote. Even if uh like me, voting for JUnit 5 support for Android for 7 years now, you've learned not to expect quick results. And second, there's an official Firebase Kotlin SDK from the GitLive team. It's actively maintained and it covers many Firebase APIs. Off, Firestore, Crashlytics, Remote Config. And we've actually looked at this for our project.
It works great for the product it covers, but we needed Remote Config, and only about 20% of the Remote Config API was implemented. That's why we couldn't ship it.
There's also a library named KMP Notifier for push notifications. We used it for it in the initial stage of our project, but ended up implementing delegate bridges.
Third, sometimes the easiest answer is to don't fight the platform. Sentry has first-class KMP support, and so does Bugsnag. And if your only reason for using Firebase Crashlytics is crash reporting, switching to a tool that ships KMP support natively can save you weeks.
So, three paths, and we took the hardest one because we had constraints. You may not, so pick that one that fits your team.
And uh case study three, dependency management. This is the architectural fix that let us keep our modules separated while solving binary and isolations for good.
Quick recap before we get to the new stuff. Earlier we saw on the case study two, we saw how Android handles this.
Firebase Crashlytics ships with uh Kotlin SDK. You just add the Gradle dependency, import it, and call Firebase Crashlytics.
On iOS, until very recently, this was the only way alternatively to manual dependency injection.
You set up a CocoaPods in your Gradle build, declared the Firebase Crashlytics pod, run pod install, and then you could finally import it in your Kotlin code.
Notice the important import path that CocoaPods prefix tells you exactly what's happening under the hood, the Kotlin compiler is pulling Objective-C headers through the CocoaPods.
But, there is a tax. Every iOS developer knows the tax.
Pod files, Ruby gems, pod install taking forever, the occasional pod repo update when your CI breaks for no reason.
The kind of stuff that ages you.
And the bigger problem isn't even the friction. It's the only It's the most modern iOS projects have already moved off CocoaPods. They use SwiftPM.
So, the moment you add KMP to a clean SPM project, you're dragging CocoaPods back in.
And then, just recently before the conference, Kotlin 2.4.0 hit RC, and that means I have to update my talk slide again.
And it also brings SwiftPM import. It's experimental, but it's what a lot of us has been waiting for.
A quick contest for the Android developers. SwiftPM, Swift package manager, plays roughly the same role CocoaPods used to. It's how iOS developers pull native dependency into their apps. The thing is, the most modern iOS projects have moved to SPM, and the default KMP setup leans to SPM, too. So, falling back to CocoaPods means a lot of work around.
And here's the catch with KMP.
Say you pick a native bind library, maybe one of the alternatives I mentioned earlier, or maybe something else entirely. You still have to physically link it into your KMP project. And until this release, if the library didn't ship KMP support, your only real options for that was CocoaPods. But now, SPM is built into Xcode, native to Swift ecosystem, and it doesn't make you install Ruby.
So, how does this actually work? Let's look at the Gradle config first. Inside your Kotlin block, there's a new DSL called SwiftPM dependency. Uh this is where you declare Swift packages you want to consume from Kotlin native code.
With products, which sub-libraries from the package you want to pull in, we grab Firebase Crashlytics for our delegate fix and our Firebase remote config because from the case study one, that's what we needed for our for sub-day feature.
An imported clang modules tells Kotlin specifically when which clang modules to generate bindings for. The same two libraries here.
And that's it. No pod file, no Ruby, just Gradle. And the Kotlin side, the code you actually write looks like this.
Look at the import statement has changed into SwiftPM import. SwiftPM import and the module name and the class, FIR Crashlytics, that's the actual Firebase object- C class pulled directly into the Kotlin native. We don't have to write a bridge or define a delegate interface.
Um so, all of these case studies worth nothing?
Maybe not because one there's one caveat I have to mention before we move on.
This works, but it's still experimental.
A few things to keep in mind before you put SwiftPM import on a critical path.
First is implement experimental status.
JetBrains is actively collecting feedback in the Kotlin and Slack. KMP Swift package manager's Slack channel.
APIs and behavior can change between releases. And we're using it on a side module to learn it, but our production crash reporting still goes through the framework export setup we saw earlier.
Second is static should be true. Dynamic Kotlin native frameworks can hit linker errors on runtime symbol collisions when combined into the Swift PM imported native libraries.
This is separate from the singleton issue we saw in case study two.
The one was about having multiple frameworks. This one is about how single framework links against Swift PM's native code.
The recommendation is the same.
Go static, but for a different reason.
And third, the round trip doesn't work yet. Regular KMP modules, you can already export them as Swift packages, and that's been around for a while. The new Swift PM import is also great. You can just combine the two.
A KMP module that uses Swift PM import can't accept itself be exported as a Swift package.
The direction is exciting, but it's not where I'd build production apps today.
Okay, let's pull case study three together. And while you are looking at the diagrams, I might have a sip of water.
Okay.
One framework called compose app static, and all our Gradle modules, logger, core, data, etc. exported through it.
From Swift's perspective, there's only one thing to import. The split singleton problem from case study two simply can't happen because there's only one binary for the singleton to live in.
However, we could still have the benefits of module separation.
Firebase comes into Swift PM import. Our Kotlin Kotlin code calls a Firebase Crashlytics directly. No Swift delegate, no manual bridge, same Firebase library that the iOS app uses for Firebase app configure.
But pulled into the Kotlin where the logic actually runs.
The Swift side gets smaller. App delegate does what only app delegate can go do. Firebase initialization, push notification things that the iOS system hands directly to Swift. Everything else moves to the Kotlin.
And content view becomes a thin layer.
It imports compose app, displays the compose UI, and forwards native events.
Push notifications, deep links, life cycle callbacks into Kotlin through shared view controllers. And that's the pattern from case study one, and it's the part that doesn't go away even the Swift PM import becomes stable.
So, that's it for the three case study, but it's too soon to wrap things up. So, let's shift gears for a bonus part, Swift and Kotlin interrupt pitfalls. And these are kinds of bugs that don't don't come from the architectural decisions, they come from the language boundary itself.
Remember the state flow bridge from case study one? When you use it, there's something you have to know.
Which thread the native callback runs on.
Firebase callback runs on the main thread, but CL location manager, CBCentralManager, URLSession, these all callbacks on background threads.
And roughly half of iOS native APIs that involve callbacks don't promise main thread.
Half. So, you really can't assume.
The only safe rule is every time you bridge a new API from Swift to Kotlin, check the documentation. And yes, we learned this on the hard way.
And if you update a state flow that compose is observing from a background thread, you'll get a UI consistency warning at best and race condition at worst. You can fix it on the Swift side with dispatch queue.main.async or guarding on the Kotlin side with coroutine scope dispatchers.main.launch.
Either works. I prefer the Kotlin side though, so the shared code stays Swift no matter who calls it.
And these are also type mapping traps.
Kotlin's long maps to the Swift's in 64, not int.
And here's why that matters. Swift's int is 64-bit on modern devices, but Kotlin's int is always 32-bit. So if you pass a Swift int into where Kotlin expects an int, you'll get a compiler error, which is actually a good case because the compiler catches for you.
But the nullable case is worse. When Kotlin long nullable becomes NSError nullable in Swift, and you will lose the type.
NS number will happily give you int value or in 64 value, and if you pick the wrong one, you'll get a wrong value with no error at all. Remember the in 64 from case studies one's remote company code? This is exactly why.
And throwable does not become NSError, it becomes Kotlin throwable. So when an iOS API expects an NSError, like Crashlytics record error, your Kotlin exception just doesn't fit. You need a manual converter at the boundary, the two NSError you saw earlier.
And here is the cheat sheet. I wish someone had shown me this table a year ago, and you can screenshot this slide.
And while you are doing it, note that this is not a complete interop reference. These are just the pitfalls we hit on production, and your project will hit different ones.
If you want the actual full reference, every Kotlin language feature, every type mapping, every annotation, and how each one shows up in Swift, JetBrains maintains a repository called Swift Swift Kot- Kotlin Swift Interopedia.
It's one nice naming. It's under GitHub under kotlin.hands-on, and that's what I wish I had on the day one.
Okay, now let's wrap up. I want to connect everything back to why the community matters.
Back in 2025, Kotlin Conf was held in Copenhagen, and I went there with a clear goal. I was struggling with the DI first, and I wanted to get the direct feedback on our architecture from the people who built it. The key The KMP community and KMP community still lacks established best practices for these kind of problems.
So, I figured the best thing to do was to go straight to the JetBrains booth.
And here's what had happened. I walked up to the JetBrains booth and asked about integrating third-party libraries into KMP project.
The first developer there walked me through the standard in C interop approach, and the second developer chimed in and said, "That approach actually doesn't work for the five races SDK."
So, I explained what our team had ended up with, the delegate pattern I just walked you through, and we really got into it. The kind of conversation you can only have with the people who built the tool. I picked up so many hints from these 15 minutes, and a lot of what we shipped after that conversation came directly from it.
The SPM-based C interop approach we ended up adopting, that cannot that came out that exact conversation.
And then, at the end, the second developer there came to me and said, "You shouldn't be here right now. You should go to the Google booth and demand official KMP support."
So, the everyone in the booth laughed, and this year I came back with a different strategy. Instead of going to the Google booth, I figured I'd just get on the stage and tell all of you about it. So, consider this uh technically fine following their advices.
And the conclusion from this experience, the KMP community is still evolving and we need your help. Best practices don't come from the API references, they come from the people building real things and sharing what they learn.
And this talk exists because I came to Kotlin Conf last year, spoke with a JetBrains engineer at their booth and got feedback that they are directly saved our architecture.
And if this is your first Kotlin Conf, go talk to people, visit the booth and that's where the real learning happens.
Try things out both for KMP supporting Firebase, experiment with the Swift PM import and tell JetBrains what breaks.
When I first started this project, most of what I needed wasn't in the official docs, official docs. It was buried in someone's blog post like 2:00 a.m.
And if you find any better solutions or hit different problems, I'd love to hear about it. Come find me or reach me on LinkedIn.
So, that was a lot of stories, so I I had to wrap up. We covered three things today. First is native state observation, when events come from the iOS side and your shared shared compose UI needs to react, you can bridge them through a state flow exposed from the Kotlin singleton.
The platform specific code stays on the platform side and the shared logic stays shared.
Second, non-KMP is the case, the hardest lesson of the year.
The Kotlin object is the singleton per compiled binary, not per process. Two frameworks and two singletons and a delegate that silently disappears. The fix is to export the shared module to both sides, so, uh, the both sides point to one copy.
Third is dependency management.
CocoaPods is what we were running production today, but Swift PM import is the direction things are going. Promising, but still experimental as of today.
One year in and here's where we are.
100% of our UI is composed and across the whole code base, 98% is Kotlin. Server and client speak the same language and for three Android developers shipping on iOS app on deadline, that's not a small thing.
KMP on iOS works in production. Not without scars, but it works. And I hope every scar in this talk is one you don't have to get yourself.
And thank you so much for listening to my talk and thank you.
Thank you so much.
Please feel free to come find me afterward or reach me out on LinkedIn if you'd like to chat and don't forget to vote.
Thank you and danke schön.
Danke schön.
Related Videos

TOP 15 Data compression Interview Questions and Answers 2019 Part-2 | Data compression | Wisdom jobs
wisdomjobs
281 views•2019-06-28

CTS 158: 802.11w Management Frame Protection
ClearToSend
4K views•2019-02-04

NDSS 2019 Send Hardest Problems My Way: Probabilistic Path Prioritization for Hybrid Fuzzing
NDSSSymposium
496 views•2019-04-02

How realistic is Cities: Skylines?
CityBeautiful
159K views•2019-02-14

GUIs & TUIs: Choosing a User Interface for Your Python Project | Real Python Podcast
realpython
2K views•2025-04-04

The OSI Model - Explained by Example
hnasr
225K views•2019-05-12

Cloud Computing - Introduction
elithecomputerguy
98K views•2019-10-07

From Traveler's Dilemma to Dynamic Routing | Demystifying Networking
IITBombayJuly
5K views•2019-08-04
Trending

WOW! Judge TURNS THE TABLES on Trump in His OWN $10B LAWSUIT!!!
MeidasTouch
197K views•2026-07-23

Playstation NO DISC/NO BUY Fight Is Over...
DavidJaffeGames
4K views•2026-07-23

Steam and Xbox Just Dropped The Hammer On PlayStation
OhNoItsAlexx
9K views•2026-07-23

Americans Confused in Australia for 17 Minutes Straight
IWrocker
17K views•2026-07-23