I've spent the last few months writing an app for both Android and iOS, and I've developed some strategies for sharing as much code as possible between the two apps to minimize duplicate work and ship quickly and consistently.
I'm not talking about React Native or Kotlin/Native here – just plain old native apps with Kotlin and Swift!
For me, a shared architecture has been the most important component of reducing duplicate work across platforms. I'm using something very close to model-view-intent as it's described in this blog post by Hannes Dorfmann. We basically want to map input events (from a user action or a change in the app's data model) to discrete updates for the view to consume and render.
This mapping of inputs to UI updates is implemented with RxJava on Android and RxSwift on iOS. Each input event is correlated with its own transformer class that encapsulates a series of operations – for example, we can
flatMap a user click to the result of an async network call and
map that result to a model for our view to consume and render. This idea of transforming observables is explored in much more detail in this talk by Jake Wharton.
Additionally, we can use RxBinding and RxCocoa for creating observables from user inputs, and relays from RxRelay and RxCocoa to create observables from other inputs. (Relays are just subjects that never terminate.)
Similar libraries + extension functions
In addition to the libraries mentioned above, we can choose libraries with similar APIs on each platform to share even more code.
For example, on Android we can use
DiffUtil to process two different lists of data and produce a diff that a
RecyclerView can render. Since iOS doesn't have anything like this built into the platform, I've been using a library called Differ to accomplish the same thing.
We can take this one step further with extension functions. Since the APIs for
Differ are not the same (one could say that they "differ"... ha ha), we can write an extension function for
RecyclerView.Adapter to get the APIs as close as possible. Extension functions allow us to alter the public API of any library to closely mimic its cross-platform counterpart.
I've also been using a bunch of client-side Firebase libraries – Auth, Firestore, Analytics, and Crashlytics – which have nearly identical APIs on each platform. This is a massive bonus for developers of cross-platform apps.
Similar language features
Architecture and libraries aside, Kotlin and Swift are not that different! Here are a few examples of similar language features that allow us to do even more code sharing:
- Both languages provide optional types for better null handling.
- Kotlin has data classes and Swift has structs with auto-synthesized
- Kotlin has sealed classes and Swift has its own version of enums. These aren't quite the same but they both work well for representing discrete UI states like I mentioned above.
- Both languages provide collection operations like
- Both languages allow you to write extension functions.
Language similarities combined with a shared architecture and similar libraries allow us to write code that is nearly identical on both platforms.
I've noticed three concrete results of implementing these code sharing strategies:
- As an Android developer, I can develop an iOS app with barely any prior knowledge of iOS or Swift. I can write a feature on Android then fumble my way through the iOS code by copying as much as possible and googling the rest.
- I can ship faster. I can write a feature for Android then spend half the time or less porting it over to iOS (unless the feature involves lots of interaction with platform-specific APIs).
- I only have to fix bugs once. If I'm seeing users creating duplicate objects on Android, odds are high that I'll be seeing the same bug on iOS. Since the code is nearly identical on both platforms, I only have to figure out the fix once – on whichever platform I'm more comfortable.
All of this comes with a few big caveats.
First, platform-specific features like cameras and databases will still require lots of platform-specific code (and domain knowledge). Third-party libraries might be able to ease this pain to some degree.
Second, each platform handles UI component lifecycles a bit differently. We can't always substitute
viewWillAppear() directly for
Finally, Interface Builder on iOS and Layout Editor or XML on Android are totally different. Auto Layout on iOS and ConstraintLayout on Android are pretty similar, but the tools for building them are not. I think it's worth investing the time to learn these tools; I don't recommend Kotlin and Swift DSLs like Anko and SnapKit, though I admit there might be some opportunities for code sharing there.
I'm excited about Kotlin/Native for multiplatform app development. I think that's the logical next step (admittedly a pretty big one) from what I'm describing here. Check out the Droidcon NYC app by Kevin Galligan if you haven't already.
Until then, I think teams can do a lot more to empower individual engineers to write code on both platforms through a combination of good documentation, pre-built libraries and infrastructure, and freedom to experiment!