· 10 min read Posted by Kevin Galligan

Iterating

KMP For Native Mobile Teams

The second phase involves improving your team's KMP skills, shipping KMP to production, and preparing for Scaling.

This is a post series with several sections. If you landed here first, make sure to go back to the Intro to get the full story.

Javascript disabled?
Our site requires Javascript for some sections. Please check that Javascript is enabled.

Iterating is the middle stage of our KMP adoption plan. The borders of this phase are less strictly defined, but do have some important milestones.

At this point, you should have successfully introduced some KMP, worked through basic technical issues, and gathered initial feedback from the team. The organization as a whole considers KMP a viable and reasonable addition to the app development tech stack.

In this phase you’ll:

  • Put KMP into production. That includes some form of crash monitoring library, to ensure potential issues related to KMP code are properly recognized and addressed.
  • Expand the core KMP team. Have more developers edit and contribute to the KMP library.
  • Gain experience on writing Swift-friendly APIs. Learn what works well and what doesn’t.
  • Continue gaining feedback and learning.
  • Prepare for scaling.

Putting KMP into production

Physically adding KMP to the production build usually involves no extra steps or effort. There are very specific cases where App Thinning and other production-related processes require configuration, but for simple KMP code, this is generally not an issue.

The critical parts of deploying to production involve:

  • Some form of crash reporting and/or monitoring that can properly report on KMP code.
  • For apps sensitive to binary size, evaluating binary size impact and learning to reduce unnecessary increases.

Crash Reporting

As KMP will be a new addition to your iOS app, and as it’s not always a technology embraced by the whole team, making sure you can recognize true KMP issues, and be able to address them, is important. Without the ability to monitor KMP effectively, novel production issues have the tendency to be blamed on KMP. We call this the “unwanted guest effect”. Being unable to quickly identify issues and address them will erode confidence.

The problem with KMP crash reporting is simple. Kotlin’s process for exceptions is different from that of Swift/ObjC. When an error occurs in Kotlin code, on any target platform, the exception bubbles up the stack until caught, or until the system receives an uncaught exception and crashes the app. On Android, crash reporting tools are built to handle this. They collect the full stack trace, along with other metrics, and are able to consolidate reports for developers to review.

For Swift and ObjC, the process is different. Exceptions are explicitly checked, if they’re allowed at all. When an error occurs, that triggers a fault, and the app is halted. Crash reporting tools collect stack traces at that point.

When you call into Kotlin from Swift, that’s the “border”. An uncaught exception from Kotlin that bubbles to that border will trigger the system halt at that point. iOS crash reporting tools will collect stack traces at that point, unaware of the Kotlin exception stack.

In summary, you lose all context of the Kotlin crash.

Kotlin-specific crash reporting libraries for iOS simply extract that Kotlin stack info and present it to the iOS crash reporting tool in a format that it understands. Properly configured, those tools can then desymbolicate the stack, accurately pointing to the error source, report Kotlin exception info such as the type and message (often critical info), and consolidate potentially many individual reports to enable efficient review.

Not all crash reporting tools have libraries available, and depending on their API and implementation, adding direct KMP support may not be possible.

For Crashlytics and Bugsnag, Touchlab publishes CrashKiOS. Along with basic crash reporting, CrashKiOS supports breadcrumb logging and instance specific information to be added to reports. An alternative tool is NSExceptionKt. It is more specifically focused on crash reporting. These tools have been in parallel development for the past few years, and CrashKiOS now relies on NSExceptionKt for some of the core internal stack processing and integration.

Sentry, another popular crash reporting tool, has published official KMP support. For private, commercial apps that prefer not to use Crashlytics for whatever reason (data privacy, features, etc), Sentry is our preferred option. They offer a free tier, and a host of extended features included with their paid tiers.

If these tools are not an option, at a minimum, your code should capture the stack trace and exception metadata and include that info in the crash report.

Binary Size

iOS apps are known to be relatively larger than their Android counterparts. For this reason, many Android engineers overlook binary size as a major concern when discussing KMP. However, binary size is a major concern for many apps. If binary size is a concern for your product, it’s critical to understand how to accurately measure KMP’s binary size impact, and how to avoid adding significant binary size unnecessarily.

We have published content about binary size impact estimation over the past few years. Little has changed, so we’ll link to those docs for detail when you need to review it.

In summary, measuring binary size impact is not as simple as looking at the compiled framework size on disk. For Android, the size of the AAR or APK is essentially the size of the application. For iOS, when deployed, Apple runs a number of operations to strip unnecessary data, unused architectures, and assets that won’t be used on the devices installing the apps. As a result, getting an accurate size estimation requires understanding how to produce these numbers.

Beyond just measuring app size, it’s important to be able to measure the size impact that your KMP code has on the total size. There are various approaches to this, again, discussed in other content.

When pitching KMP, you haven’t built much KMP yet, if any. Having ballpark figures is necessary. This information is part of the research that should be done prior to pitching KMP, as iOS professionals will usually have questions. Generally speaking, a minimal KMP library with no libraries will add 500k. With more common libraries (Ktor, Sqldelight, etc) and more code, KMP will usually add a few megabytes of binary. The specific amount is relative to how much code, how many libraries, and how much of those libraries you actually use. Kotlin/Native is static, and can strip code you never call, so it would be difficult to give estimates for any particular library as you may only be using a portion of it.

As for incremental increase, we’ve done some relatively simple tests. Additional Kotlin code adds roughly similar binary size when compared to additional Swift code, but only when that Kotlin code is internal to the library. When exported, it is a different situation.

Reduce your API Surface

