forbestheatreartsoxford.com

Integrating Platform-Specific Libraries in Kotlin Multiplatform Mobile

Written on

Chapter 1: Introduction

In native application development, developers often have access to a vast array of libraries and tools tailored to their specific requirements. However, the situation changes when moving to cross-platform technologies, where the selection of libraries may not always keep pace with the latest innovations or meet specialized needs. This can create difficulties for Kotlin Multiplatform Mobile (KMM) developers who wish to utilize platform-specific features in their applications.

This article addresses a frequent challenge encountered by KMM developers: integrating a crucial platform-specific library that isn't natively compatible with KMM. We will specifically focus on how to incorporate a database library that is available for both Android and iOS native development but lacks direct support within the KMM framework.

By following a detailed step-by-step guide, we will demonstrate how to create a wrapper around the platform-specific database library, enabling its smooth integration into a KMM project. By the conclusion, you will possess the knowledge and tools necessary to connect platform-specific libraries with KMM, allowing you to fully exploit the advantages of cross-platform development without sacrificing functionality or performance. Let’s get started!

Chapter 2: Problem Statement

In this project, we will create a simple counter application featuring a user interface (UI) built with Compose Multiplatform. The app will display a counter at the top, along with two buttons below: one to reset the counter and another to increment it.

What distinguishes this project is the use of the ObjectBox database, a robust and efficient solution for both Android and iOS. The counter's value will be linked to entities stored in the database, ensuring a consistent and synchronized experience across devices. By merging Compose Multiplatform for the UI with ObjectBox for data management, we will illustrate the effective integration of platform-specific technologies within a KMM project.

Note: The UI design may not be visually appealing, but that is not our primary focus at this point.

Chapter 3: Prerequisites

Before we dive into implementation, make sure your Kotlin Multiplatform Mobile (KMM) project is properly set up with the necessary configurations:

  • KMM Application Setup: Initiate a KMM project with the required structure and settings for both Android and iOS platforms, ensuring effective code sharing.
  • Compose Multiplatform for UI: Use Compose Multiplatform to create the UI for both Android and iOS. This involves defining UI components and layouts with Jetpack Compose for Android and SwiftUI for iOS within the shared Kotlin codebase.
  • Cocoapods Dependency Manager: For iOS integration, configure your KMM project to utilize Cocoapods as the dependency manager, allowing smooth integration of iOS-specific libraries and frameworks into your KMM application.

Step-by-Step Guide for Setting Up Dependencies

Step 1: Modify gradle.properties

Insert the following parameters in your gradle.properties file:

# Kotlin

kotlin.code.style=official

# Android

android.useAndroidX=true

android.nonTransitiveRClass=true

kotlin.mpp.enableCInteropCommonization=true

kotlin.mpp.androidSourceSetLayoutVersion=2

org.jetbrains.compose.experimental.uikit.enabled=true

Step 2: Configure project-level build.gradle.kts

In your project-level build.gradle.kts file, adjust the buildscript block:

buildscript {

val objectboxVersion by extra("3.8.0") // For KTS build scripts

repositories {

// Add your repositories here

}

dependencies {

classpath("io.objectbox:objectbox-gradle-plugin:$objectboxVersion")

}

}

Step 3: Configure shared build.gradle.kts

In the shared build.gradle.kts file, apply the ObjectBox plugin and set up Cocoapods:

plugins {

id("io.objectbox")

}

kotlin {

cocoapods {

summary = "Some description for the Shared Module"

homepage = "Link to the Shared Module homepage"

version = "1.0"

ios.deploymentTarget = "16.0"

podfile = project.file("../iosApp/Podfile")

name = "MyCocoaPod"

extraSpecAttributes["swift_version"] = ""5.0""

framework {

baseName = "shared"

isStatic = true

embedBitcode(BitcodeEmbeddingMode.BITCODE)

}

pod("ObjectBox")

}

}

Step 4: Configure androidApp build.gradle.kts

In the androidApp module's build.gradle.kts, apply the ObjectBox plugin:

plugins {

id("io.objectbox")

}

Step 5: Add ObjectBox dependency in iOS App Podfile

Include pod 'ObjectBox' in your iOS App's Podfile to add the ObjectBox dependency for iOS.

By completing these steps, you will set up the essential dependencies for integrating the ObjectBox database into your KMM project. If you need more clarification or help with any of these steps, feel free to ask!

Sync your KMM project, then navigate to the iosApp directory and run pod install to set up Cocoapods dependencies. Test your project on both Android and iOS devices or emulators to ensure the counter works correctly.

Step 6: Define Common Database Interface

