Scanbot SDK has been acquired by Apryse! Learn more

Learn more
Skip to content

How to build a Kotlin Multiplatform Barcode & QR Code Scanner

Yurii May 26, 2026 11 mins read
Kotlin Multiplatform Barcode Scanner SDK

In this tutorial, you’ll learn how to create a cross-platform Android and iOS app for scanning barcodes and QR codes using Kotlin Multiplatform for the shared codebase and Compose Multiplatform for the UI.

Scanning a single QR code with the Kotlin Multiplatform QR Code & Barcode Scanner app
Scanning a single QR code
Scanning multiple barcodes with the Kotlin Multiplatform QR Code & Barcode Scanner app
Scanning multiple barcodes with the AR overlay

To achieve this, you’ll follow these steps:

  1. Preparing the project
  2. Configuring your project for iOS and Android
  3. Implementing the barcode scanner

Make sure you have the latest version of Android Studio installed.

Step 1: Prepare the project

1. Creating a Kotlin Multiplatform project

If you haven’t worked with Kotlin Multiplatform before, set up your development environment as described in the Kotlin Multiplatform quickstart.

Next, create and download a Kotlin Multiplatform project using the Kotlin Multiplatform Wizard. Choose Android and iOS as platforms and select “Share UI via Compose Multiplatform UI framework“, then hit the download button.

Creating a new Kotlin Multiplatform project with the Wizard

After extracting the project, open it in Android Studio. The structure when opening the Project view will look like this:

The Kotlin Multiplatform project structure

2. Adding the SDK dependency

The SDK is distributed via a private Maven repository. Add the repository URLs to settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        // ...
        mavenCentral()

        // Add the repositories here:
        maven(url = "https://nexus.scanbot.io/nexus/content/repositories/releases/")
        maven(url = "https://nexus.scanbot.io/nexus/content/repositories/snapshots/")
    }
}

Then add the SDK dependency in composeApp/build.gradle.kts:

commonMain.dependencies {
      // ...
    implementation("io.scanbot:kmp-barcode-sdk:8.0.0") // or higher
}

💡 This tutorial uses SDK version 8.0.0. You can find the latest version in the changelog.

To ensure full compatibility with Kotlin Multiplatform, check that your jvmTarget in composeApp/build.gradle.kts is at least 17.

compilerOptions {
    jvmTarget.set(JvmTarget.JVM_17)
}

Your android-minSdk should also be 26 or higher. You can check this in gradle/libs.versions.toml.

[versions]
# ...
android-minSdk = "26"

Step 2: Configure your project for iOS and Android

1. iOS integration

The Scanbot iOS native binaries must be made available to the Kotlin/Native compiler.

You can do this in two ways:

  • via Swift Package Manager (SPM),
  • by directly embedding the XCFramework.

Option A: Swift Package Manager (SPM)

This approach uses the spm4Kmp Gradle plugin to pull the Scanbot iOS SDK directly from its SPM repository as part of your Gradle build. No Xcode configuration needed.

💡 JetBrains provides an official SPM integration API built into the Kotlin Multiplatform toolchain. However, at the time of writing this article, it is still experimental and requires at least Kotlin 2.4.0-Beta1. If your project is already on Kotlin 2.4.0-Beta1 or later and you’re comfortable with experimental APIs, the official approach is worth exploring. For stable production setups, the spm4Kmp plugin used in this guide is the recommended alternative.

In composeApp/build.gradle.kts, apply the plugin at the top of the file:

plugins {
    id("io.github.frankois944.spmForKmp") version "1.4.8" // or higher
}

At the top of app/build.gradle.kts, add the opt-in imports required by the plugin:

@file:OptIn(ExperimentalSpmForKmpFeature::class)

import io.github.frankois944.spmForKmp.swiftPackageConfig
import io.github.frankois944.spmForKmp.utils.ExperimentalSpmForKmpFeature

Replace (or update) your existing iOS target configuration with the following:

listOf(
    iosArm64(),
    iosSimulatorArm64()
).forEach { iosTarget ->
    iosTarget.binaries.framework {
        baseName = "ComposeApp"
        isStatic = true
    }

    iosTarget.swiftPackageConfig {
        minIos = "13.0"
        dependency {
            remotePackageVersion(
                url = uri("https://github.com/doo/scanbot-barcode-scanner-sdk-ios-spm.git"),
                version = "8.0.1", // or higher
                products = {
                    add("ScanbotBarcodeScannerSDK", exportToKotlin = true)
                },
            )
        }
    }
}

