How to create a Compose Multiplatform iOS and Android application with barcode scanning functionality using the Scanbot SDK

app store

Developing mobile applications for iOS and Android often involves managing different coding environments and languages. UI frameworks like Compose Multiplatform simplify the development process significantly, especially when combined with a software development kit like our Scanbot Barcode Scanner SDK.

For Android developers, Jetpack Compose has become the standard toolkit for creating user interfaces. With its small but powerful codebase, the UI toolkit has revolutionized how Android apps are developed, making the process less cumbersome and more intuitive. To bring these advantages to more platforms, JetBrains created Compose Multiplatform. This framework enables developers to wield the power of Kotlin to write UI code compatible with both Android and iOS (with support for the latter currently in alpha), thereby streamlining development.

SDKs, too, simplify app development. The Scanbot SDK provides a straightforward solution for rapidly integrating fast and reliable barcode scanning into any mobile app. Barcode scanning is an essential feature for countless use cases among various industries, including logistics, healthcare, and self-checkout in retail.

In this article, we’ll walk you through the integration process for iOS and Android using Compose Multiplatform and the Scanbot Barcode Scanner SDK.

Setting up the Compose Multiplatform project

We will use Compose Multiplatform to create both Android and iOS apps. For this, you need:

Check your environment

Before you start, use the KDoctor tool to ensure that your development environment is configured correctly:

Install KDoctor with Homebrew:

brew install kdoctor

Run KDoctor in your terminal:

kdoctor

Preparing the project

Use the template from GitHub or just clone:

git@github.com:JetBrains/compose-multiplatform-template.git

The project was recently migrated from CocoaPods to a manual XCFramework, which makes the use of third-party dependencies a bit more complicated. Therefore, we will stick with the CocoaPods approach in this tutorial. Please checkout this commit to follow the tutorial step-by-step.

Open the project in Android Studio, sync the project, and switch to the Project Files view. You should see the following structure:

Let’s try to run the template app on an Android device (it looks very similar on iOS).

Removing unused targets

For this example project, we only want to deploy our app on iOS and Android. Therefore, we can remove the following lines from shared/build.gradle.kts:

jvm("desktop")

...

val desktopMain by getting {

    dependencies {

        implementation(compose.desktop.common)

    }

}

And this one from settings.gradle.kts:

include(":desktopApp")

You can also delete the desktopApp folder from the project, as well as shared/src/desktopMain.

Adding an Android dependency on the Scanbot Barcode Scanner SDK

First off, let’s set up our app on the Android side to work with the Scanbot Barcode Scanner SDK. 

For this, we will follow the steps in our documentation

In settings.gradle.kts, let’s modify the dependencyResolutionManagement section like this:

dependencyResolutionManagement {
    repositories {
        ...
        maven { url = java.net.URI("https://nexus.scanbot.io/nexus/content/repositories/releases/") }
        maven { url = java.net.URI("https://nexus.scanbot.io/nexus/content/repositories/snapshots/") }
        ...
  }
}

Next, let’s modify shared/build.gradle.kts by adding our Scanbot SDK dependencies:

kotlin {
 sourceSets {
...
        val androidMain by getting {
            dependencies {
                ...
                implementation("io.scanbot:scanbot-barcode-scanner-sdk:4.0.1")
                implementation("io.scanbot:scanbot-barcode-scanner-sdk-ui:4.0.1")
                ...
            }
        }
...
}

Adding an iOS dependency on the Scanbot Barcode Scanner SDK

To be able to use our barcode scanner functionalities in iOS apps as well, we need to add a dependency on Scanbot Barcode Scanner SDK for iOS. We will use the CocoaPods approach. 

Unfortunately, the Compose Multiplatform template project now uses the XCFramework approach, which introduced additional steps for using a third-party dependency. This is why, for this tutorial, we are using this commit of the template project, which still used CocoaPods.

To add a dependency on Scanbot Barcode Scanner SDK for iOS, just add the following line into the cocoapods block in the same shared/build.gradle.kts:

cocoapods {

        ...

        pod("ScanbotBarcodeScannerSDK") {

            version = "4.0.2"

        }

        ...    }

If the Scanbot SDK pod can’t be found after syncing the project, run the following:

cd iosApp
pod install --repo-update

Implementing shared Compose Multiplatform UI 

It’s pretty cool that we can create a shared UI for Android and iOS using Compose, even though iOS support is still in alpha. Let’s get right to it!

We start by adding a button and a text element in the template code. The text element is where we will put the scanning results.

The template provides the following code in the App class (shared/src/commonMain/kotlin/App.kt):

@OptIn(ExperimentalResourceApi::class)
@Composable
fun App() {
    MaterialTheme {
        var greetingText by remember { mutableStateOf("Hello, World!") }
        var showImage by remember { mutableStateOf(false) }
        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            Button(onClick = {
                greetingText = "Hello, ${getPlatformName()}"
                showImage = !showImage
            }) {
                Text(greetingText)
            }
            AnimatedVisibility(showImage) {
                Image(
                    painterResource("compose-multiplatform.xml"),
                    null
                )
            }
        }
    }
}

