Building a Kotlin Multiplatform Mobile SDK for Location-Related Services
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:
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:
- Create a new Kotlin Multiplatform module.
- Define an interface for location-related services.
- Implement the interface for each platform separately.
- Use the implementation in the common code.
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 code
to 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 thebuild.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 thebuild/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
orSPM
to pull the dependencies in the existing project (we are using this approach) - For
cocoapods
integration add the following code in thebuild.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 thepodspec
script ) to the repository and create agit-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! 🙌🙏