Introducing Mobile Multi-platform to a Native Pod

Idan Aizik Nissim
Nanit Engineering
Published in
7 min readApr 21, 2022

--

A Product Oriented Delivery (Pod) team consists of members from different disciplines, from ML engineers, Backend, BI, Mobile, QA engineers, all the way to UX/UI designers.

A Pod is assigned and focused on a single project that they need to deliver successfully, while collaborating, making joint decisions and keeping cross environment awareness of which member handles which product’s needs.

One of the many KPIs of a Pod is to deliver faster. Most of the requirements can be assigned to a single discipline, API to the Backend, Containerize to the DevOp, the QA on the test plan, etc.
But in a Pod that is made up of mobile native teams, we have iOS/Android developers who both need to be assigned with the same need.

This can setback the project delivery times, due to the simple fact that the same code needs to be developed and tested twice on both platforms.

Collaborating & Making joint decisions

The challenges of moving to multi-platform

At the point where your company decides to move to Pods, your mobile apps might be too big to throw in the bin and rewrite in one of the many available cross platform solutions.
Also, switching to another framework will require your mobile engineers to learn a new tech stack from scratch. Languages JS/TS for React Native, maybe Angular for ionic, or Dart for Flutter, to a completely different IDE — and this is out of the question for your fast growing, highly new feature delivery demanding company.

Don’t touch my UI

Another issue that may arise is your UI, which has been built using the native platform components.
Your user is used to the native look and feel of your app. Your developers are wizards in Jetpack Compose and SwiftUI, and your designers are used to designing using these native components catalogs.
So you don’t want to rock the boat in this area as well.

Giving in

This is the crossroads at which most companies give up on the idea of moving to multi-platform solutions. They don’t have the time and/or resources to allow for the learning curve and refactors.
But is it really a situation of all or nothing?
Let’s take a closer look at what mobile apps consist of.

Common application components

All (nicely designed) mobile applications are built in layers.
A network layer in charge of API calls, and a storage layer to cache the API responses or user input to be synced when they get back online.
Repositories that coordinate between API calls and their caching mechanism, as well as serving data to the many use cases and view models that can be found in mass all over the app.
We can break those layers into two minimal groups

User Facing Layer

Views, what the user sees and interacts with, it can be a button, textfield on screen, home screen widget, or even a notification with an action button.
This layer will likely be written using the native UI framework, Views or Compose on Android, Storyboard or SwiftUI on iOS.

Data (Business logic) Layer

All others.
From API wrappers, Database service, Repositories, Use cases and View Models. Likely to be written in Java/Kotlin on Android or ObjectiveC/Swift on iOS.

Common App Components

Back to The challenges

Now that we’re aligned on what common apps consist of, let’s tackle the challenges keeping us from moving to multi-platform.

Native UI — just looks too good to be replaced

We’ve already stated that we have no interest in sharing the UI. Our users are used to the native look and experience, and in the end you are what you wear, and your app is what the user sees and feels. We don’t want to “underdress” our app’s UI for the sake of code sharing.
Moreover, our design system is using native UI components heavily, and our developers already have experts in laying the UI 1:1 to the designer’s designs. But that’s just the User Facing Layer, what about the Data layer that contains most of the code?

Learning new tech stack — ain’t nobody got time for that

We want to move fast, deliver features one by one like an assembly line. We have demanding users, and we don’t have time to take a step back and learn a new technology and refactor the existing code. While saying that, finding a way to share the Data layer will allow us to move a significant part of the app’s code.

We want to find a way to keep the native look and feel, but share all the other code in a familiar language and development environment. We are being a little bit greedy don’t you think?
Luckily there is something exactly for that!

Enter Kotlin Multiplatform Mobile

In contrast to other cross-platform solutions that require all code to be shared, Kotlin Multiplatform allows you to implement a single codebase for the application logic, while keeping the possibility of implementing native UIs.

Android developers will feel at home developing in Kotlin and inside Android Studio using Jetbrain’s KMM Plugin. The iOS developers, on the other hand, will have to adapt to writing in Kotlin, although Swift is like Kotlin so they will find some similarity in it. When it comes to the IDE they can choose to stay in Xcode, using a Touchlab plugin, or develop the KMM module in Android Studio/Intellij idea.

Using KMM, you can replace your existing modules with a single codebase. For example, your Api Module, which is probably written in Retrofit on Android and Alamofire on iOS, can be replaced with Ktor. Your CoreData and Room of the Caching Module can be replaced with sqldelight.
Those Core Modules can be used throughout the entire future development of the KMM feature modules of your app.

But again, we don’t have time to leave everything we’re doing and start writing these core modules.

We have features to deliver!

Combined benefits of creating
cross-platform and native apps

A little bit goes a long way

Start small, really small.

Take a look at the UML below, the Minimal Share box is what you will break out of the KMM feature module, without making drastic changes to your code base. You will see that the Use Cases, Repositories and View Model state’s classes can simply be cut and pasted into the module.

But what about the repo that needs an API service? Your Android code might be using Retrofit to handle API calls, and iOS Alamofire. Those libraries can’t be used in the KMM module, also we don’t want each feature module to handle API calls directly.
It is a good idea to create a core module for the API wrapper, but it’s way out of scope for the small step we want to take right now.

So don’t worry about the API here. Define an interface, and inject it into the provider using the expect factory pattern

interface BabyLogApi {
fun getBabyLogs(babyId: Long, day: Long): List<BabyLog>
fun startFeedingSession(babyId: Long): Long
fun endFeedingSession(id: Long, cancelled: Boolean): BabyLog
}

expect class BabyLogApiFactory {
fun createBabyLogApi(): BabyLogApi
}

Your native code API service will implement the interface, and an actual class for iOS and Android will proxy the service to the repository. The same rules can be used on the database/cache system in your app.

Your module’s structure will look similar to this:

You are now ready to add the module as a dependency for your native project and ship it.

Allocate time for core modules

Yea! Your native app has shared code! Although it’s a really small part, you’ve taken the first step and opened a window for more.
Now that your appetite is whet for more shared code, it’s time to add tasks to your board.
Again, start small, decide on a single core module, for example the API wrapper module, and split it into sections.
Each sprint, the iOS developer will implement one section and the Android another one, you will cover the entire API in no time.

Retrace back

When a core module is complete (or just part of it), revisit a feature module and replace the placeholder interface and factory with the module.

Impact

Shared code — Shared bugs

Your shared code can now be tested by QA.
Any bugs they find, will need a fix in a single place.
Of course they won’t find any because you will test your KMM module.

Move Faster

All we really want is to move faster, deliver new features and iterate on existing features rapidly.
Now that most of the feature code is being shared, the mobile developers can split the business code between them. For example, one will implement the use cases, while the other will implement the view model and manage the state.

Pair programming

Sit down with your mobile coworker, take a cease-fire for a minute from the questions of which platform is better, and take a look at the feature TLD and UI you were asked to implement.
Then raise the following questions:

  1. What possible states does the UI have?
  2. What API does this feature require?
  3. What Will the UI communication with the View Model look like?
    (user driven events, device connectivity state change, etc..)

Try answering those questions, and stub together the business logic.
When you feel comfortable with it, split the implementation work.

Wrapping up

Integrating shared code into native code base apps can be overwhelming.
Don’t be afraid to take the first step and make it the smallest that you can.
From there you’ll find your own way to start replacing more chunks of your native code without taking up a lot of time from other important tasks.

--

--