We replace the class’ code with the following:

@Composable
fun App(scannedText: String, onOpenBarcodeScannerClicked: () -> Unit = { }) {
    MaterialTheme {
        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            Button(onClick = {
                onOpenBarcodeScannerClicked()
            }) {
                Text("Open Barcode Scanner")
            }
            AnimatedVisibility(scannedText.isNotBlank()) {
                Text(scannedText)
            }
        }
    }
}

This way, we have a button to open the platform-specific scanner implemented via a callback (onOpenBarcodeScannerClicked) and a place to display the result, which we will also receive from the platform-specific code (in the form of scannedText). 

Since we changed the App constructor, we now have to adapt it in the corresponding platform-specific parts.

Scanbot SDK:
Unlimited scanning at a fixed price

Your reliable data capture solution for mobile and web app integration.


Supports all common platforms and frameworks.

Android integration

Let’s start with the Android-specific part.

First of all, as our Barcode Scanner feature will use the device’s camera, we need to add the Camera permission in our Android manifest (androidApp/src/androidMain/AndroidManifest.xml):

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

Next, we need to adapt our shared Android MainView class, located in shared/src/androidMain/kotlin/main.android.kt.

The default implementation of the MainView calls the empty App constructor:

import androidx.compose.runtime.Composable

actual fun getPlatformName(): String = "Android"

@Composable fun MainView() = App()

But as our new implementation of App(…) requires parameters with a callback and a text value, we need to adapt it now. 

We will also do a couple of other important Android-specific steps there. 

Initializing the SDK

We need to initialize the Scanbot Barcode Scanner SDK for Android before using it. Normally, we recommend doing this in the Application subclass in your Native Android part. 

However, for simplicity, we will initialize it with LaunchedEffect in this tutorial. To call the ScanbotBarcodeScannerSDKInitializer, we also need to get the Application object, which we can request from the LocalContext.current. You’ll see the initialization in the code for the Barcode Scanner screen, which we’ll implement next.

💡 Note: Without a license key, the Scanbot SDK will only work for 60 seconds. You can request a free trial license key on our website.

Implement calling the RTU UI Barcode Scanner screen

Now we follow the steps in the Ready-to-Use UI section and implement the activity launcher and the corresponding result handler. For simplicity’s sake, we will only take the first scanned barcode and use its raw value as the scanning result.

@Composable
fun MainView() {
    // Initializing the variable and the callback for the result of the scanning:
    var scannedText by remember { mutableStateOf("") }
    val barcodeScanningResult =
        rememberLauncherForActivityResult(BarcodeScannerActivity.ResultContract()) {
            // When the text is scanned, for simplicity we return only 
            // the first scanned barcode as a raw text value:
            scannedText = it.result?.barcodeItems?.first()?.text ?: ""
        }

    // SDK Initializing when the screen is launched:
    val application = LocalContext.current.applicationContext as Application
    LaunchedEffect(Unit, block = {
        ScanbotBarcodeScannerSDKInitializer()
            // Replace "YOUR_SCANBOT_SDK_LICENSE_KEY" with your Scanbot SDK license key:
            // .license(application, "YOUR_SCANBOT_SDK_LICENSE_KEY")
            .initialize(application)
    })

    App(onOpenBarcodeScannerClicked = {
        // Implementing a callback when the barcode scanning button is clicked
        val barcodeCameraConfiguration = BarcodeScannerConfiguration()
        barcodeScanningResult.launch(barcodeCameraConfiguration)
    }, scannedText = scannedText)
}

Now the Android part is ready! With just a few lines of code, we’re now ready to run the application on our Android device:

It scans the barcode very quickly. Perfect! 

iOS integration

