Building a Kotlin Multiplatform Mobile SDK for Location-Related Services

Inder singh
Rapido Labs
Published in
8 min readMar 1, 2023

Kotlin Multiplatform Mobile (KMM) has been gaining popularity among mobile app developers. The KMM SDK allows developers to write business logic and share it between different platforms such as Android, iOS, and the web. In this article, we’ll explore how I developed a Kotlin Multiplatform Mobile SDK for location-related services for Rapido.

Background

Rapido has been using native mobile development for several years. We have separate codebases for Android and iOS; maintaining them is quite challenging. We also noticed that the code for location-related services was almost identical for both platforms, with a few differences in implementation. We wanted to find a way to share this code between platforms and minimize the overhead of maintaining two separate codebases.

Kotlin Multiplatform Mobile

Kotlin Multiplatform Mobile was a perfect solution to our problem. KMM allows developers to write business logic using Kotlin and share it between different platforms. The code can be compiled into platform-specific code, allowing it to run natively on each platform.

Architecture: The SDK is built using Kotlin’s multiplatform capabilities, with shared code for location data retrieval and platform-specific implementations for location data. Our SDK uses a Repository Pattern for data retrieval, with the repository layer handling data retrieval from different sources and abstracting it from the presentation layer. We have also implemented a Network Service Layer that handles network calls to our internal backend service and returns the data to the repository layer.

The SDK’s architecture can be visualized in the following diagram:

High level flow diagram of the location sdk
Location Sdk flow diagram

The SDK provided the following functionalities:

  • Get current location: This functionality was used to get the user’s current location.
  • Get the last known location: This functionality was used to get the user's last known location.
  • Get address from location: This functionality was used to get the address from the user’s location(Reverse Geocode).
  • Get location from the address or place id: This functionality was used to get the location from an address or place id(Geocode).
  • Places Autocomplete: This functionality was used to get the suggestion of address based on the user search
  • Location Selection: This functionality was used to provide the list of locations near a provided location in request the returned locations can be used as pickup points and sticky locations depending on various use cases
  • Many more features in the pipeline 🏗️

Implementation

We implemented the SDK using the following steps:

  1. Create a new Kotlin Multiplatform module.
  2. Define an interface for location-related services.
  3. Implement the interface for each platform separately.
  4. Use the implementation in the common code.

From official doc

Application that needs to access platform-specific APIs that implement the required functionality, use the Kotlin mechanism of expected and actual declarations.

With this mechanism, a common source set defines an expected declaration, and platform source sets must provide the actual declaration that corresponds to the expected declaration.

Let's dive into the sample code of the user's current location implementation

In the common section, we have to define the abstraction

// Define a data class to store latitude and longitude coordinates
data class Location(val latitude: Double, val longitude: Double)

// Define a common class for location service
expect class LocationService() {
suspend fun getCurrentLocation(): Location
}

In the Android and iOS sections, we have to provide the implementation like this

// Implement the LocationService in Android
actual class LocationService {

// Initialize the FusedLocationProviderClient
private val fusedLocationClient by lazy {
LocationServices.getFusedLocationProviderClient(RapidoLocationController.getContext() as Context)
}

// Implement the getCurrentLocation() method
@SuppressLint("MissingPermission") // Assuming location permission check is already handled
actual suspend fun getCurrentLocation(): Location = suspendCoroutine { continuation ->
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
location?.let {
continuation.resume(Location(it.latitude, it.longitude))
} ?: run {
continuation.resumeWithException(Exception("Unable to get current location"))
}
}.addOnFailureListener { e ->
continuation.resumeWithException(e)
}
}
}
// Implement the LocationService in iOS
actual class LocationService {

// Define a native CLLocationManager object
private val locationManager = CLLocationManager()

// Define an atomic reference to store the latest location
private val latestLocation = AtomicReference<Location?>(null)

// Define a custom delegate that extends NSObject and implements CLLocationManagerDelegateProtocol
private class LocationDelegate : NSObject(), CLLocationManagerDelegateProtocol {

// Define a callback to receive location updates
var onLocationUpdate: ((Location?) -> Unit)? = null

override fun locationManager(manager: CLLocationManager, didUpdateLocations: List<*>) {
didUpdateLocations.firstOrNull()?.let {
val location = it as CLLocation
location.coordinate.useContents {
onLocationUpdate?.invoke(Location(latitude, longitude))
}

}
}

override fun locationManager(manager: CLLocationManager, didFailWithError: NSError) {
onLocationUpdate?.invoke(null)
}
}


// Implement the getCurrentLocation() method
actual suspend fun getCurrentLocation(): Location = suspendCoroutine { continuation ->
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.startUpdatingLocation()

// Define a callback to receive location updates
val locationDelegate = LocationDelegate()
locationDelegate.onLocationUpdate = { location ->
locationManager.stopUpdatingLocation()
latestLocation.value = location
if (location != null) {
continuation.resume(location)
} else {
continuation.resumeWithException(Exception("Unable to get current location"))
}
}

// Assign the locationDelegate to the CLLocationManager delegate
locationManager.delegate = locationDelegate
}
}

For Android, we used the Fused Location Provider API provided by Google Play Services to get the user’s location. For iOS, we used the Core Location framework to get the user’s location.

We used the Geocoder API provided by Google Play Services to get the address from the user’s location for Android. For iOS, we used the CLGeocoder class provided by the Core Location framework to get the address from the user’s location.

Testing

We tested the SDK on both Android and iOS platforms. We used JVM-based libraries for testing like mockk ,kotlin(“test”) , and coroutines-test for the common codeto ensure the functionality worked as expected.