💡 You can find the latest SDK version in the Scanbot iOS SDK SPM repository.

Option B — XCFramework (Direct Embedding)

This approach gives you full control over the binary and is useful when SPM is unavailable or restricted.

Download and unzip the framework

Download the XCFramework directly from the Scanbot SDK distribution server using the latest SDK version.

https://download.scanbot.io/barcode-scanner-sdk/ios/xcframeworks/scanbot-ios-barcode-scanner-sdk-xcframework-8.0.1.zip

Once downloaded, unzip the archive:

unzip scanbot-ios-barcode-scanner-sdk-xcframework-{version}.zip

In the root of your project, create a scanbot_sdk folder and move the extracted framework into it:

project-root/
└── scanbot_sdk/
    └── ScanbotBarcodeScannerSDK.xcframework
Create the cinterop definition file

Kotlin/Native uses a .def file to describe how to interface with a native framework. Inside the scanbot_sdk folder, create a file named ScanbotBarcodeScannerSDK.def with the following content:

language = Objective-C
modules = ScanbotBarcodeScannerSDK
package = ScanbotBarcodeScannerSDK

This tells the Kotlin compiler that the framework exposes Objective-C modules and makes them accessible under the ScanbotBarcodeScannerSDK package in your shared Kotlin code.

Configure app/build.gradle.kts

Update app/build.gradle.kts to wire up the XCFramework slices for each iOS target:

val xcfRoot    = rootDir.resolve("scanbot_sdk/ScanbotBarcodeScannerSDK.xcframework")
val xcfDefFile = rootDir.resolve("scanbot_sdk/ScanbotBarcodeScannerSDK.def")

val xcfSlices = mapOf(
    "iosArm64"          to xcfRoot.resolve("ios-arm64"),
    "iosSimulatorArm64" to xcfRoot.resolve("ios-arm64_x86_64-simulator"),
)

listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->
    val slicePath = xcfSlices.getValue(target.name).absolutePath

    target.compilations.getByName("main") {
        cinterops.create("ScanbotBarcodeScannerSDK") {
            definitionFile.set(xcfDefFile)
            compilerOpts("-framework", "ScanbotBarcodeScannerSDK", "-F$slicePath")
            extraOpts += listOf("-compiler-option", "-fmodules")
        }
    }

    target.binaries.all {
        linkerOpts("-framework", "ScanbotBarcodeScannerSDK", "-F$slicePath")
    }

    target.binaries.framework {
        baseName = "ComposeApp"
        isStatic = true
    }
}

Some key points about this configuration:

  • xcfSlices maps each Kotlin/Native target to the correct architecture slice: iosArm64ios-arm64 (real devices), iosSimulatorArm64ios-arm64_x86_64-simulator (Apple Silicon simulators).
  • cinterops.create registers the cinterop binding using the .def file. The F$slicePath flag points the compiler to the framework directory for the specific slice.
  • fmodules enables Clang module support, required for Objective-C frameworks to be correctly imported into Kotlin/Native.
  • linkerOpts links the framework at the binary level for each target.

After syncing your Gradle project, the ScanbotBarcodeScannerSDK APIs will be accessible.

2. Android registration

On Android, the SDK requires an additional registration step before any API calls are made.

Register the Activity in composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)

        // Register the Activity here:
        ScanbotSDK.registerActivity(this)
        setContent {
            App()
        }
    }
}

3. Setting the camera permissions

Since the SDK needs to access the device camera to scan barcodes, add the necessary permissions in composeApp/src/androidMain/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />

    <application
    ...

… and in iosApp[project-name]/iosApp/Info.plist:

<key>NSCameraUsageDescription</key>
<string>Please allow camera usage to scan documents.</string>

Step 3: Implement the barcode scanner

1. Initializing the SDK

With the SDK installed on both platforms, the next step is to initialize it in your shared code. The SDK must be initialized once at app startup, typically inside your root @Composable function, and before any scanning UI is launched. For example, in composeApp/src/commonMain/kotlin/App.kt:

import io.scanbot.sdk.kmp.ScanbotSDK
import io.scanbot.sdk.kmp.common.sdk.configuration.SdkConfiguration

const val SCANBOT_SDK_LICENSE_KEY = "" // Replace with your Scanbot SDK license key

