Membangun Aplikasi Scan Barcode dengan ML Kit (CameraX, Jetpack Compose)

Bagian 1: On Device Machine Learning Exploration

Veronica Putri Anggraini
11 min readJan 30, 2025

Hello folks! Pada artikel kali ini, saya akan membahas sebuah proyek sederhana berupa aplikasi scan barcode yang dapat digunakan secara offline. Keyword offline menjadi penting karena artikel ini merupakan bagian dari explorasi On Device Machine Learning yang saya lakukan. Sehingga jika teman — teman belum memahami apa itu On Device ML, langsung saja cek artikel dibawah ini.

Proyek ini dibuat dengan menggunakan Jetpack Compose sebagai UI komponen, CameraX dan ML Kit sebagai library untuk menganalisis barcode.

Sebelum membaca lebih jauh, sedikit nostalgia dengan library barcode scanning yang hampir pasti pernah kita gunakan untuk explorasi atau bahkan production app yaitu Zxing barcode library. Namun pada proyek ini peran Zxing barcode library akan digantikan dengan ML Kit Lib. Selain itu Scanning barcode bukan sekedar menampilkan preview dari barcode tetapi juga terdapat proses analisis didalamnya. Terdapat 3 kamera API yang sebenarnya dapat digunakan yaitu Camera, Camera2 dan CameraX.

Namun dalam proyek ini preview barcode dan analisis barcode perlu dilakukan secara bersamaan. Sehingga penggunaan CameraX dapat mempermudah proses ini. Dalam praktiknya integrasi CameraX dapat melalui abstraction yang disebut use case. Terdapat 4 usecase dari CameraX API

  • Preview berfungsi menampilkan preview gambar dari kamera di layar device.
  • Image Analysis berfungsi memproses gambar yang diambil dari kamera secara real-time.
  • Image Capture berfungsi mengambil gambar.
  • Video Capture berfungsi mengambil video dan audio.

Sesuai kebutuhan proyek ini penggunaan CameraX tentu pilihan yang tepat karena preview barcode dapat dilakukan dengan Preview Usecase dan Analisis Barcode dapat dilakukan dengan Image Analysis Usecase.

Apa itu ML-Kit ?

ML Kit merupakan sebuah Mobile SDK untuk mengintegrasikan berbagai fitur Machine Learning ke dalam aplikasi Android maupun iOS secara on device. ML Kit menyediakan dua fitur yaitu :

  1. Natual Language API adalah fitur untuk mengidentifikasi dan menerjemahkan hingga 58 bahasa.
  2. Vision API adalah fitur untuk analisis video dan gambar pada image labelling, barcode, teks, wajah, dan objek detection.
Fitur ML Kit https://developers.google.com/ml-kit

Dengan berbagai fitur yang dimiliki ML Kit jelas yang akan digunakan pada proyek ini adalah Vision API yaitu Barcode Scanning.

Konfigurasi Proyek Aplikasi Scan Barcode

Scan Barcode UI

Ilustrasi diatas merupakan UI dari Aplikasi scan barcode sederhana yang akan kita buat. Sebagai langkah awal, buatlah proyek dengan template jetpack compose pada android studio. Selanjutnya tambahkan library yang akan digunakan sebagai berikut:

Proyek ini menggunakan gradle catalog format sehingga terdapat 2 file yang perlu diperhatikan yaitu libs.versions.toml dan build.gradle.kts (Module :app)

  1. Constraint Layout library yang digunakan untuk membangun positioning dari UI.
// add on libs.versions.toml file 
constraintlayoutVersion = "1.1.0"
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintlayoutVersion"}

//add on build.gradle.kts file
implementation(libs.androidx.constraintlayout)

2. CameraX library yang digunakan untuk menampilkan preview dan analyze image.

// add on libs.versions.toml file
cameraxcameraxVersion = "1.4.1"
camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraxVersion" }
camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraxVersion" }
camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraxVersion" }

//add on build.gradle.kts file
implementation(libs.camera.core)
implementation(libs.camera.camera2)
implementation(libs.camera.view)

3. MLKit library yang digunakan untuk scanning barcode dan extract barcode data.

// add on libs.versions.toml file
mlKitVersion = "1.4.1"
camera-mlkit-vision = { group = "androidx.camera", name = "camera-mlkit-vision", version.ref = "mlKitVersion" }

