When a company ships the same product on several different platforms, engineering teams invariably begin to wonder whether they could be sharing code between the client applications. It’s easy to be overwhelmed by the number of tools that purport to be best for the job, and evaluating cross-platform frameworks can be a time-consuming (and possibly contentious) undertaking. Quizlet’s team dealt with this conundrum a bit earlier than most.
As we searched for ways to overcome these obstacles, we evaluated (and decided against) React Native before migrating to Kotlin Multiplatform (KMP) in 2018, becoming one of its largest and earliest adopters. Along the way, we learned a lot about what to look out for when selecting cross-platform tools and how to make cross-platform code succeed at scale. Hopefully, you’ll find these learnings helpful for your own code sharing undertakings.
In a cross-platform code sharing utopia, programming would be a “write once, run anywhere” experience.
In a cross-platform code sharing utopia, programming would be a “write once, run anywhere” experience, and no engineer would need to concern themselves with platform-specific nuances. Mobile engineers, however, know this isn’t the reality, and we view shared code frameworks with a healthy degree of skepticism. In our experience, the nuances, complexities, and edge cases that go into developing a best-in-class app are impossible to cleanly abstract away.
Stories of glamorous successes and explosive failures told on company blogs, at conferences, and on Twitter often make it difficult to determine objective evaluation criteria. But each code-sharing tool provides different capabilities, so it’s important to evaluate them within the unique context of your stack, business, and team. Does the tool allow for flexible integration with your existing code? Does it complement your team’s strengths? Does it blend with your existing processes? Will you need to rethink one or more of these aspects?
Understand the scope
Many engineers assume that every cross-platform shared code framework aims (with varying degrees of success) to handle all possible scenarios one might encounter when building an app. While this is the vision of tools like Flutter and React Native, tools such as KMP or J2ObjC place user interfaces outside the scope of the core framework. When evaluating a tool that purports to cover a wider surface area, consider that this breadth is frequently supported by a more shallow handling of use cases—for example, Shopify found that React Native’s threading model couldn’t handle background tasks at the scale it needed without the app becoming unresponsive. When you inevitably need to go deeper than the shared framework allows, prepare to write the native components for each platform, as well as the bridging code to the framework.
KMP allows you to write cross-platform libraries to share business logic across your codebases, and because it assumes you’ll need to write platform-specific code (for example, to implement native UIs), it tries to make the process as easy as possible. This same interoperability allows engineers to incrementally evaluate KMP versus native code in existing projects without having to commit to drastic architecture changes or a large rewrite.
Again, it’s critical to evaluate these nuances within your specific context. The Quizlet team, for instance, revels in building native UIs; it was our complex state machines and rule engines that were tripping us up. KMP allowed us to focus our shared code efforts on the challenges that constrained us without also forcing a shared UI.
Think of the user
Of course, performance and app size aren’t the only user-facing costs of shared code. Mobile users expect their apps to have consistent patterns and gestures, such as those outlined in Apple’s Human Interface Guidelines and Google’s Material Design. Building shared UIs without accounting for platform-specific differences can result in apps that seem off to users.
Although Quizlet’s UI is intentionally free of complex interactions, the intricacy of the business logic that powers features such as our Learning Assistant (which provides personalized study paths, progress insights, and smart grading) was an obstacle to iterating quickly across platforms. Quizlet’s decision to go with UI-agnostic shared code platforms allowed us to specify what information users should see in the shared code while using native components and design language to render that information.
Making it usable
Take it one step at a time
Cross-platform tools tend to promise the world, and it’s tempting to try to use a new tool in multiple architectural components or user-facing features at the same time. But it’s best to start simple. Rather than attempting several sweeping changes at once, pick a single, strategic problem to solve and make incremental progress. Celebrate your successes, learn from the shortcomings, and evaluate how to move forward.
Recognize it’s okay to bail
You did your research, picked a code sharing framework, and started to build with it. That doesn’t mean you’re stuck with it forever. Airbnb, for example, famously stopped using React Native for a variety of technical and organizational reasons, while Dropbox dropped its shared C++ libraries because of the hiring and maintenance overhead.
Stress the API
When working with complex code, misunderstandings about the meanings or valid values of parameters or outputs can easily arise, even within a single programming language. Imagine the issues that can crop up when you’re working across languages with differing type systems!
Also in this issue
A primer on automated mobile testing
Insights for teams looking to tailor their test suites and build apps that just work.
Don’t skip the tests
Making changes to shared client-side libraries comes with extra overhead with regard to releasing, debugging, and communicating, just as it does when making changes to backend APIs. Without solid testing practices, small bugs, no matter how trivial, can bring your productivity to a crawl.
The logic our team extracted into shared code was frequently the easiest code to test. Shared code components often have very little state, and their most complex dependencies are abstracted away. Using KMP has forced us to create a clear separation between UI and business logic concerns—plus, KMP provides a cross-platform test framework that’s well-suited to testing business logic.
Making it successful
At the start of your journey with a shared code framework, it might not seem like every engineer needs to know the ins and outs. But not sharing knowledge and resources early on can have serious consequences down the road. Before you know it, folks are either going out of their way to avoid using the framework, or they’re using it on several projects without enough people who can critique engineering plans, review pull requests, and get the team unstuck if they run up against a limitation of the platform (or their understanding of it).
When rolling out any new technology, it’s vital to support teams as they learn. Put together a concise curriculum for people new to the language and framework; we started with the Kotlin resources Learn by Example, Hands-On, and Koans. Ensure newcomers feel comfortable leaning on peers for support, and celebrate wins as a team. Most importantly, turn those newcomers into advocates for the platform. Once you’ve selected the right cross-platform tool for your needs and empowered your team to work with and advocate for it, you’ll be well on your way to success with shared code.