In the shared module of your KMM project, create two Kotlin files, ObjectBoxLocalStore.kt and ObjectBoxObject.kt, within the commonMain source set. The ObjectBoxLocalStore file will establish the contract for database operations, including methods for counting, removing, inserting, and fetching records. The ObjectBoxObject will serve as the database access object.

interface ObjectBoxLocalStore {

fun count(): Long

fun removeAll()

fun put(entity: ObjectBoxObject)

fun all(): List

}

interface ObjectBoxObject

Step 7: Define Common Data Object Interface

In addition to the database interfaces, we will create a common data object interface, IExampleEntity, within the shared module's commonMain source set. This interface will represent our data object stored in the database, allowing for platform-specific implementations on Android and iOS.

interface IExampleEntity {

var string: String

}

Step 8: Define Composable Function

Next, we will define a composable function, App, that will use the entity within our KMM project. This function will be responsible for displaying UI components and interacting with the database through the provided ObjectBoxLocalStore and entityDataSource.

@Composable

fun App(

objectBoxLocalStore: ObjectBoxLocalStore,

entityDataSource: (string: String) -> ObjectBoxObject

) {

var count by remember {

mutableIntStateOf(objectBoxLocalStore.count().toInt())

}

Column(

verticalArrangement = Arrangement.Center,

horizontalAlignment = Alignment.CenterHorizontally,

modifier = Modifier.fillMaxSize()

) {

Text(" $count", style = TextStyle(fontSize = 30.sp))

Spacer(Modifier.height(10.dp))

Row {

Button(

onClick = {

objectBoxLocalStore.removeAll()

count = objectBoxLocalStore.count().toInt()

},

colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray)

) {

Text("Button 1", modifier = Modifier.padding(8.dp))

}

Spacer(Modifier.width(10.dp))

Button(

onClick = {

objectBoxLocalStore.put(entityDataSource(getRandomString()))

count = objectBoxLocalStore.count().toInt()

},

colors = ButtonDefaults.buttonColors(backgroundColor = Color.Cyan)

) {

Text("Button 2", modifier = Modifier.padding(8.dp))

}

}

}

}

Step 9: Implement ObjectBoxLocalStore in androidApp

In the androidApp module of our KMM project, we will implement the ObjectBoxLocalStore interface for Android. While we won't go into the detailed implementation of ObjectBox itself, we will create a wrapper that integrates seamlessly with our KMM application.

class ObjectBoxLocalStoreAndroidImpl(entityClass: KClass): ObjectBoxLocalStore {

private val entityBox: Box = MyApplication.getInstance().boxStore.boxFor(entityClass.java)

override fun all(): List = entityBox.all

override fun count(): Long = entityBox.count()

override fun removeAll() {

entityBox.removeAll()

}

override fun put(entity: ObjectBoxObject) {

val androidEntity = entity as ObjectBoxObjectAndroid

entityBox.put(androidEntity.entity)

}

}

class ObjectBoxObjectAndroid(box: E): ObjectBoxObject {

val entity: E

init {

entity = box

}

}

Step 10: Implementing Entity in androidApp

In the androidApp module, we will create an entity class, ExampleEntity, that implements the IExampleEntity interface. This entity class will be marked with @Entity to designate it as an ObjectBox entity.

@Entity

data class ExampleEntity(

@Id(assignable = false)

var id: Long = 0,

override var string: String = ""

): IExampleEntity

Step 11: Creating iOS App

To begin integrating our iOS app in the shared module of our KMM project, we will use the iosMain source set. Since we want to use a Swift class from the iOS app in our shared module, we will create a MainViewController in the App.ios.kt file under the shared > iosApp directory.

fun MainViewController(

objectBoxLocalStore: ObjectBoxLocalStore,

entityDataSource: (string: String) -> ObjectBoxObject

) = ComposeUIViewController {

App(objectBoxLocalStore, entityDataSource)

}

Step 12: Swift Implementation for the ObjectBox Wrapper in iOS

Next, we will implement the ObjectBox wrapper in iOS using Swift. Open the Xcode project generated for your iOS app and create a new file named ObjectBoxStore.swift. Paste the following implementation into this file:

import Foundation

import shared

import ObjectBox