//add on build.gradle.kts file
implementation(libs.camera.mlkit.vision)
implementation(libs.barcode.scanning)

4. Accompanist library yang digunakan untuk membantu mengembangkan aplikasi dengan jetpack compose dengan berbagai fungsi dan komponen umum yang dimilikinya.

// add on libs.versions.toml file
accompanistVersion = "0.30.0"
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistVersion" }

//add on build.gradle.kts file
implementation(libs.accompanist.permissions)

Klik Sync Now project setelah menambahkan beberapa library yang digunakan.

Membuat Camera Interface

Langkah berikutnya adalah menampilkan camera view sebagai based UI interface dari proyek ini. Namun menampilkan camera view mengharuskan aplikasi mengakses fungsi hardware dari camera. Sehingga pengguna harus secara sadar memberikan izin kepada aplikasi untuk mengaksesnya.

1. Tambahkan request permission untuk kamera dan camera hardware pada AndroidManifest file.

 <uses-feature
android:name="android.hardware.camera"
android:required="true" />

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

Kedua block kode diatas diperlukan karena aplikasi scan barcode menggunakan kamera.

  • <uses-permission> Camera digunakan untuk mendapatkan izin dari pengguna.
  • <uses-feature> Hardware Camera digunakan untuk memberi tahu sistem dan layanan tentang penggunaan kamera, dan untuk mengontrol ketersediaan aplikasi di Google Play Store. Jika aplikasi yang dikembangkan menggunakan kamera sebagai fungsi utama dan tidak dapat berjalan tanpa kamera maka required value harus di beri value true. Namun jika salah satu fitur dalam aplikasi memiliki fitur yang memerlukan kamera tetapi tetap dapat berjalan walaupun device tidak memiliki kamera maka dapat memberikan value false.

2. Buat sebuah composabe function berupa CameraPreview. Dalam function ini lakukan pengecekan terhadap camera permission dengan code dibawah ini.

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPreview() {
// check camera permission
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
if (!cameraPermissionState.status.isGranted) {
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
} else {
}
}

Pada kode diatas terdapat beberapa point yang perlu diperhatikan. Halaman utama pada aplikasi akan menampilkan kamera preview sehingga camera permission diperlukan. Jika kamera belum berstatus granted maka akan menampilkan required permission dialog, namun jika status granted maka akan menampilkan camera preview nantinya. Berikut adalah beberapa fungsi dari snippet code yang digunakan :

  • rememberPermissionState() merupakan fungsi dari Accompanist Permissions Library untuk menyimpan state permission dari kamera.
  • LaunchedEffect merupakan sebuah fungsi yang digunakan untuk mengeksekusi code diluar composition sehingga sering disebut side effect. Pemanggilan permission tidak mengubah UI secara langsung sehingga sangat tepat medeklarasikannya didalam launched effect.

3. Setelah pengecekan dan permintaan izin kamera permission dideklarasikan selanjutnya buatlah sebuah function getCameraPreview dengan return PreviewView yang merupakan object dari CameraX, dengan kode dibawah ini.

    private fun getCameraPreview(): PreviewView {
cameraController = LifecycleCameraController(this)
val previewView = PreviewView(this)
cameraController.cameraSelector =
CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()

cameraController.bindToLifecycle(this)
previewView.controller = cameraController
return previewView
}

Kode diatas secara umum digunakan untuk memebuat sebuah function yang menginisialisasi sebuah camera preview. Terdapat beberapa poin penting yang perlu diperhatikan.

  • LifecycleCameraController merupakan kelas dari CameraX yang mampu mengelola lifecycle dari kamera serta mem-provide kontrol atas kamera hingga ke konfiguarsi camera.
  • CameraSelector merupakan sebuah property yang dapat digunakan untuk menentukan camera yang akan digunakan. Seperti pada kode diatas Barcode Scanning App meng-capture image dari kamera belakang sehingga konfigurasi kamera dilakukan melalui cameraController.cameraSelector untuk hanya menampilkan preview camera dari kamera belakang.

Perlu diketahui bahwa PreviewView merupakan sebuah komponen View dari CameraX yang merupakan pendekatan imperatif pada view system. Untuk menggunakan komponen ini dapat menjadi sebuah factory dari AndroidView komponen. Sehingga kodenya akan menjadi seperti dibawa ini.

    @OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPreview() {
//other code ...

} else {
ConstraintLayout(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
val (cameraPreview) = createRefs()
AndroidView(
modifier = Modifier
.fillMaxSize()
.constrainAs(cameraPreview) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
factory = {
getCameraPreview()
})
}
}

