How We Integrated Kotlin Multiplatform Into Profi

IceRock Development
IceRock Development
10 min readJun 28, 2021

Hi, guys, it’s IceRock team. So great to get feedback from customers. This time guys from Profi.ru wrote an article. Hope their experience will be useful and inspiring for you.

Hello! My name is Mikhail Ignatov, and I am a team lead at Profi. My team is responsible for client-side mobile apps for Android and iOS. We have been using Kotlin Multiplatform in production since 2019. Let me tell you about why we chose this particular technology, how we integrated it, the key stages that we went through during the process, and the conclusions we reached in the end.

Kotlin Multiplatform

Kotlin Multiplatform allows users to run the same Kotlin code on multiple platforms. In August of 2020, JetBrains introduced Kotlin Multiplatform Mobile (KMM), an SDK that facilitates the use of shared code across Android and iOS. The purpose of the technology is to extract business logic while the UI layer remains native, which is good for a better user experience and the look and feel of the apps.

Why We Chose Kotlin Multiplatform

We studied various cross-platform technologies. For example, React Native and Flutter allow to program and develop a feature for both platforms at the same time, but they narrow the choice of programming language and libraries. We chose Kotlin Multiplatform for three reasons.

Ease of integration

The common code written in Kotlin can be injected with minimal effort into a finished application. It then compiles into platform-familiar libraries. In the case of Android, it is a jar or aar library, while for iOS, it is a Universal Framework. The connection process and further work does not differ from interacting with any native library.

The Kotlin language’s syntax is close to Swift

The similarity of the language lowers the entry barrier for iOS developers. Both languages ​​share a similar ideology focused on development speed and usability. Anyone on the team can understand what is going on in the common code and adjust it if needed.

Developer Resource Waste Minimization

The business logic of our applications is the same. More than 70% of the code is not related to the platform it runs on. We request data from the server, transform it, cache it, and prepare it for display. Without code sharing, we would have to write the same code in Kotlin for Android and in Swift for iOS. There are differences only in the design side due to the difference in the UX on mobile platforms and the interaction with the system in terms of requests to various peripherals like cameras, geolocation, galleries, notifications, and so on.

The Implementation Process

We decided to act thoughtfully and methodically. We started by tackling simple problems, gradually increasing the complexity. We reflected on every stage, assessing the costs, the results and the consequences. Here is a description of the three main steps we have taken.

Step 1. The first line in the shared code

The first task is to make common API request strings to avoid differences in the requested data structures on the two platforms.

Data exchange with the server was implemented via GraphQL. The code request consisted of a multiline string made up of five lines, sometimes under a hundred. When sending such a volume of code, the backend will have to spend time parsing the structure. On the other hand, there is a need to control the requested data during the code review process and the validation of production requests. Therefore, we “train” the server for new requests before release. This allows hashes to be used instead of strings.

Previously, we did the “training” of the server manually and separately for each platform. This took up a lot of resources and increased the likelihood of errors. For example, it is easy to forget to “train” a request on one of the platforms and corrupt the application as a result.

We decided to move several requests into a shared code. A multiplatform shared module was developed for the purpose in the Android project. We moved the query strings into it and wrapped the object in singleton classes, and then called the methods of these classes in client applications. Fun fact — an iOS developer suggested using KMM.

The first line in the shared code

package ru.profi.shared.queries.client.city  /** 
*
City search query by [Params.term]
*/
object GeoSelectorWarpQuery : WarpQuery<Params> {
override val hash: String? = "\$GQLID{c9d4adbb7b9ef49fc044064b9a3e662b}" override val dirtyQuery = listOf("\$term").let { (term) ->
"""
query geoSelector($term: String) {
suggestions: simpleGeoSelector(term: $term, first: 100) {
edges {
node {
name
geoCityId
regionName
hostname
countryId
}
}
}
}
"""
}.trimIndent()
}

Application in an Android project

override fun getQuery() = GeoSelectorWarpQuery.getQuery()

Application in an iOS project

import KotlinComponentsstruct GraphQLWarpRequests {
static let GeoSelectorWarpQuery = GeoSelectorWarpQuery()
...
}
let model = GraphQLRequestModel(query: GraphQLWarpRequests.GeoSelectorWarpQuery.getQuery(), variables: variables)

The query structures were relocated into a single repository. In the next release, the shared library was connected on both platforms and the system started operating smoothly. The app size on iOS was increased by only 0.8 MB. As a result, the transfer of requests into the shared code halved the number of approaches needed for the “training”.

The manual learning problem was solved with a utilitarian library consisting of several classes written in Kotlin. It finds untrained requests in the code, generates and then sends new hashes via a pull request to the backend repository. This allows us to avoid wasting time on training, as it is fully automated.

In this step, we built a framework for the shared code on Kotlin Multiplatform, allowing us to move on to more important tasks.

Step 2. Creating a multiplatform SDK

At one point, the company decided to create its own in-house analytics tool based on Clickhouse. An API for applications was created for this purpose on the backend side. All my team had to do was send events. In order not to interfere with the work of the main functionality and avoid losing events if the user did not have a network, it was necessary to learn how to cache, group batches of events, and send them with a lower priority than requests for the main functionality.

We decided to write the module in the shared code. We selected the Ktor network client for this purpose, as it suited us perfectly.

When a network is lacking, the events must be saved until the next communication session. We chose SQLDelight for the purpose — a multiplatform library for a native database.