class ObjectBoxLocalStoreIOSImpl: ObjectBoxLocalStore where T : ObjectBox.EntityInspectable, T : ObjectBox.__EntityRelatable, T == T.EntityBindingType.EntityType {

var store: Store = ObjectBoxStoreInstance.instance

var exampleEntityBox: Box

init(_ entityType: T.Type) {

exampleEntityBox = store.box(for: entityType)

}

func all() -> [Any] {

try! exampleEntityBox.all()

}

func put(entity: ObjectBoxObject) {

guard let iosEntity = entity as? ObjectBoxObjectIos else {

print("Error: Unable to cast entity to ObjectBoxObjectIos")

return

}

try! exampleEntityBox.put(iosEntity.entity)

}

func count() -> Int64 {

return try! exampleEntityBox.count().int64Value

}

func removeAll() {

try! exampleEntityBox.removeAll()

}

}

class ObjectBoxObjectIos: ObjectBoxObject where E : ObjectBox.EntityInspectable, E : ObjectBox.__EntityRelatable, E == E.EntityBindingType.EntityType {

let entity: Box.EntityType

init(_ entity: Box.EntityType){

self.entity = entity

}

}

Step 13: Creating an Instance of the ObjectBox Store in Our iOS App

import Foundation

import ObjectBox

class ObjectBoxStoreInstance {

private let store: Store

static let instance = ObjectBoxStoreInstance().store

private init () {

let databaseName = "dbName"

let appSupport = try? FileManager.default.url(for: .applicationSupportDirectory,

in: .userDomainMask,

appropriateFor: nil,

create: true)

.appendingPathComponent(Bundle.main.bundleIdentifier!)

let directory = appSupport!.appendingPathComponent(databaseName)

try? FileManager.default.createDirectory(at: directory,

withIntermediateDirectories: true,

attributes: nil)

store = try! Store(directoryPath: directory.path)

}

}

Step 14: Creating the ExampleEntity Class in Our iOS App

In our iOS app, we will define an ExampleEntity class that implements the IExampleEntity interface from the shared module. Annotating the class with // objectbox:entity ensures compatibility with ObjectBox.

import ObjectBox

//objectbox:entity

class ExampleEntity: IExampleEntity {

var id: Id = 0

var string: String

required init() {

self.string = ""

self.id = 0

}

convenience init(string: String) {

self.init()

self.string = string

}

}

Step 15: Initializing ObjectBox in the iOS App and Calling Our Content View

@main

struct iOSApp: App {

public let store: ObjectBoxLocalStoreIOSImpl

init() {

store = ObjectBoxLocalStoreIOSImpl(ExampleEntity.self)

}

var body: some Scene {

WindowGroup {

ContentView()

.environmentObject(store)

}

}

}

Step 16: Calling MainViewController from App.ios.kt in ContentView

import Foundation

import SwiftUI

import shared

struct ContentView: View {

let greet = Greeting().greet()

var body: some View {

MainViewController(objectBoxLocalStore: store) { string in

ObjectBoxObjectIos(ExampleEntity(string: string))

}

}

}

struct ContentView_Previews: PreviewProvider {

static var previews: some View {

ContentView()

}

}

With this setup, you can run the iOS app and observe the counter functionality, which mirrors that of the Android app.

Chapter 4: Future Enhancements

While the initial setup may seem extensive, it's essential to remember that these tasks are often one-time configurations. We can also simplify the process and reduce boilerplate code by using dependency injection frameworks like Koin. By utilizing Koin, we can effortlessly inject iOS and Android implementations into our shared module.

Additionally, we could consider employing Kotlin Symbol Processing (KSP) for code generation, particularly for repetitive tasks such as creating entity implementations in both Android and iOS whenever a new entity is introduced. Stay tuned for an upcoming article where we will explore these advanced techniques. Follow me to keep updated on the latest advancements!

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

Essential Frameworks Every Programmer Should Master

Discover key frameworks that can enhance your programming skills and career opportunities.

Essential Programming Skills for Data Engineers to Boost Earnings

Explore key programming skills for Data Engineers to enhance market value and salary.

Innovative Alternatives to the iPad Pro 12.9 Keyboard Experience

Explore alternatives to the iPad Pro 12.9 Magic Keyboard, comparing functionality, design, and usability of various keyboard options.

Embracing Mondays: A Fresh Perspective on the Workweek

Discover how to change your outlook on Mondays and find excitement in your work life, leading to greater fulfillment.

Don't Allow Fear to Halt Your Progress: Keep Moving Forward

Overcome fear and maintain your momentum towards personal growth by confronting your internal struggles.

# Exploring the Nature of Creativity and AI's Role in Art

This article delves into the essence of creativity and the role of AI in enhancing artistic expression.

Exploring the Role of Dopamine in Consciousness and Awareness

This article delves into how dopamine influences consciousness, its implications for disorders, and the potential for new treatments.

Goodbye to

A reflective farewell to