Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

ScanKit KMP 🔍

Kotlin Compose License Maven Central

A modern, cross-platform barcode scanner library built with Kotlin Multiplatform and Jetpack Compose. Scan QR codes and barcodes efficiently across Android, iOS, and Desktop platforms with a clean, customizable API following Clean Architecture principles.

✨ Features

  • 📱 Cross-Platform Support: Android, iOS, and Desktop (JVM)
  • 🎨 Jetpack Compose UI: Modern declarative UI components
  • 📊 Multiple Formats: QR codes, Data Matrix, Code 128, EAN-13, UPC-A, and more
  • 📷 Real-time Scanning: Live camera feed with instant detection
  • 🔧 Highly Customizable: Configurable UI, themes, and scanning parameters
  • 🏗️ Clean Architecture: SOLID principles with dependency injection support
  • 🎯 Performance Optimized: Efficient scanning with minimal battery usage
  • 📖 Comprehensive Documentation: Detailed guides and API documentation
  • 🌙 Material Design 3: Full theming support including dark mode
  • Accessibility Ready: Built-in accessibility features and testing

🚀 Platform Support

Platform Status Min Version
Android API 21+
iOS iOS 13+
Desktop JVM 11+

📦 Installation

Gradle (Kotlin DSL)

Add the dependency to your build.gradle.kts:

dependencies {
    implementation("io.github.mobilebytelabs:scankit-kmp:1.0.0")
    
    // For Compose UI components
    implementation("io.github.mobilebytelabs:scankit-compose:1.0.0")
}

Version Catalog

[versions]
scankit = "1.0.0"

[libraries]
scankit-kmp = { group = "io.github.mobilebytelabs", name = "scankit-kmp", version.ref = "scankit" }
scankit-compose = { group = "io.github.mobilebytelabs", name = "scankit-compose", version.ref = "scankit" }

🎯 Quick Start

Basic Scanner Implementation

@Composable
fun BarcodeScanner(
    onBarcodeDetected: (Barcode) -> Unit,
    modifier: Modifier = Modifier,
    scannerConfig: ScannerConfig = ScannerConfig(),
    overlayContent: @Composable BoxScope.() -> Unit = { DefaultScannerOverlay() }
) {
    ScanKitCameraView(
        onBarcodeDetected = onBarcodeDetected,
        config = scannerConfig,
        modifier = modifier,
        overlayContent = overlayContent
    )
}

@Composable
fun ScannerScreen(
    onNavigateBack: () -> Unit,
    onBarcodeScanned: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    
    Column(
        modifier = modifier.fillMaxSize()
    ) {
        BarcodeScanner(
            onBarcodeDetected = { barcode ->
                onBarcodeScanned(barcode.displayValue)
            },
            modifier = Modifier.weight(1f),
            scannerConfig = ScannerConfig(
                supportedFormats = setOf(
                    BarcodeFormat.QR_CODE,
                    BarcodeFormat.EAN_13,
                    BarcodeFormat.CODE_128
                ),
                enableTorch = true,
                enableAutoFocus = true
            )
        )
        
        ScannerControls(
            onNavigateBack = onNavigateBack,
            modifier = Modifier.fillMaxWidth()
        )
    }
}

Custom Scanner Overlay

@Composable
fun CustomScannerOverlay(
    isScanning: Boolean = true,
    overlayColor: Color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f),
    scanFrameColor: Color = MaterialTheme.colorScheme.primary,
    scanFrameSize: Dp = 250.dp,
    cornerRadius: Dp = 16.dp,
    animationDuration: Int = 2000
) {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        // Semi-transparent overlay
        Canvas(modifier = Modifier.fillMaxSize()) {
            val scanFramePx = scanFrameSize.toPx()
            val centerX = size.width / 2f
            val centerY = size.height / 2f
            
            // Draw overlay with cutout
            drawRect(
                color = overlayColor,
                size = size
            )
            
            // Draw scan frame
            drawRoundRect(
                color = scanFrameColor,
                topLeft = Offset(
                    centerX - scanFramePx / 2f,
                    centerY - scanFramePx / 2f
                ),
                size = Size(scanFramePx, scanFramePx),
                cornerRadius = CornerRadius(cornerRadius.toPx()),
                style = Stroke(width = 4.dp.toPx())
            )
        }
        
        // Scanning instruction text
        Text(
            text = stringResource(R.string.scan_instruction),
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurface,
            modifier = Modifier
                .align(Alignment.BottomCenter)
                .padding(bottom = 100.dp)
        )
    }
}