Code that is exported in the header directly to Swift requires additional binary to handle that communication and overhead. This can add significant binary to the total.

The code that is actually called directly from Swift is usually a small subset of the total Kotlin code that exists in your KMP. The Kotlin compiler does not know what code you plan to call from Swift, so it will export anything public and reachable from the module generating the Framework. When we audit KMP code, teams almost universally will expose dependencies and code that Swift does not need, which results in extra binary that serves no purpose.

Additionally, you can configure the Kotlin compiler to export modules and dependencies to Swift that otherwise would not be. This is often useful and necessary, but needs to be done with care. Kotlin also lets you transitively export dependencies. Doing so makes accessing code from Swift simpler, but is almost never needed, and can have a significant, negative impact on both your binary size and header size, thereby also decreasing readability of your KMP API.

The general rules:

  • Mark structures that don’t need public visibility as internal
  • Don’t export dependencies unless necessary, and minimize the public surface of those that you do
  • Never transitively export
  • Be careful with types of parameters and return values. Making a type from a dependency visible requires dragging in everything it exports. This can dramatically increase the export size
  • Consider isolating portions of your code in modules that aren’t globally exported, but only referenced with parameters and return types, which will be selectively and minimally exported.
  • Be careful with code-generation tools. They often expose code you don’t need directly.

Expand the core KMP team

If you’ve formed a core team that does not currently include the whole team, add some new members. As the iOS team if of particular importance as far as exposure, try to make sure there is good iOS representation.

Also, encourage the iOS team to directly edit some KMP code as opposed to mostly consuming or configuring the code.

Keep your goals and roadmap in mind

We have seen teams struggle when they simply have specialists start editing code in other platforms. This is true in both directions. Kotlin, it’s conventions, patterns of the Android/Kotlin/JVM world, and in particular Coroutines and Compose, are as foreign to iOS developers as the Swift/iOS equivalents are to Android developers. Either-side specialists can generally “figure out” how to make things work, but core Kotlin/KMP code written by Swift engineers will likely raise eyebrows during code review. The same would be true of Swift code written by Android engineers.

Having a plan for how to integrate iOS engineers into KMP code editing is important. While the total amount of KMP code at this point may be small, starting to think through what layers of the KMP code make sense for iOS developers to edit, and what the learning process will look like, will help with planning for the Scaling phase.

Still, though, iOS developers getting involved with KMP will need to learn some Kotlin. Just be aware that simply pointing them at the code may not be the best way to approach it. Also important to keep in mind, you are in a phase where making mistakes and being inefficient is expected. You’ll eventually have new iOS developers joining the team when you are using KMP in fully-scaled, maximum efficiency mode. Onboarding those devs should have a process.

Sharpen the Tools

As mentioned, Swift-friendly APIs are not a guarantee out-of-the-box. Far from it. Learning what works well and sharing it with the team is important.

When you’re doing full feature dev, having some standards set for architecture and “the borders” is important. The iterating phase is a good time to start that planning and practice.

This is also a good time to get feedback on the tooling you use. Is there a negative impact on CI? Are the iOS builds taking much longer than expected? Do edits to KMP, particularly for iOS builds, have a significant impact on development round-trip build time?

Some issues with KMP are not avoidable. Build times have improved considerably, and are being improved by JetBrains as I type this, but that is still an issue. However, changes to configuration and to workflow can dramatically improve the development experience.

Some issues that teams experience with KMP are simply due to inexperience or misconfiguration. We’ve seen a number of teams with significant KMP code make relatively obvious mistakes. Mostly because KMP is new, and production-scale advice and best-practice are scarce. Keeping an eye on these issues and mitigating them is important.

Continue collecting feedback

The ultimate goals of this phase are to get the team on board, prepare for actually efficient development with KMP, and to move to scaling. Regular communication and feedback from the team are important. This sounds like an obvious statement, but if “assumed” or ad-hoc, you’ll likely miss key information, opportunities to improve, and opportunities to foster wider team collaboration and ownership.

In short, don’t skip the management fundamentals.

Prepare for scaling

The switch to Scaling is more dramatic than the library dev mode of Piloting and Iterating. As discussed in earlier sections, the bulk of code for most apps is the feature code involved in implementing the features of the app. That is the architecture, the data management, and everything involved in implementing whatever the app “does”.

To implement significant amounts of code with KMP, that means implementing this feature code with KMP.

Most teams struggle to make this transition. With a better understanding of why teams struggle, along with better preparation and planning, scaling KMP is certainly achievable.

Understanding how to produce usable Swift APIs from Kotlin is important, and one of the focus areas for the first two phases. In preparation for scaling, thinking about the architectural “borders” is important. Not just the raw API surface, but where does the shared code stop and the native code start? What patterns for architecture and borders will your team adopt? Architecture patterns are important for single, homogenous platforms. They are significantly more so when sharing a single codebase across different platforms, with different lifecycles, conventions, and architectural structures.

These are the kinds of decisions that need to be made when moving to feature dev with KMP. While you don’t need to have everything figured out on day 1, preparing the team to start thinking through and discussing these topics is a good idea, and as you’re improving your KMP skills and tools, it’s a good time to start thinking ahead.

Next Section : Scaling: Why it’s hard

While many teams struggle with scaling, it is not a transition that needs to be abrupt and disruptive. Also, if planned well, you can pause scaling and iterate on fundamentals. Feature dev with KMP is not required to be universally adopted all at once. That is one of KMP’s core benefits. You can introduce KMP incrementally.

The difficultly involved with scaling is in fully realizing it. Not so much with starting. So, let’s get started.