Kode diatas menggunakan AndroidView untuk menampilkan komponen getPreview yang me-return pada View komponen PreviewView dari CameraX.

4. Buatlah component button seperti pada desain Scan Barcode UI. Terdapat dua button dengan desain yang sama, sehingga untuk mencegah penulisan kode yang berulang buatlah sebuah custom circle button pada file baru dengan nama CustomButton.kt seperti dibawah ini.

@Composable
fun CircleButton(size: Int, icon: Int, modifier: Modifier = Modifier, onClick: () -> Unit) {
Button(
onClick = { onClick() },
shape = CircleShape,
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.Black
),
modifier = modifier
.size(size.dp)
.padding(8.dp),
contentPadding = PaddingValues(10.dp)
) {
Icon(
painterResource(id = icon),
contentDescription = "Button",
modifier = Modifier
.size(30.dp)
.scale(1f)
)
}
}

Pada custom CircleButton diatas size, icon, click action dikeluarkan menjadi sebuah parameter. Sehingga value dari parameter tersebut dapat kita atur sesuai kebutuhan.

5. Buatlah component frame scan area dengan menambahkan sebuah composable function pada file baru dengan nama CustomScanFrame.kt seperti dibawah ini.

@Composable
fun CustomScanFrame(modifier: Modifier) {
Canvas(
modifier = modifier
.padding(horizontal = 10.dp)
.fillMaxSize()
) {
val width = size.width
val height = 1000f
val cornerRadius = 40.dp.toPx()
val strokeWidth = 4.dp.toPx()
val horizontalPadding = (width - (width - 300f)) / 2
val verticalPadding = 100f

// Top-left
drawArc(
color = Color.White,
startAngle = 180f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(horizontalPadding, verticalPadding),
size = Size(cornerRadius * 2, cornerRadius * 2),
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)

// Top-right
drawArc(
color = Color.White,
startAngle = 270f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(width - horizontalPadding - cornerRadius * 2, verticalPadding),
size = Size(cornerRadius * 2, cornerRadius * 2),
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)

// Bottom-left
drawArc(
color = Color.White,
startAngle = 90f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(horizontalPadding, verticalPadding + height - cornerRadius * 2),
size = Size(cornerRadius * 2, cornerRadius * 2),
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)

// Bottom-right
drawArc(
color = Color.White,
startAngle = 0f,
sweepAngle = 90f,
useCenter = false,
topLeft = Offset(width - horizontalPadding - cornerRadius * 2, verticalPadding + height - cornerRadius * 2),
size = Size(cornerRadius * 2, cornerRadius * 2),
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
}
}

Custom frame diatas menggunakan Canvas karena canvas menyediakan area untuk menggambar secara manual. Karena frame scan barcode terdiri dari sudut round di empat sisi. Sehingga komponen drawArc yang digunakan untuk menggambar busur pada masing — masing sudut dengan dua fungsi penting yaitu startAngle yang menentukan sudut awal, dan sweepAngle yang menentukan berapa derajat busur akan digambar. Detail value setiap function sebagai berikut :

  • Top-left, startAngle = 180f, sweepAngle = 90f
  • Top-right, startAngle = 270f, sweepAngle = 90f
  • Bottom-left, startAngle = 90f, sweepAngle = 90f
  • Bottom-right, startAngle = 0f, sweepAngle = 90f

Selain itu pada block function diatas, setiap sudut memiliki fuction cap dengan value round yang digunakan untuk membuat ujung garis berbentuk rounded.

6. Tambahkan title scan barcode page, button galeri, button flash dan frame scan pada halaman utama. Setiap button memerlukan icon, tambahkan icon pada folder drawable, dan icon dapat di download dari link berikut. Langkah berikutnya tambahkan setiap komponen seperti kode dibawah ini.

    @OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPreview() {
var enableFlash by remember { mutableStateOf(false) }

// check camera permission
...
if (!cameraPermissionState.status.isGranted) {
...
} else {
ConstraintLayout(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
val (cameraPreview, galleryButton, flashButton, txtInstruction, scanFrame) = createRefs()

// Camera preview
...

// Title Scan
Text(
text = "Scan barcode",
fontSize = 24.sp,
color = Color.White,
modifier = Modifier.constrainAs(txtInstruction) {
start.linkTo(parent.start)
top.linkTo(parent.top, margin = 100.dp)
end.linkTo(parent.end)
})

// Scan Frame
CustomScanFrame(modifier = Modifier.constrainAs(scanFrame) {
top.linkTo(txtInstruction.bottom, 30.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
})

// Button Gallery
CircleButton(modifier = Modifier.constrainAs(galleryButton) {
start.linkTo(parent.start, margin = 120.dp)
bottom.linkTo(parent.bottom, margin = 70.dp)
},
size = 70, icon = R.drawable.icon_gallery, onClick = {})

// Button Flash
CircleButton(
modifier = Modifier
.constrainAs(flashButton) {
end.linkTo(parent.end, margin = 120.dp)
bottom.linkTo(parent.bottom, margin = 70.dp)
},
size = 70,
icon = if (enableFlash) R.drawable.icon_flash_off else R.drawable.icon_flash_on,
onClick = {
enableFlash = !enableFlash
cameraController.cameraControl?.enableTorch(enableFlash)
}
)
}
}
}

Setiap komponen memiliki ID untuk menyusun tata letak sesuai dengan desain UI menggunakan constraint layout. Terdapat hal penting yang harus diperhatikan yaitu button flash yang iconnya berubah sesuai dengan value yang disimpan oleh enableFlash state. Selain itu pada onClick handler flash button diperlukan sebuah kode yang digunakan untuk mengontrol status torch pada kamera, sesuai dengan value enableFlash state On/Off.

Menganalisis Barcode

Saat melakukan scanning pada barcode agar area yang terpindai lebih jelas, diperlukan sebuah highlight frame. Buatlah sebuah highlight frame pada file baru dengan nama QrCodeHighlightDrawable.kt dan code sebagai berikut.

class BarCodeHighlightDrawable(private val rect: Rect) : Drawable() {
private val paint = Paint().apply {
style = Paint.Style.STROKE
color = Color.WHITE
strokeWidth = 20F
}

override fun draw(p0: Canvas) {
p0.drawRect(rect, paint)
}

override fun setAlpha(p0: Int) {
paint.alpha = p0
}

override fun setColorFilter(p0: ColorFilter?) {
paint.colorFilter = colorFilter
}

@Deprecated("Deprecated in Java",
ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat")

)
override fun getOpacity(): Int {
return PixelFormat.OPAQUE
}
}

Highlight Barcode akan muncul dan membingkai image barcode yang terdeteksi. Buatlah konfigurasi barcode detection dengan MLKit seperti dibawah ini.

val options = BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS).build()
val barcodeScanner = BarcodeScanning.getClient(options)

BarcodeScannerOptions merupakan kelas dari ML Kit yang digunakan untuk mengonfigurasi scan barcode. Untuk menentukan format barcode yang ingin dideteksi. Pada proyek ini scanner digunakan untuk mendeteksi semua tipe barcode, sehingga Barcode.FORMAT_ALL_FORMATS digunakan. Untuk menganalisis barcode gunakan cameraController (dari CameraX) untuk melakukan analisis gambar secara real-time menggunakan ML Kit Barcode Scanning. Untuk menampilkan sorotan (highlight) atau kotak pembatas (bounding box) di sekitar barcode yang terdeteksi gunakan composable function BarCodeHighlightDrawable yang sebelumnya telah dibuat.

     cameraController.setImageAnalysisAnalyzer(
Executors.newSingleThreadExecutor(),
MlKitAnalyzer(
listOf(barcodeScanner),
COORDINATE_SYSTEM_VIEW_REFERENCED,
ContextCompat.getMainExecutor(this)
) { result ->
val barcodeResults = result?.getValue(barcodeScanner)
if (barcodeResults?.firstOrNull() == null) {
previewView.overlay.clear()
previewView.setOnTouchListener { _, _ ->
false
}
return@MlKitAnalyzer
}
val firstResult = barcodeResults.first()
val drawable = firstResult.boundingBox?.let {
BarCodeHighlightDrawable(it)
}
})

Pada aplikasi ini result barcode akan di bedakan menjadi dua yaitu url dan non-url, jika data berupa url maka akan direct ke browser dan jika bukan akan menampilkan data kedalam alert dialog seperti kode dibawah ini.

 if (firstResult.valueType == Barcode.TYPE_URL) {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(firstResult.rawValue)
)
startActivity(browserIntent)
} else {
onBarcodeData(firstResult.rawValue ?: "")

}
previewView.overlay.clear()
drawable?.let {
previewView.overlay.add(
it
)
}
previewView.setOnTouchListener { _, event ->
if (event.action == android.view.MotionEvent.ACTION_DOWN) {
if (firstResult.boundingBox!!.contains(event.x.toInt(), event.y.toInt())) {
if (firstResult.valueType == Barcode.TYPE_URL) {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(firstResult.rawValue)
)
startActivity(browserIntent)
}
}
}
true
}

