MultiPlatform development using Kotlin

Julius Canute
Jun 7, 2020 · 11 min read

I wanted to experience Kotlin MultiPlatform development to understand the complexities involved in adopting this for Android and iOS projects. For this, I wanted to target a small use case and solve that by using Kotlin Multiplatform. In this article, I’ll share my observations with you as you take this journey with me.

Prerequisites

  • I assume you have sufficient exposure to Kotlin & Swift to understand DSL.

First, We’ll go through the problem, followed by one of the possible solutions to the problem and how we’ll use the MultiPlatform approach to solve the problem for Android and iOS, finally conclude by showing how we can use what we built in both platforms.

1. Overview

1.1 Problem

1.1.1 Environment
To help understand the environment issue, the figure below shows the non-working environment in red and working environments in Green. An environment can be DEV, SIT, UAT or PROD. Given our App was pointing to red, we wanted to have the flexibility of switching the environments without performing a rebuild to triage if this issue is related to the environment as opposed to a code path within the App.

Image for post
Image for post
[1.1.1] Environment

1.1.2 Feature
To understand the issue related to a non-working feature, the figure below shows our App as being composed of Feature blocks and the working features in Green. In contrast, the non-working feature in Red. Given we wanted to identify the problematic feature in Red, we wanted to have the flexibility to turn features on or off without performing a rebuild to triage the problem.

Image for post
Image for post
[1.1.2] Feature

1.1.3 Tuning Thresholds
Consider we wanted to adjust timing intervals in our App to make them perform better or appear suitable. The figure below shows an App contacting the API and receiving a response. If our timeout interval is too short, our App stops listening soon on the other hand if it is long our App waits for a long time and when this wait is on a critical path of the App, we’ll see a notable lag in our App’s performance. To predict the correct values, we wanted to have a flexibility of adjusting the timeout values to identify what works for our App better, without having to rebuild our App each time.

Image for post
Image for post
[1.1.3] Tuning Thresholds

1.1.4 Dynamic Value
Consider we wanted to edit a configuration value to test a change quickly without having to rebuild our App. To illustrate this scenario, the figure below shows our App pointing to a non-existing domain example.com while we wanted to perform a quick check to ensure the new domain global.example.com works as expected.

Image for post
Image for post
[1.1.4] Dynamic Value

1.2 Goal

“Given that the App has the necessary code. we wanted to change how the App behaves once deployed on the device with minimal effort.”

2. Solution

Image for post
Image for post
[2.0.1] Solution Overview

We’ll implement this solution using the MultiPlatform approach to generate native Library for Android & iOS. Our Library consists of three layers, as shown in the diagram below:

Image for post
Image for post
[2.0.2] Solution Architecture

Layer One — This layer contains the data models and the logic of our solution. The code in this layer does not depend on the other two layers and uses Kotlin. The Kotlin code here can natively compile for use in Multiple Platforms such as Android, iOS, Windows, Mac, Linux, JavaScript and JVM to name the prominent ones.

Layer Two — This layer contains platform-specific implementation for saving the configuration values in our solution. The code in this layer depends on Layer One but does not depend on Layer Three. This layer contains platform-specific Kotlin code for iOS and Android.

Layer Three — This layer contains native UI implementation on the corresponding platform. The code in this layer depends on Layer Two and Layer One. In the target Android, this layer contains Android layout files and the corresponding Kotlin code for the UI. On the target iOS, this layer contains Swift code that uses the UIKit framework to handle configuration state change in UI.

Note: Whatever is in Layer Two can also be in Layer Three and vice versa. It is a decision we have to make based on the complexity. In this case, having the persistence layer done completely in Layer Two was simple compared to having to do the UI layer completely in Layer Two. Hence the decision to push the UI implementation to Layer Three.

2.1 DSL — Layer One

  • key — enables retrieval of value in the configuration.
  • description — provides an option to describe the intention of the configuration.