Configuration

data class ScannerConfig(
    val supportedFormats: Set<BarcodeFormat> = BarcodeFormat.ALL_FORMATS,
    val enableTorch: Boolean = false,
    val enableAutoFocus: Boolean = true,
    val enableBeep: Boolean = true,
    val enableVibration: Boolean = true,
    val scanDelay: Long = 1000L,
    val cameraFacing: CameraFacing = CameraFacing.BACK,
    val imageQuality: ImageQuality = ImageQuality.MEDIUM
)

enum class BarcodeFormat {
    QR_CODE,
    DATA_MATRIX,
    CODE_128,
    CODE_39,
    EAN_13,
    EAN_8,
    UPC_A,
    UPC_E,
    PDF_417,
    AZTEC,
    CODABAR,
    ITF,
    RSS_14,
    RSS_EXPANDED;
    
    companion object {
        val ALL_FORMATS = values().toSet()
        val COMMON_FORMATS = setOf(QR_CODE, EAN_13, CODE_128)
    }
}

🏗️ Architecture

ScanKit KMP follows Clean Architecture principles with clear separation of concerns:

┌─────────────────┐
│   Presentation  │  ← Compose UI Components
├─────────────────┤
│    Domain       │  ← Use Cases, Entities, Repositories (Interfaces)
├─────────────────┤
│      Data       │  ← Repository Implementations, Data Sources
├─────────────────┤
│   Framework     │  ← Platform-specific implementations
└─────────────────┘

Dependency Injection

The library is designed to work with popular DI frameworks:

Koin Example

val scanKitModule = module {
    single<BarcodeScanner> { PlatformBarcodeScanner() }
    single<ScannerRepository> { ScannerRepositoryImpl(get()) }
    factory { ScanBarcodeUseCase(get()) }
}

Dagger/Hilt Example

@Module
@InstallIn(SingletonComponent::class)
abstract class ScanKitModule {
    
    @Binds
    abstract fun bindScannerRepository(
        scannerRepositoryImpl: ScannerRepositoryImpl
    ): ScannerRepository
}

🎨 Theming

ScanKit KMP fully supports Material Design 3 theming:

@Composable
fun ThemedScannerScreen() {
    MaterialTheme(
        colorScheme = dynamicColorScheme(),
        typography = AppTypography,
        shapes = AppShapes
    ) {
        BarcodeScanner(
            onBarcodeDetected = { /* handle result */ },
            scannerConfig = ScannerConfig(),
            overlayContent = {
                CustomScannerOverlay(
                    scanFrameColor = MaterialTheme.colorScheme.primary,
                    overlayColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f)
                )
            }
        )
    }
}

📱 Platform-Specific Usage

Android Permissions

Add required permissions to AndroidManifest.xml:

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

iOS Permissions

Add camera usage description to Info.plist:

<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan barcodes</string>

Permission Handling

@Composable
fun ScannerWithPermissions(
    onBarcodeScanned: (String) -> Unit
) {
    val cameraPermissionState = rememberPermissionState(
        android.Manifest.permission.CAMERA
    )
    
    LaunchedEffect(Unit) {
        cameraPermissionState.launchPermissionRequest()
    }
    
    when {
        cameraPermissionState.hasPermission -> {
            BarcodeScanner(
                onBarcodeDetected = { barcode ->
                    onBarcodeScanned(barcode.displayValue)
                }
            )
        }
        cameraPermissionState.shouldShowRationale -> {
            PermissionRationaleDialog(
                onRequestPermission = {
                    cameraPermissionState.launchPermissionRequest()
                }
            )
        }
        else -> {
            PermissionDeniedContent()
        }
    }
}