Sedangkan untuk menampilkan alert dialog jika data bukan link akan seperti dibawah ini.

        if (isShowPopup) {
AlertDialog(
onDismissRequest = { isShowPopup = false },
title = { Text("Barcode Data") },
text = { Text(barcodeData) },
confirmButton = {
Button(
onClick = { isShowPopup = false }, colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF008000),
contentColor = Color.White
)
) {
Text("OK")
}
}
)
}

Selain dari scan camera aplikasi ini memiliki fitur untuk menganalisis barcode yang berasal dari galeri. Gunakan rememberLauncherForActivityResult yang berfungsi untuk memilih gambar dari galeri, kemudian memproses gambar tersebut untuk mendeteksi barcode menggunakan ML Kit. Setelah pengguna memilih gambar, URI gambar tersebut dikonversi menjadi Bitmap, lalu diubah menjadi InputImage yang diproses oleh barcodeScanner.

Jika barcode berhasil terdeteksi, rawValue dari barcode pertama yang ditemukan akan disimpan ke dalam variabel barcodeData, dan variabel isShowPopup akan di assign dengan value true untuk men-trigger tampilan popup yang berisi informasi barcode tersebut, seperti kode dibawah ini.

     val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val uri = result.data?.data
if (uri != null) {
val bitmap = if (Build.VERSION.SDK_INT < 28) {
MediaStore.Images.Media.getBitmap(
context.contentResolver,
uri
)
} else {
val source = ImageDecoder.createSource(context.contentResolver, uri)
ImageDecoder.decodeBitmap(source)
}
val image = InputImage.fromBitmap(bitmap, 0)
barcodeScanner.process(image)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
val firstResult = barcodes.first()
barcodeData = firstResult.rawValue ?: ""
isShowPopup = true
}
}
}
}
}