2.1.1 Solving Environment Problem using Choice DSL
To tackle the problem highlighted in Environment[1.1.1], we’ll develop a choice DSL, as shown below. This DSL gives us the flexibility to select one of the items from the available choices. In the Choice DSL we have the following:

  • currentChoiceIndex — a value of the default item selected in the configuration.
  • item — available options in this configuration.
[2.1.1] Choice DSL (Implementation)

2.1.2 Solving Feature Problem using Switch DSL
To tackle the problem highlighted in the Feature [1.1.2], we’ll develop a switch DSL, as shown below. This DSL gives us the flexibility to turn features ON or OFF. In the Switch DSL we have the following:

  • switchValue — current value of the Switch true/false.
[2.1.2] Switch DSL (Implementation)

2.1.3 Solving Tuning Thresholds using Range DSL
To tackle Tuning Threshold [1.1.3], we’ll develop a range DSL, as shown below. This DSL gives us the flexibility to adjust a value within a specified min & max range. In the range DSL we have the following:

  • min — minimum value allowed by this configuration
  • max — maximum value allowed in this configuration
  • currentValue — the present value of this configuration
2.1.3 Range DSL(Implementation)

2.1.4 Solving Dynamic Value using Editable DSL
To tackle Dynamic Value[1.1.4], we’ll develop an editable DSL, as shown below. This DSL gives us the flexibility to edit a given value to our heart’s content.

  • currentValue — the present value of this configuration
[2.1.4] Editable DSL(Implementation)

2.1.5 AppConfig and Config DSL
We wanted the flexibility to change multiple configurations related to the App and be able to try different variations of the grouping to find what works better for our App. For this, we have AppConfig & Config DSL as shown below.

[2.1.5] AppConfig(Implementation) & Config DSL(Implementation)

2.2 Persistence — Layer Two

  • Android uses SharedPreferences
  • iOS uses NSUserDefaults

2.2.1 Load Configuration — Layer One
The following snippet illustrates loading of saved configuration.

[2.2.1] Load Configuration (Implementation)

2.2.2 Save Configuration — Layer One
The following snippet illustrates saving an updated configuration.

[2.2.2] Save Configuration (Implementation)

2.2.3 Setting Expectations — Layer One
We can see from [2.2.1] and [2.2.2] that the code in Layer One is a template of the operation expecting implementation from the platform for Settings. Kotlin native has an elegant way of doing this, as shown below.

[2.2.3] Setting Expectations (Implementation)

2.2.4 Actual Implementation — Layer Two
To meet the expectations of Layer One, we have to provide corresponding actual implementations in Layer Two. Kotlin has an excellent way of meeting this expectation, as illustrated below.

Android Implementation
In Android, we meet the expectations using SharedPreferences.

[2.2.4] Android Persistence (Implementation)

iOS Implementation
In iOS, we meet the expectations using NSUserDefaults.

[2.2.4] iOS Persistence (Implementation)

2.3 UI — Layer Three

To make sense of how the whole Library works, we’ll look at a sample app that makes use of the Library.

Note: The more code we have in Layer One than in the other two layers, the easier it is to maintain the codebase. The fixes our improvements applied in Layer One is available to all targets. Our goal in coding all of these layers is to get as much as possible into Layer One.

3. Building App using the Library

3.1 Dependencies

3.1.1 Android Dependency

[3.1.1] build.gradle

3.1.2 iOS Dependency

[3.1.2] Podfile

3.2 Configure the App

  • Visibility
  • Text Size
  • Current Text
  • Text Color

3.2.1 Android Configuration
The configuration for our sample project is expressed in Android as follows.

[3.2.1] Android App Configuration (Full Snippet)

3.2.2 iOS Configuration
The configuration for our sample project is expressed in iOS as follows.

[3.2.2] iOS App Configuration (Full Snippet)

3.3 Initialise Configuration

3.3.1 Initialise Android with UI
For Android, we’ll use the context in Layer Two of our Library for getting a handle on shared preference. On the other hand, we use the Intent on Layer Three of our Library to launch the start activity after we have updated the configuration.

[3.3.1] Initialise Android with UI

3.3.2 Initialise iOS with UI
For iOS, we’ll use the group name in Layer Two of our Library to initialise the NSUserDefaults. On the other hand, we use the controller in Layer Three to navigate to the start of our App after updating the configuration.

[3.3.2] Initialise iOS with UI

3.4 Using current configuration values

3.4.1 Android Usage

[3.4.1] Android Usage

3.4.2 iOS Usage

[3.4.2] iOS Usage

4. Result

Image for post
Image for post
[4] Android Left, iOS Right — MEP & CEP

We launch our App using the CEP. During the launch, we use the DSL to create the UI for the App dynamically. In the illustration below, shows the starting point of the CEP. Each item visible here is an aggregate of configurations that are applied to the App if selected. In the illustration below, we wanted to adjust the premium configuration for our App. Hence we tap on it to edit the configuration further.

Image for post
Image for post
[4] Android Left, iOS Right — Dynamic UI from DSL

Once in PREMIUM configuration, we update the settings as shown below:

Image for post
Image for post
[4] Android Left, iOS Right — Update configuration value

After updating the configuration, launching our MEP by tapping the Launch PREMIUM button or launching the MEP from the Launcher has the updated configuration values.

Image for post
Image for post
[4] Android Left, iOS Right — Load using new configuration

5. Easter Egg

5.1 Android

5.1.1 Dependency
We have to package the Non-UI variant separately in Android because we wanted to avoid placing the CEP on Launcher. CEP is auto-configured when we depend on the UI variant of the Library due to Manifest merge.

[5.1.1] Non-UI Dependency

5.1.2 Initialising Library
You’ll find that the start intent and the context are missing in the initialisation.

[5.1.2] Initialising Library

5.2 iOS Initialising Library

[5.2] Initialising Library

5.3 Output

Image for post
Image for post
[5] No UI Start

We have come to the end of our journey in MultiPlatform, and before we depart, I have some comments. At the time of this writing, MultiPlatform is experimental. Before it stabilises, the implementation shown here allows me to use the code in test environments and gain the confidence to extend its use further.

I undertook this journey because I was excited at the proposition of being able to target Multiple Platforms using Kotlin. I was surprised a few times when I was calling top-level Kotlin functions, Kotlin objects and overloaded member functions from Swift. But, fear not for you can find your way easily using the generated framework headers.

At this point, we conclude this material, hope it is useful to someone who takes the Multiplatform adventure. However, when I look at the illustration on [5], I can’t help but ponder the fact:

Though we have our differences, we are the same at the core.

References

  1. https://eladnava.com/publish-a-universal-binary-ios-framework-in-swift-using-cocoapods/
  2. https://github.com/bintray/gradle-bintray-plugin
  3. https://developer.apple.com/xcode/swiftui/
  4. https://developer.android.com/jetpack/compose
  5. https://kotlinlang.org/docs/reference/platform-specific-declarations.html
  6. https://kotlinlang.org/docs/tutorials/native/apple-framework.html
  7. https://kotlinlang.org/docs/reference/type-safe-builders.html
  8. https://developer.android.com/training/data-storage/shared-preferences
  9. https://developer.apple.com/documentation/foundation/nsuserdefaults
  10. https://github.com/russhwolf/multiplatform-settings
  11. https://github.com/Kotlin/kotlinx.serialization

Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

Sign up for Best Stories

By Dev Genius

The best stories sent monthly to your email. Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Thanks to Nivin Anton

Julius Canute

Written by

I am a Mobile Developer interested in learning and sharing new ideas. Also, I 💖 to architect and code.

Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

Julius Canute

Written by

I am a Mobile Developer interested in learning and sharing new ideas. Also, I 💖 to architect and code.

Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store