For iOS, we won’t be able to implement the entire logic in the Shared project due to the limitations of Kotlin Multiplatform. We will later switch to Xcode to implement most of it. 

But for now, we’ll start with the Shared part in Android Studio and Kotlin, as we still need to implement the changes in the constructor of the App(…).

Let’s open our shared/src/iosMain/kotlin/main.ios.kt:

fun MainViewController() = ComposeUIViewController { App() }

In comparison to the Android part, we have to pass the parameters further to the Swift UI part and to the Native iOS project.

fun MainViewController(onOpenBarcodeScannerClicked: () -> Unit, scannedText: String) = ComposeUIViewController {
    App(onOpenBarcodeScannerClicked = {
        onOpenBarcodeScannerClicked()
    }, scannedText = scannedText)
}

Now we need to build the iOS project. For that, we can try to run the iOS app to let it build the bindings.

💡 Note: If you haven’t run the following commands yet, do so before running the build:

cd iosApp
pod install --repo-update

Obviously, the build will fail, as the Native iOS part does not expect MainViewController to have the parameters we just added. However, we will now see the hints for the constructor in the Xcode project. 

Let’s jump straight into the Xcode project and open iosApp/iosApp.xcworkspace in Xcode.

Now that the project is open, we can immediately see that there’s a problem with the constructor of our MainViewController, to which we added additional parameters in the Shared part.

Before we go on, let’s also add the Camera permission, just as we did for Android. For the iOS project, you add it to the Info.plist file:

Let’s switch back to the ContentView.swift file. As we also can see on the iOS side, our MainViewController has the Main_iosKt prefix. 

Next, we’ll add the corresponding calls to the Scanbot Barcode Scanner SDK for iOS.

As of right now, the Scanbot Barcode Scanner SDK for iOS does not contain the Swift UI wrappers, which we need for using it in the Swift UI application. However, it’s fairly simple to create one. We will take inspiration from the Swift UI example in the Scanbot SDK GitHub repository. So, first of all, copy the following files from the example project into our iosApp:

  • BarcodeScanningResult.swift
  • BarcodeScannerRTUUIView.swift
  • BarcodeScannerContainerView.swift

Next, we need to make a few changes. We’ll start by creating a wrapper around the Result object in BarcodeScanningResult.swift:

import ScanbotBarcodeScannerSDK

struct BarcodeScanningResult {
    let barcodeScannerName: String
    let scannedBarcodes: [SBSDKBarcodeScannerResult]
    
    init(barcodeScannerName: String = "", scannedBarcodes: [SBSDKBarcodeScannerResult] = []) {
        self.barcodeScannerName = barcodeScannerName
        self.scannedBarcodes = scannedBarcodes
    }
}

Next, we need to wrap our SBSDKUIBarcodeScannerViewController in a UIViewControllerRepresentable, which we do in BarcodeScannerRTUUIView.swift:

import SwiftUI
import ScanbotBarcodeScannerSDK

struct BarcodeScannerRTUUIView: UIViewControllerRepresentable {
    
    @Binding var scanningResult: BarcodeScanningResult
    @Binding var isRecognitionEnabled: Bool
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> SBSDKUIBarcodeScannerViewController {
        let acceptedTypes = SBSDKBarcodeType.allTypes()
        let configuration = SBSDKUIBarcodeScannerConfiguration.default()
        configuration.behaviorConfiguration.acceptedBarcodeTypes = acceptedTypes
        
        let scannerViewController = SBSDKUIBarcodeScannerViewController.createNew(with: configuration,
                                                                                   andDelegate: nil)
        scannerViewController.delegate = context.coordinator
        return scannerViewController
    }
    
    func updateUIViewController(_ uiViewController: SBSDKUIBarcodeScannerViewController, context: Context) {
        uiViewController.isRecognitionEnabled = isRecognitionEnabled
    }
}

extension BarcodeScannerRTUUIView {
    final class Coordinator: NSObject, SBSDKUIBarcodeScannerViewControllerDelegate, UINavigationControllerDelegate {
        
        private var parent: BarcodeScannerRTUUIView
        
        init(_ parent: BarcodeScannerRTUUIView) {
            self.parent = parent
        }
        
        func qrBarcodeDetectionViewController(_ viewController: SBSDKUIBarcodeScannerViewController,
                                              didDetect barcodeResults: [SBSDKBarcodeScannerResult]) {
                parent.scanningResult = BarcodeScanningResult(barcodeScannerName: "RTU UI Barcode Scanner",
                                                       scannedBarcodes: barcodeResults)
                parent.isRecognitionEnabled = false
        }
    }
}