@Composable
fun App() {
    MaterialTheme {
        LaunchedEffect(Unit) {
            val config = SdkConfiguration(
                licenseKey = SCANBOT_SDK_LICENSE_KEY,
                loggingEnabled = true, // Set to false in production to avoid leaking scan data to logs
            )

            ScanbotSDK.initialize(config).onSuccess { licenseInfo ->
                println("Scanbot SDK initialized. License status: ${licenseInfo.status}")
            }.onFailure { error ->
                println("Error initializing Scanbot SDK: ${error.message}")
            }
        }
    }
}

The SDK runs without a license key for one minute per session. You can also generate a free trial license.

2. Setting up the main screen

For a simple navigation on the app’s main screen, add two buttons inside the root App() composable in App.kt. The MainScreen is then rendered once initialization is in place.

Each button calls a dedicated scanning function that we’ll implement in the next steps.

@Composable
fun MainScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Kotlin Multiplatform Barcode Scanner",
            style = MaterialTheme.typography.headlineMedium,
            modifier = Modifier.padding(bottom = 32.dp)
        )

        ScanButton("Scan a Single Barcode") {
            startSingleBarcodeScanning(
                onResultHandler = { result -> /* handle result */ },
                onErrorHandler = { error -> /* handle error */ }
            )
        }

        Spacer(modifier = Modifier.height(16.dp))

        ScanButton("Scan Multiple Barcodes") {
            startMultipleBarcodeScanning(
                onResultHandler = { result -> /* handle result */ },
                onErrorHandler = { error -> /* handle error */ }
            )
        }
    }
}

@Composable
fun ScanButton(label: String, onClick: () -> Unit) {
    Button(
        onClick = onClick,
        modifier = Modifier.fillMaxWidth()
    ) {
        Text(label)
    }
}

3. Implementing single-barcode scanning

The SDK’s single-barcode scanning mode reads one barcode and shows a confirmation sheet with the result before proceeding. This example also demonstrates some configuration options.

import io.scanbot.sdk.kmp.barcode.BarcodeFormatCommonConfiguration
import io.scanbot.sdk.kmp.barcode.BarcodeFormats
import io.scanbot.sdk.kmp.ui_v2.barcode.configuration.BarcodeScannerScreenConfiguration
import io.scanbot.sdk.kmp.ui_v2.barcode.configuration.BarcodeScannerUiResult
import io.scanbot.sdk.kmp.ui_v2.barcode.configuration.SingleScanningMode
import io.scanbot.sdk.kmp.ui_v2.common.ScanbotColor

fun startSingleBarcodeScanning(
    onResultHandler: (BarcodeScannerUiResult) -> Unit,
    onErrorHandler: (error: Throwable) -> Unit
) {
    ScanbotSDK.barcode.startScanner(
        configuration = BarcodeScannerScreenConfiguration().apply {

            this.useCase = SingleScanningMode().apply {
                // Enable and configure the confirmation sheet.
                this.confirmationSheetEnabled = true
                this.sheetColor = ScanbotColor("#FFFFFF")

                // Hide/unhide the barcode image.
                this.barcodeImageVisible = true

                // Configure the barcode title of the confirmation sheet.
                this.barcodeTitle.visible = true
                this.barcodeTitle.color = ScanbotColor("#000000")

                // Configure the barcode subtitle of the confirmation sheet.
                this.barcodeSubtitle.visible = true
                this.barcodeSubtitle.color = ScanbotColor("#000000")

                // Configure the cancel button of the confirmation sheet.
                this.cancelButton.text = "Close"
                this.cancelButton.foreground.color = ScanbotColor("#C8193C")
                this.cancelButton.background.fillColor = ScanbotColor("#00000000")

                // Configure the submit button of the confirmation sheet.
                this.submitButton.text = "Submit"
                this.submitButton.foreground.color = ScanbotColor("#FFFFFF")
                this.submitButton.background.fillColor = ScanbotColor("#C8193C")
            }

            // Set the accepted barcode formats.
            this.scannerConfiguration.barcodeFormatConfigurations =
                listOf(BarcodeFormatCommonConfiguration(formats = BarcodeFormats.common))
        },
        onResult = { result ->
            result.onSuccess { onResultHandler(it) }
                  .onFailure { onErrorHandler(it) }
        }
    )
}
Scanning a single QR code with the Kotlin Multiplatform QR Code & Barcode Scanner app