🧪 Testing

Unit Tests

class ScanBarcodeUseCaseTest {
    
    @Test
    fun `should return success when barcode is valid`() = runTest {
        // Given
        val mockRepository = mockk<ScannerRepository>()
        val useCase = ScanBarcodeUseCase(mockRepository)
        val testBarcode = Barcode(
            format = BarcodeFormat.QR_CODE,
            displayValue = "test_value"
        )
        
        coEvery { mockRepository.scanBarcode(any()) } returns Result.success(testBarcode)
        
        // When
        val result = useCase.execute(ByteArray(0))
        
        // Then
        assertTrue(result.isSuccess)
        assertEquals("test_value", result.getOrNull()?.displayValue)
    }
}

UI Tests

@RunWith(AndroidJUnit4::class)
class ScannerScreenTest {
    
    @get:Rule
    val composeTestRule = createComposeRule()
    
    @Test
    fun scannerScreen_displaysCorrectly() {
        composeTestRule.setContent {
            ScannerScreen(
                onNavigateBack = {},
                onBarcodeScanned = {}
            )
        }
        
        composeTestRule
            .onNodeWithContentDescription("Scanner overlay")
            .assertIsDisplayed()
    }
}

🛠️ Advanced Usage

Custom Result Processing

class BarcodeProcessor {
    
    suspend fun processBarcode(
        barcode: Barcode,
        processingConfig: ProcessingConfig = ProcessingConfig()
    ): ProcessedResult {
        return when (barcode.format) {
            BarcodeFormat.QR_CODE -> processQRCode(barcode)
            BarcodeFormat.EAN_13 -> processProductCode(barcode)
            else -> ProcessedResult.Raw(barcode.displayValue)
        }
    }
    
    private suspend fun processQRCode(barcode: Barcode): ProcessedResult {
        return when {
            barcode.displayValue.startsWith("http") -> 
                ProcessedResult.Url(barcode.displayValue)
            barcode.displayValue.contains("@") -> 
                ProcessedResult.Email(barcode.displayValue)
            else -> ProcessedResult.Text(barcode.displayValue)
        }
    }
}

Batch Scanning

@Composable
fun BatchScanner(
    onBatchComplete: (List<Barcode>) -> Unit,
    batchSize: Int = 10,
    modifier: Modifier = Modifier
) {
    var scannedCodes by remember { mutableStateOf(listOf<Barcode>()) }
    
    LaunchedEffect(scannedCodes.size) {
        if (scannedCodes.size >= batchSize) {
            onBatchComplete(scannedCodes)
            scannedCodes = emptyList()
        }
    }
    
    BarcodeScanner(
        onBarcodeDetected = { barcode ->
            if (!scannedCodes.any { it.displayValue == barcode.displayValue }) {
                scannedCodes = scannedCodes + barcode
            }
        },
        modifier = modifier
    )
}

📚 Sample Projects

Check out our sample projects in the /samples directory:

  • basic-scanner: Simple QR code scanner
  • product-scanner: E-commerce barcode scanner
  • multi-format-scanner: Scanner supporting multiple formats
  • custom-ui-scanner: Advanced UI customization examples

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

  1. Clone the repository:

    git clone https://github.com/mobilebytelabs/scankit-kmp.git
    cd scankit-kmp
  2. Set up the development environment:

    ./gradlew build
  3. Run tests:

    ./gradlew allTests
  4. Format code:

    ./gradlew spotlessApply

Code Style

This project follows Kotlin Coding Conventions and uses:

🙏 Acknowledgments

📞 Support


Made with ❤️ by the MobileByteLabs team

About

A modern Kotlin Multiplatform barcode scanner library with Jetpack Compose UI, supporting QR codes, barcodes, and multiple platforms. Built with Clean Architecture and SOLID principles.

Resources

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors