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.
- I assume you have familiarity with Android & iOS development.
- 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.
During a fast-paced App Development cycle, we’ll encounter some problems. We’ll go through them briefly and define a goal we wanted to achieve.
In this section, we’ll walk through four scenarios which can create bottlenecks.
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.
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.
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.
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.
Having gone through the concerns mentioned above, let us define our goal, as shown below:
“Given that the App has the necessary code. we wanted to change how the App behaves once deployed on the device with minimal effort.”
Considering our goal, we wanted the flexibility to edit the configuration of our App once deployed. For this, we’ll place a Configuration Entry Point(CEP) in the launcher of our App, that allows us to update the configuration before the Main App is loaded. To illustrate this with an example, the diagram below shows launcher having two entry points to our App, the one shown in Green is the Main Entry Point(MEP) to our App. In contrast, the one shown in yellow allows us to customise the settings before launching the MEP.
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:
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
To have a consistent way of writing the configuration across platforms iOS and Android, we’ll develop DSL’s using Kotlin Lambda Extension. In the implementation of those DSL’s the following attribute are shared.
- 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.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.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.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.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.2 Persistence — Layer Two
Once the configuration change is applied, subsequent starts of the Main App must have the new configuration. To do that the persistence layer has two responsibilities one is to load an existing configuration the other is to save a modified configuration. For those responsibilities, we have Android & iOS implementation written using Kotlin in 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.2 Save Configuration — Layer One
The following snippet illustrates saving an updated configuration.
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.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.
In Android, we meet the expectations using SharedPreferences.
In iOS, we meet the expectations using NSUserDefaults.
2.3 UI — Layer Three
When we launch the App through the CEP, we’ll transform the configuration written in DSL into UI controls corresponding to the platform. We have covered this logic using the native implementation of Android & iOS written in Kotlin & Swift.
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
After developing all three layers of our Library, Consider we have published Android & iOS Libraries to Maven Central and CocoaPods. In the following section, we’ll use the published libraries on our sample project.
To use these libraries natively on the target platform, follow the setup process shown below:
3.1.1 Android Dependency
3.1.2 iOS Dependency
3.2 Configure the App
The sample project has two different configurations for Free and Premium users and contains a view with a text whose attributes we would like to configure before launch. The attributes we are interested in are listed below:
- 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.2 iOS Configuration
The configuration for our sample project is expressed in iOS as follows.
3.3 Initialise Configuration
We can initialise our configuration module during the application start as follows.
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.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.4 Using current configuration values
After initialising our Library, we can obtain the updated configuration values at any point in our App, as shown below. We can use the keys defined in our configuration DSL to acquire the value corresponding to the configuration.
3.4.1 Android Usage
3.4.2 iOS Usage
Shows our App has two entry points the MEP and the CEP. In Android on including the Library, this happens automatically as a result of Manifest merge. In iOS, we create two targets and associate the targets to the same App Group, so updating the settings in one target reflects in the other.
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.
Once in PREMIUM configuration, we update the settings as shown below:
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.
5. Easter Egg
We have an easter egg hidden in Layer One of the Common Code that can be useful at the time of shipping the App where we don’t need Layer Two(Persistence) and Layer Three(UI). It is a read-only configuration useful after we have finalised the configuration values.
Making use of this easter egg in Android is a two-step process.
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.2 Initialising Library
You’ll find that the start intent and the context are missing in the initialisation.
5.2 iOS Initialising Library
You’ll find that the group name and the start controller are missing in the initialisation.
The figure below shows the App start using the DSL without Persistence Layer and UI Layer from the Library.
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 , I can’t help but ponder the fact:
Though we have our differences, we are the same at the core.