Now we can wrap the result into a Swift UI View together with a basic navigation bar. We implement this in BarcodeScannerContainerView.swift:

import SwiftUI
import ScanbotBarcodeScannerSDK

struct BarcodeScannerContainerView: View {
    @Binding private var scanningResult: BarcodeScanningResult
    @State private var isRecognitionEnabled = true
    @State private var selectedBarcode: SBSDKBarcodeScannerResult? = nil
    
    init(scanningResult: Binding<BarcodeScanningResult>) {
        _scanningResult = scanningResult
    }

    var body: some View {
        viewForScanner()
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarTitle(Text("Barcode scanner"))
            .onAppear{ self.isRecognitionEnabled = true }
            .onDisappear { self.isRecognitionEnabled = false }
    }
    
    private func viewForScanner() -> some View {
        Group {
                BarcodeScannerRTUUIView(scanningResult: $scanningResult,
                                        isRecognitionEnabled: $isRecognitionEnabled)
        }
    }
}

struct ScannerContainerView_Previews: PreviewProvider {
    static var previews: some View {
        BarcodeScannerContainerView(scanningResult: .constant(BarcodeScanningResult()))
    }
}

Now we’re finally ready to use all this together in our ContentView.swift:

import UIKit
import SwiftUI
import shared
import ScanbotBarcodeScannerSDK

struct ComposeView: UIViewControllerRepresentable {
    var onOpenBarcodeScannerClicked: () -> Void
    var scannedText: String
    
    init(_ onOpenBarcodeScannerClicked:@escaping () -> Void, scannedResult: BarcodeScanningResult) {
        self.onOpenBarcodeScannerClicked = onOpenBarcodeScannerClicked
        if (!scannedResult.scannedBarcodes.isEmpty) {
            self.scannedText = scannedResult.scannedBarcodes[0].rawTextString
        } else {
            self.scannedText = ""
        }
        
    }
    
    func makeUIViewController(context: Context) -> UIViewController {
        return Main_iosKt.MainViewController(onOpenBarcodeScannerClicked: onOpenBarcodeScannerClicked, scannedText: scannedText)
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    }
}

struct ContentView: View {
    @State private var isShowingModal = false
    @State private var scannedResult = BarcodeScanningResult()
    var body: some View {
        
        if (isShowingModal && scannedResult.scannedBarcodes.isEmpty) {
            BarcodeScannerContainerView(scanningResult: $scannedResult)
        } else {
            ComposeView(
                {
                    scannedResult = BarcodeScanningResult()
                    isShowingModal = true
                }, scannedResult: scannedResult
            ).ignoresSafeArea(.keyboard)
        }
    }
}

Let’s take a closer look at this. 

ContentView is a SwiftUI View which either shows the Main screen with a button and a result view, or the Barcode Scanner Container View we just implemented. 

When the Barcode Scanner screen detects a barcode, it updates the scannedResult variable in the ContentView. As soon as the scan result contains a value, the modal screen with the Barcode Scanner closes.

For when we need to display our Compose Multiplatform view (Compose View), we also provide a listener which cleans the state and forces the modal Scanning Screen to be shown. We’ll assign this listener to the Button inside the Compose view. 

In the Compose View itself, we just initialize the Kotlin Compose View and provide another listener for the Kotlin part. We also react to the change in the Result object and always pass the raw value of the first barcode to the Kotlin part to display it. 

Let’s try to run our app on an iOS device. Before we continue, we have to make sure the signing properties are in order:

The build may take some time. However, after a short wait, we can see our Compose Multiplatform app with the barcode scanning functionality running on our iOS device as well!

Concluding remarks

We hope you enjoyed this tutorial! We’re looking forward to seeing the apps you build with Compose Multiplatform and the Scanbot Barcode Scanner SDK!

Want to get started integrating the Scanbot SDK into your app? We offer a free 7-day trial license – no strings attached. If you have any questions, don’t hesitate to get in touch. And keep an eye on this blog for more tutorials!

💡 Did you have trouble with this tutorial?

We offer free integration support for the implementation and testing of the Scanbot SDK. If you encounter technical issues or need advice on choosing the appropriate framework or features, join our Slack or MS Teams or send us an email to receive your free support.

Developers, ready to get started?

Adding our free trial to your app is easy. Download the Scanbot SDK now and discover the power of mobile data capture