4. Implementing multi-barcode scanning

The multi-barcode scanning mode keeps scanning barcodes and accumulates the results in a sheet at the bottom of the screen. In this example, we’re enabling the SDK’s optional AR Overlay, so users can tap on the barcodes they want to add to the results sheet.

fun startMultipleBarcodeScanning(
    onResultHandler: (BarcodeScannerUiResult) -> Unit,
    onErrorHandler: (error: Throwable) -> Unit
) {
    ScanbotSDK.barcode.startScanner(
        configuration = BarcodeScannerScreenConfiguration().apply {

            this.useCase = MultipleScanningMode().apply {
                // UNIQUE mode ignores duplicate barcodes, keeping the overlay clean.
                this.mode = MultipleBarcodesScanningMode.UNIQUE

                // Show a small collapsed sheet at the bottom.
                this.sheet.mode = SheetMode.COLLAPSED_SHEET
                this.sheet.collapsedVisibleHeight = CollapsedVisibleHeight.SMALL

                // Enable the AR overlay and disable automatic selection,
                // so the user taps a barcode to select it manually.
                this.arOverlay.visible = true
                this.arOverlay.automaticSelectionEnabled = false
            }

            this.scannerConfiguration.barcodeFormatConfigurations =
                listOf(BarcodeFormatCommonConfiguration(formats = BarcodeFormats.common))
        },
        onResult = { result ->
            result.onSuccess { onResultHandler(it) }
                .onFailure { onErrorHandler(it) }
        }
    )
}
Scanning multiple barcodes with the Kotlin Multiplatform QR Code & Barcode Scanner app

Now it’s time to test your app and its barcode scanning functionalities.

You can run both the Android and iOS builds from Android Studio or IntelliJ IDEA – more on how this works in the Compose Multiplatform documentation.

And if you’re in need of some barcodes, we’ve got you covered:

Various barcodes for testing

Conclusion

Congratulations! You’ve successfully built a cross-platform barcode scanning app for Android and iOS using Kotlin Multiplatform.

If this tutorial has piqued your interest in integrating barcode scanning functionalities into your Android or iOS app, make sure to take a look at our SDK’s other neat features in our documentation or run our example project for a more hands-on experience.

Should you have questions about this tutorial or run into any issues, we’re happy to help! Just shoot us an email via tutorial-support@scanbot.io.

FAQ

What’s the difference between Kotlin Multiplatform and Compose Multiplatform?

Kotlin Multiplatform (KMP) enables sharing business logic, data models, and non-UI code across platforms like Android, iOS, desktop, and web. If you want your app to also use a unified UI on every platform instead of native UIs, you can use Compose Multiplatform (CMP) and its shared declarative UI code to complement KMP.

Is there a way to use Kotlin Multiplatform to scan QR codes with a custom UI?

You can build your own QR code scanning UI with Custom UI Components. While this tutorial used the SDK’s Ready-to-Use UI, its Custom UI Components give you an even greater degree of customization.

What’s the relationship between Kotlin Multiplatform and Jetpack Compose?

Kotlin Multiplatform (KMP) and Jetpack Compose are closely integrated technologies for cross-platform development. Jetpack Compose is Google’s declarative UI toolkit designed primarily for native Android apps, similar to what SwiftUI is for iOS apps. JetBrains’ Kotlin Multiplatform integrates Jetpack Compose through Compose Multiplatform, an extension of the Jetpack Compose framework that enables sharing declarative UIs across Android, iOS, desktop, and web platforms.

What are the performance considerations for a KMP QR scanner on mobile devices?

Kotlin Multiplatform offers cross-platform code sharing that comes with slight performance trade-offs, particularly in startup times and CPU usage. Battery drain and real-time scanning speed hinge on efficient camera API integration and frame processing, so using an SDK tailored to the KMP ecosystem is recommended.

Is Kotlin Multiplatform better than Flutter?

Both Kotlin Multiplatform (KMP) and Flutter excel in cross-platform app development, but with different strengths. KMP shines for sharing business logic while using native UIs (or a unified UI via Compose Multiplatform) and is ideal for complex apps, migrations, or deep native integrations, especially if your team knows Kotlin. On the other hand, Flutter enables full code reuse (including pixel-perfect UIs via its Skia engine), rapid prototyping with hot reload, and broad platform support, making it superior for MVPs, consistent designs, or projects with a quick time-to-market.

Related blog posts