Exporting

Exporting a KMM module involves creating a library that can be used in both Android and iOS projects

Exporting a KMM module for Android

Exporting a KMM module for Android requires creating an AAR (Android Archive) library that can be used in Android projects. The following steps outline how to create an AAR library for a KMM module:

  • Create a publish.gradle file, add the following code
apply plugin: 'maven-publish'

def LIB_GROUP_ID = 'com.example.company'
def LIB_ARTIFACT_ID = 'locationsdk'
def LIB_VERSION = '1.0.0'
def LIB_VERSION_SNAPSHOT = '1.0.0-SNAPSHOT'
def USER_NAME = System.getenv("USERNAME")
def PASSWORD = System.getenv("PASSWORD")


android {
publishing {

singleVariant("debug") {
withSourcesJar()
withJavadocJar()
}
singleVariant("release") {

}
}
}


afterEvaluate {
publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/repository_path")
credentials {
username = USER_NAME
password = PASSWORD
}
}
}

publications {
release(MavenPublication) {
from components.release
groupId LIB_GROUP_ID
artifactId LIB_ARTIFACT_ID
version LIB_VERSION
}

debug(MavenPublication) {
from components.debug
groupId LIB_GROUP_ID
artifactId LIB_ARTIFACT_ID
version LIB_VERSION_SNAPSHOT
}
}
}
}
  • In the module’s build.gradle.kts file, add the following code to the android section:
apply(from = "$rootDir/locationsdk/publish.gradle")

This code adds a Maven publication that can be used to generate an AAR library.

  • In the same build.gradle.kts file, add the following code to the repositories section:
repositories {
mavenCentral()
mavenLocal()
}

This code adds the Maven Central and Maven Local repositories, which are required to publish the AAR library.

  • Open the Gradle panel in Android Studio and navigate to the KMM module.
  • Run the “publishToMavenLocal” task. This will publish the module to the local Maven repository, found in the “build/publications/Release/” folder.
  • You can run “publishReleasePublicationToGitHubPackagesRepository” to publish to the GitHub packages. (this can be automated via git-workflows as well)
  • Share the AAR file with other developers or add it to your Android project’s dependencies.

Exporting a KMM module for iOS

Exporting a KMM module for iOS requires creating a framework that can be used in Xcode. The following steps outline how to create a framework for a KMM module.

  • Add the following code to the kotlin block of the build.gradle.kts file
 val xcf = XCFramework()
val iosTargets = listOf(iosX64(), iosArm64(), iosSimulatorArm64())

iosTargets.forEach {
it.binaries.framework {
baseName = "MyFramework"
xcf.add(this)
}
}
  • Run ./gradlew assembleXCFramework in the terminal, this will generate the xcFramework in the build/XCFrameworks folder of the app which will contain both the release and debug framework
  • XCFramworks can be integrated into the existing iOS project as a frameworks, follow this for details
  • Alternatively, you can use cocoapods or SPM to pull the dependencies in the existing project (we are using this approach)
  • For cocoapods integration add the following code in the build.gradle.kts file of the multiplatform module.
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
}

kotlin {
cocoapods {
// Required properties
// Specify the required Pod version here. Otherwise, the Gradle project version is used.
version = "1.0"
summary = "Some description of the module"
homepage = "Link to the repository"

// Optional properties
// Configure the Pod name here instead of changing the Gradle project name
name = "LocationSdkKmm"

framework {
// Required properties
// Framework name configuration. Use this property instead of deprecated 'frameworkName'
baseName = "locationSdk"

// Optional properties
// Specify the framework linking type. It's dynamic by default.
isStatic = false
}

// Maps custom Xcode configuration to NativeBuildType
xcodeConfigurationToNativeBuildType["DEBUG"] = NativeBuildType.DEBUG
xcodeConfigurationToNativeBuildType["RELEASE"] = NativeBuildType.RELEASE
}
}
  • Re-import the project.
  • This will generate the podspec file in the project as below.
Pod::Spec.new do |spec|
spec.name = 'LocationSdkKmm'
spec.version = '1.0.0'
spec.homepage = 'Link to the repository'
spec.source = { :http=> ''}
spec.authors = ''
spec.license = ''
spec.summary = 'Some description of the module'
spec.vendored_frameworks = 'shared/build/XCFrameworks/release/shared.xcframework'
spec.libraries = 'c++'

spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':locationSdk',
'PRODUCT_MODULE_NAME' => ':locationSdk',
}

end
  • Push the code along with the xcframework (it can be automated as well by the podspec script ) to the repository and create a git-tag with release version
  • In the existing iOS project pod file add the dependencies like the code below
  pod 'locationSdk', :git => 'https://github.com/repo_path', :branch => 'master', :tag  => '1.0.0'
  • Run “pod install” in the terminal, it will pull the framework and add it to the existing project

Conclusion

In conclusion, we were able to develop a Kotlin Multiplatform Mobile SDK for location-related services for Rapido. The SDK allowed us to share code between platforms, reducing the overhead of maintaining separate codebases for Android and iOS. KMM proved to be an efficient tool for building a cross-platform mobile SDK. We plan to continue exploring KMM and using it in other areas of our application.

I want to thank Suryakant Sharma & sharanabasappa from the iOS team, who helped in the integration with the iOS app.

That’s it for now, hope it helps! Enjoy, and feel free to leave a comment if something is not clear or if you have questions. Thank you for reading! 🙌🙏

--

--