Untuk menampilkan opsi galeri, buatlah variable isShowGallery dan gunakan LaunchedEffect untuk meluncurkan sebuah Intent untuk memilih gambar dari galeri perangkat ketika isShowGallery bernilai true, dengan kode sebagai berikut.

    if (isShowGallery) {
LaunchedEffect(Unit) {
val galleryIntent = Intent(
Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
)
launcher.launch(galleryIntent)
isShowGallery = false
}
}

Pada kode diatas LaunchedEffect memastikan bahwa galery intent hanya eksekusi sekali saat proses composition terjadi. Sedangkan ACTION_PICK dan URI MediaStore.Images.Media.EXTERNAL_CONTENT_URI dibuat untuk membuka galeri. Function launcher.launch digunakan untuk me-lauch gallery activity. Setelah gallery activity di-launch, variabel isShowGallery di-assign dengan value false untuk mencegah LaunchedEffect dijalankan kembali pada recomposition berikutnya, sehingga gallery activity hanya diluncurkan sekali.

Hasil akhir akan aplikasi seperti dibawah ini dan full code dapat diakses pada link dibawah ini.

Kesimpulan

ML Kit menyediakan solusi yang efisien untuk pemindaian barcode melalui Vision API yang dimilikinya. Dengan fitur Barcode Scanning, ML Kit mendukung berbagai format barcode, baik 1D maupun 2D, dan dapat digunakan secara on-device, sehingga memungkinkan scanning secara cepat dan dalam offline mode. Tetapi kembali lagi semua tergantung kebutuhan, aplikasi scan barcode diatas merupakan contoh implementasi dari Barcode Scan API yang di provide oleh ML Kit. Jika teman — teman merasa artikel ini bermanfaat, jangan lupa follow akun ini dan clap untuk artikel berikutnya :) cheers.

--

--

Veronica Putri Anggraini
Veronica Putri Anggraini

Written by Veronica Putri Anggraini

Software Engineer Android @LINE Bank 🤖 Google Developer Expert for Android https://github.com/veroanggraa

No responses yet