For asynchronous operations, we used kotlinx.coroutines and kotlinx.serialization for the serialization and deserialization processes.

To increase the reliability of the code, the functionality of the module was ensured with unit tests, since they can be run on different platforms with ease.

There were no problems with integrating the application on Android, but there were crashes at the start on iOS. The stack trace did not get us very close to our goal on the Xcode console and Firebase Crashlytics logs. But the failing element inside the shared code was evident to us.

To get a stack trace, we included the CrashKiOS library from the Touchlab studio. When creating the coroutines, we added the CoroutineExceptionHandler, which identifies exceptions during their execution.

It turned out that the event was dispatched after the coroutine’s scope was canceled. And this was the reason for the failures. It turned out that we had incorrectly canceled CoroutineScope in the application lifecycle.

Kotlin Multiplatform made it possible to combine the responsibility for sending and storing analytical events into a single module. As a result, we built a full-fledged SDK in a shared code.

Step 3. Transfer of the business logic from the Android application to the multiplatform

I am certain that many projects have code that they want to bypass, since it is difficult to read, regularly causes subtle problems with the product, and was written so long ago that its authors are no longer with the company.

The iOS app had this code in the chat business logic module. This was our pain point, as it became more expensive to add new functionality, since the code was written in Objective-C with an outdated and complex architecture. We felt that the developers were reluctant to take on chat tasks.

In an Android application, the chat business logic has recently been rewritten to Kotlin. Therefore, we decided to try to move the existing module into the shared code and adapt it to iOS.

The developers from IceRock helped us a lot. They have long embarked on the path of multiplatform operations, promoting KMM and developing the community. And together, we drew up a plan.

Configure Kotlin Multiplatform support in the gradle module.

Create a module, connect plugins, configure the sourceSets and dependencies.

Move platform independent classes to commonMain.

Move all the elements of JVM and Android independent to commonMain. This is a repository for common code that has no platform dependencies.

Replace JVM / Android libraries with their multiplatform counterparts.

Move from org.json to kotlinx.serialization and from JodaTime to klock. Some parts had to be moved into the platform-dependent code in the form of expect/actual.

Move the JVM-dependent code that requires changes to commonMain.

For example, replace JVM IOException with kotlin.Exception and ConcurrentHashMap with Stately.

Move the Android dependent code that requires changes to commonMain.

The only Android SDK dependency was the Service component, which works with WebSocket, since there is no stable multiplatform analogue on Kotlin yet.

We decided to leave the native implementations in the application and connect them through the SocketService interface.

The SocketService interface

interface SocketService {      /**
*
Connect on a socket to [chatUrl]. All events from the socket must be sent to [callback]
*/
fun connect(chatUrl: String, callback: (SocketEvent) -> Unit)
/**
*
Disconnect from the current socket connection
*/
fun disconnect()
/**
*
Send message [msg] on current socket connection
*/
fun send(msg: String)
}

Making the API module usable for both platforms.

Since it is impossible to detect runtime exceptions from Kotlin on iOS, we decided to handle them inside the SDK and add callback onError to the interface methods. We had to slightly redesign the interface for interacting with client applications.

When transferring the code to the multiplatform, we formulated an algorithm for migrating the modules with the business logic into a shared code. We use it to extract other modules.

IceRock.dev’s plan helped us a lot with moving faster and with greater confidence. We will surely continue to contact them as needed and share our development experience.

What We Can Conclude

Kotlin Multiplatform helped create a single source of business logic in Profi client applications. The UI and UX were left native for the user. With the right design of the interface for interacting with shared code, the change and extension of business logic takes place from a single source, and client applications just need to support this approach.

We reduced waste of resources. When migrating modules to Kotlin Multiplatform, we noticed how much development time was saved, as the chat module on iOS did not have to be refactored. Instead, we moved the solution from the Android project into the shared code and adapted it for iOS. It costs less than writing chats from scratch.

The developers quickly got used to the process. The only new elements for Android developers were the multiplatform libraries and module build-script customization. The rest was familiar territory. It was easy for iOS developers to understand the syntax of the language, but they had to dig into the assembly using Gradle. And now, each of them has already solved at least one problem in the shared code.

The main disadvantage of the technology is the build time for iOS. For example, when we were looking for the reason for the application’s crash, we often had to rebuild the shared code for iOS. This is barely noticable when publishing shared modules. With the release of new versions of Kotlin, the build speed increases, we hope that future development cycles will be more convenient.

We did encounter some problems as a result of our ignorance. When we started on the project, there was very little information on the implementation of KMM, so we had to solve all of the issues ourselves. The Kotlin Multiplatform community is growing rapidly with more articles and reports appearing at conferences and meetups. There are channels in Slack, libraries for Kotlin Multiplatform, and so on, plenty of information sources available.

It Was Worth It In The End

The first steps were difficult, because the technology was new. At first, it seemed easier to use native solutions on libraries known to the team than to understand new ones. But we understood that things would unravel faster later on. And we were right. We can say that the speed of development using shared code and native projects is fairly equal.

We already have 10 common modules of varying complexity, and we are continuing to migrate the business logic into a shared code. I am sure that Kotlin Multiplatform Mobile is ready to conquer the world of mobile application development.

Read more about KMM in our blog.

--

--

IceRock Development
IceRock Development

📱IOS&Android Mobile Application Development 🔝Kotlin Multiplatform technology experts https://icerockdev.com/