Scan QR code/Bar code Android Kotlin tutorial using ML Kit

Anu Chaudhary
11 min readOct 13, 2023

--

QR codes are now widely used for various purposes, including tracking products, sharing website links, and encoding information for easy scanning.

Before diving into the code we are assuming you have basic knowledge of MVVM with clean Architecture, Hilt and navigation and Kotlin DSL.

Basic Steps to start a Android scanner App:

  1. Camera Permission is mandatory So provide the run time permission to the user to make seamless experience.
  2. Define the Camera Preview with some beautiful animation gradient and bind it with the Image Analyzer to get the scan result.
  3. Add vibration on successful scan to make some better user experience.

Dependencies used in the project:

object Plugin {
const val androidApplication = "com.android.application"
const val androidKotlin = "org.jetbrains.kotlin.android"
const val androidLibrary = "com.android.library"
const val daggerHilt = "dagger.hilt.android.plugin"
const val kotlinAndroidKapt = "kapt"
const val parcelize = "kotlin-parcelize"
}

object Dependencies {
const val materialComponent =
"com.google.android.material:material:${Versions.materialComponent}"
const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}"
const val appCompat = "androidx.appcompat:appcompat:${Versions.appCompat}"
const val constraintLayout =
"androidx.constraintlayout:constraintlayout:${Versions.constraintLayout}"
const val mlKit = "com.google.mlkit:barcode-scanning:${Versions.mlKit}"
const val camera2= "androidx.camera:camera-camera2:${Versions.cameraX}"
const val cameraLifecycle = "androidx.camera:camera-lifecycle:${Versions.cameraX}"
const val cameraView = "androidx.camera:camera-view:${Versions.cameraX}"
const val lifeCycleRuntimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycleRuntimeKtx}"
const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
const val daggerHilt = "com.google.dagger:hilt-android:${Versions.daggerHilt}"
const val daggerHiltCompiler = "com.google.dagger:hilt-compiler:${Versions.daggerHilt}"
const val navigationFragmentKtx = "androidx.navigation:navigation-fragment-ktx:${Versions.navigation}"
const val navigationUi = "androidx.navigation:navigation-ui-ktx:${Versions.navigation}"
}

Config File:

object Versions {
/* UI Libraries */
const val coreKtx = "1.12.0"
const val materialComponent = "1.9.0"
const val appCompat = "1.6.1"
const val constraintLayout = "2.1.4"
const val jUnit = "4.13.2"
const val jUnitExt = "1.1.5"
const val espressoCore = "3.5.1"
const val kotlin = "8.0.2"
const val androidKotlin = "1.8.20"
const val lifecycleRuntimeKtx = "2.5.1"

/*ML KIT for barcode scanner */
const val mlKit = "17.0.2"

/*CameraX */
const val cameraX = "1.2.1"

/* Async Programming Library */
const val coroutines = "1.7.1"

/* DI Libraries */
const val daggerHilt = "2.44"

/* Navigation library*/
const val navigation = "2.5.0"
}

Required Permissions:

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

DRY: Don’t Repeat Yourself and Modular:

The DRY principle is stated as “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system”. Don’t write the same code more than once. It is important to abstract repeated functionalities at one place, this makes code more readable and helps a lot of maintain. So we have 4 different modules with single responsibility.

  1. Design Module : This module is basically contains all basic design stuff like colors, style, theme, dimensions, attributes and common reusable components like permissions, base Activity and base fragment.
  2. Scanner Module: Scanner Module contains information related to base camera that will scan and analyse the qrcode/barcode. It contains Scanner Analyzer, ScannerManager and ScannerViewState.
  3. BuildSrc : Kotlin DSL that holds all the dependencies and plugins information.
  4. App Module: This is main layer that contains all UI part and its business logic.

Utility Classes:

  1. Permission Utils: This a singleton class that handle all logic related to permissions that can be either single or multiple and it notify the User by using IGetPermissionListener that are implemented on Scanner Activity So that User can manage navigation as per the user action.
package com.licious.sample.design.ui.permission

import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat

/**
* This class is used for Android runtime permissions handling.
*/
class PermissionUtil {
private var permissionName: String = ""
private var permissionListener: IGetPermissionListener? = null

/**
* Handle single permission results.
*/
fun handleSinglePermissionResult(activity: AppCompatActivity, isGranted: Boolean) {
when {
isGranted -> {
permissionListener?.onPermissionGranted()
}
isShouldShowRequestPermissionRationale(activity, permissionName) -> {
permissionListener?.onPermissionRationale()
}
else -> {
permissionListener?.onPermissionDenied()
}
}
}

/**
* Register listener to get updates on permission.
*/
fun setPermissionListener(permissionListener: IGetPermissionListener) {
this.permissionListener = permissionListener
}

/**
* Handle multiple permission results.
*/
fun handleMultiPermissionResult(
activity: AppCompatActivity,
permissions: Map<String, @JvmSuppressWildcards Boolean>
) {
var isGranted = true
permissions.entries.forEach {
if (!it.value) {
isGranted = false
return@forEach
}
}
when {
isGranted -> {
permissionListener?.onPermissionGranted()
}
isShouldShowRequestPermissionRationale(activity, permissions) -> {
permissionListener?.onPermissionRationale()
}
else -> {
permissionListener?.onPermissionDenied()
}
}
}

/***
* Check the requested permission is granted ot not.
*/
fun hasPermission(context: Context, permissionName: String): Boolean {
if (ContextCompat.checkSelfPermission(
context,
permissionName
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
return true
}

/***
* Check the requested permission is granted ot not.
*/
fun hasMultiPermissions(context: Context, permissions: Array<String>): Boolean {
permissions.forEach {
if (!hasPermission(context, it)) {
return false
}
}
return true
}

/**
* Ask user for single permission.
*/
fun requestPermission(permission: String, requestPermissions: ActivityResultLauncher<String>) {
permissionName = permission
requestPermissions.launch(permission)
}

/**
* Ask user for multiple permission for seamless experience of the App features.
*/
fun requestMultiplePermissions(
permissions: Array<String>,
requestMultiplePermissions: ActivityResultLauncher<Array<String>>
) {
requestMultiplePermissions.launch(permissions)
}

/**
* Open App setting page to allow permission manually.
*/
fun openAppSettingPage(
activity: AppCompatActivity,
resultLauncher: ActivityResultLauncher<Intent>
) {
val intent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", activity.packageName, null)
)
resultLauncher.launch(intent)
}

/**
* Check if user select "Never ask again" option on the permission dialog.
* If it is true that means user have to go on setting to enable permission manually.
*/
private fun isShouldShowRequestPermissionRationale(
activity: AppCompatActivity,
permission: String
): Boolean {
return !ActivityCompat.shouldShowRequestPermissionRationale(
activity, permission
) && ContextCompat.checkSelfPermission(
activity,
permission
) != PackageManager.PERMISSION_GRANTED
}

/**
* Check rationale case for multiple permission.
*/
private fun isShouldShowRequestPermissionRationale(
activity: AppCompatActivity,
permissions: Map<String, Boolean>
): Boolean {
permissions.entries.forEach {
val isRationale = isShouldShowRequestPermissionRationale(activity, it.key)
if (isRationale) {
permissionName = it.key
return isRationale
}
}
return false
}
}

2. BaseCameraManager: First we need to define thread executor service and take the instance of Camera Provider and bind it to the Camera preview. We can also control the camera Facing lens either back or front using CameraSelector LENS_FACING_FRONT/ LENS_FACING_BACK and flash light as well.

package com.licious.sample.scanner.base

import android.content.ContentValues
import android.content.Context
import android.util.Log
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

/**
* This class is the base class to handle all the camera functionality .
*/
abstract class BaseCameraManager(
private val owner: LifecycleOwner,
private val context: Context,
private val viewPreview: PreviewView,
private var lensFacing: Int,
private val showHideFlashIcon: (show: Int) -> Unit
) : DefaultLifecycleObserver {
private var imgCapture: ImageCapture? = null
private lateinit var cameraProvider: ProcessCameraProvider
private var stopped: Boolean = false
protected var camera: Camera?= null
private var flashMode: Int = ImageCapture.FLASH_MODE_OFF

protected val cameraExecutor: ExecutorService by lazy {
Executors.newSingleThreadExecutor()
}

init {
owner.lifecycle.addObserver(this)
startCamera()
}

/**
* Initialise Camera and call this method again when switch camera is clicked or you want to reinitialise camera.
*/
private fun startCamera(isSwitchButtonClicked: Boolean = false) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
controlWhichCameraToDisplay(isSwitchButtonClicked)
bindCameraUseCases()
}, ContextCompat.getMainExecutor(context))
}

/**
* Return front or back camera based on which was open last.
*/
private fun controlWhichCameraToDisplay(isSwitchButtonClicked: Boolean): Int {
if (isSwitchButtonClicked) {
lensFacing =
if (lensFacing == CameraSelector.LENS_FACING_FRONT) CameraSelector.LENS_FACING_BACK
else CameraSelector.LENS_FACING_FRONT
} else lensFacing
showHideFlashIcon(lensFacing)
return lensFacing
}

/**
* Bind Camera provider to lifecycler owner.
*/
private fun bindCameraUseCases() {
val cameraSelector = getCameraSelector()
val previewView = getPreviewUseCase()
imgCapture = getImageCapture()
cameraProvider.unbindAll()
try {
imgCapture?.let {
bindToLifecycle(cameraProvider, owner, cameraSelector, previewView, it)
}
previewView.setSurfaceProvider(viewPreview.surfaceProvider)
} catch (exc: Exception) {
Log.e(ContentValues.TAG, "Use case binding failed $exc")
}
}

/**
* unbind camera provider.
*/
override fun onPause(owner: LifecycleOwner) {
if (this::cameraProvider.isInitialized) {
cameraProvider.unbindAll()
stopped = true
super.onPause(owner)
}
}

/**
* bind camera use case again.
*/
override fun onResume(owner: LifecycleOwner) {
if (stopped) {
bindCameraUseCases()
stopped = false
}
super.onResume(owner)
}

/**
* Shutdown camera executor.
*/
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
cameraExecutor.shutdown()
}

/**
* This is a abstract method that will be implemented in the barcode manager class.
*/
protected abstract fun bindToLifecycle(
cameraProvider: ProcessCameraProvider,
owner: LifecycleOwner,
cameraSelector: CameraSelector,
previewView: Preview,
imageCapture: ImageCapture
)

/**
* Initialise Camera Selector.
*/
private fun getCameraSelector(): CameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()

/**
* Initialise Preview Builder.
*/
private fun getPreviewUseCase(): Preview = Preview.Builder()
.build()

/**
* Initialise Image capture builder.
*/
private fun getImageCapture(): ImageCapture = ImageCapture.Builder().setFlashMode(flashMode).build()

fun enableFlashForQrCode(onFlashMode: Boolean) {
camera?.cameraControl?.enableTorch(onFlashMode)
}

fun enableFlashForCamera(flashStatus: Boolean) {
flashMode = if (flashStatus)
ImageCapture.FLASH_MODE_ON
else
ImageCapture.FLASH_MODE_OFF
// Re-bind use cases to include changes
imgCapture?.flashMode = flashMode
}
}

3. Scanner View State: This class define interface for scan either successfully or failure.

sealed class ScannerViewState {
object Success : ScannerViewState()
object Error : ScannerViewState()
}

4. Scanner Analyzer : This class analyze the image and read the qrcode/barcode as per the defined options. If you define Barcode.FORMAT_QR_CODE then it will support qrcode only but if you define Barcode.FORMAT_ALL_FORMATS in that case it will scan both qrcode and barcode as well. So this class scan the code and give callback to the respective function.

/**
* This class scan the codes based on defined format(QR/Barcode) and deliver the result value.
*/
class ScannerAnalyzer(
private val onResult: (state: ScannerViewState, barcode: String) -> Unit
) : ImageAnalysis.Analyzer {

private val delayForProcessingNextImage = 300L

@SuppressLint("UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
val options =
BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS).build()
val scanner = BarcodeScanning.getClient(options)
val mediaImage = imageProxy.image
if (mediaImage != null) {
InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
.let { image ->
scanner.process(image)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
onResult(ScannerViewState.Success, barcode.rawValue ?: "")
}
}
.addOnFailureListener {
onResult(ScannerViewState.Error, it.message.toString())
}
.addOnCompleteListener {
CoroutineScope(Dispatchers.IO).launch {
delay(delayForProcessingNextImage)
imageProxy.close()
}
}
}
} else {
onResult(ScannerViewState.Error, "Image is empty")
}
}
}Hilt Integration: We are using Hilt to inject dependency for permission Util to manage the permission for Camera.

5. Scanner Manager: This class is responsible for binding the camera with the images and provide the result.

package com.licious.sample.scanner

import android.content.Context
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.lifecycle.LifecycleOwner
import com.licious.sample.scanner.base.BaseCameraManager

/**
* This class bind the camera to scan the code.
*/
class ScannerManager(
owner: LifecycleOwner,
context: Context,
viewPreview: PreviewView,
private val onResult: (state: ScannerViewState, result: String) -> Unit,
lensFacing: Int
) : BaseCameraManager(owner, context, viewPreview, lensFacing, {}) {

private fun getImageAnalysis(): ImageAnalysis {
return ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, ScannerAnalyzer(onResult))
}
}

override fun bindToLifecycle(
cameraProvider: ProcessCameraProvider,
owner: LifecycleOwner,
cameraSelector: CameraSelector,
previewView: Preview,
imageCapture: ImageCapture
) {
camera = cameraProvider.bindToLifecycle(
owner,
cameraSelector,
previewView,
getImageAnalysis(),
imageCapture
)
}
}

Hilt Integration: We use Hilt to inject the dependency of permission Util to check the Android Camera run time permissions.

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

@Singleton
@Provides
fun getPermissionUtil(): PermissionUtil {
return PermissionUtil()
}
}

Also kick off code generation of Hilt Components, we will need an application class, annotated with @HiltAndroidApp

@HiltAndroidApp
class ScannerApplication: Application() {}

UI Design and implementation: Firstly we have to provide Run time Camera Permission to the Android Application then navigate user to the Scanner Fragment by setting up the nav graph to the NavHostFragment. So we inject permission utils to the ScannerActivity to grant run time access for Camera permission and set navigation graph to redirect User to the scanner fragment.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<include
android:id="@+id/view_tool_bar"
layout="@layout/layout_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_tool_bar"
app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
package com.licious.sample.scannersample.ui

import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import com.licious.sample.design.ui.base.BaseActivity
import com.licious.sample.design.ui.permission.IGetPermissionListener
import com.licious.sample.design.ui.permission.PermissionUtil
import com.licious.sample.scannersample.R
import com.licious.sample.scannersample.databinding.ActivityScannerBinding
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

/**
* This class
*/
@AndroidEntryPoint
class ScannerActivity : BaseActivity<ActivityScannerBinding>(), IGetPermissionListener {
private var navController: NavController? = null

// Launcher to lunch Single Camera request.
private val requestLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
)
{ isGranted: Boolean ->
permissionUtil.handleSinglePermissionResult(this, isGranted)
}

// OnActivityResult to handle permission result.
private val resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) {
checkPermission()
}
}

override fun getLogTag(): String = TAG

override fun getViewBinding(): ActivityScannerBinding =
ActivityScannerBinding.inflate(layoutInflater)

@Inject lateinit var permissionUtil: PermissionUtil

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initView()
checkPermission()
}

override fun onPermissionGranted() {
navController?.setGraph(R.navigation.nav_main)
}

override fun onPermissionDenied() {
checkPermission()
}

override fun onPermissionRationale() {
permissionAlertDialog()
}

private fun initView(){
permissionUtil.setPermissionListener(this)
navController = binding.navHostFragment.getFragment<NavHostFragment>().navController
binding.viewToolBar.toolbar.setNavigationOnClickListener{
finish()
}
}

/**
* Check camera permission.
*/
private fun checkPermission() {
permissionUtil.apply {
if (!hasPermission(
this@ScannerActivity as AppCompatActivity,
Manifest.permission.CAMERA
)
) {
requestPermission(Manifest.permission.CAMERA, requestLauncher)
} else {
navController?.setGraph(R.navigation.nav_main)
}
}
}

/**
* Ask User to enable camera permissions.
*/
private fun permissionAlertDialog() {
AlertDialog.Builder(this).apply {
setTitle(getString(R.string.permission_required))
setMessage(getString(R.string.permission_msg))

setPositiveButton(getString(R.string.yes)) { dialog, _ ->
permissionUtil.openAppSettingPage(this@ScannerActivity as AppCompatActivity, resultLauncher)
dialog.dismiss()
}

setNegativeButton(getString(R.string.no)) { dialog, _ ->
dialog.dismiss()
checkPermission()
}
show()
}
}

companion object{
private const val TAG = "ScannerActivity"
}
}

Scanner Fragment: Define the Camera preview with some animation layout that will animate while camera will initialise and start the analyze the image.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.scanner.ScannerFragment">

<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>

<FrameLayout
android:id="@+id/ll_animation"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

<View
android:id="@+id/view_red_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/_80dp"
android:background="@drawable/bg_scanner_gradient" />

</FrameLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

ScannerViewModel: This class will start the camera from onViewCreated of fragment and bind the camera with the preview and get back the result by using interface.

/**
* This class contain the business logic for scanner.
*/
@HiltViewModel
class ScannerViewModel @Inject constructor(): ViewModel(){
private lateinit var qrCodeManager: ScannerManager

/**
* Initialize Camera Manager class.
*/
internal fun startCamera(
viewLifecycleOwner: LifecycleOwner,
context: Context,
previewView: PreviewView,
onResult: (state: ScannerViewState, result: String) -> Unit,
) {
qrCodeManager = ScannerManager(
owner = viewLifecycleOwner, context = context,
viewPreview = previewView,
onResult = onResult,
lensFacing = CameraSelector.LENS_FACING_BACK
)
}
}

Scanner Fragment: Firstly we define the vibrator to show some indication when code scanning is successful.

private val vibrator: Vibrator by lazy {
requireActivity().getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}

/**
* Vibration mobile on Scan successful.
*/
private fun vibrateOnScan() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(
VibrationEffect.createOneShot(
VIBRATE_DURATION,
VibrationEffect.DEFAULT_AMPLITUDE
)
)
} else {
vibrator.vibrate(VIBRATE_DURATION)
}
} catch (e: Exception) {
e.printStackTrace()
}
}

Now start the camera for scanning and start the animation with the camera surface and show some toast message for results.

package com.licious.sample.scannersample.ui.scanner

import android.content.Context
import android.os.Build
import android.os.Bundle
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.Toast
import androidx.fragment.app.viewModels
import com.licious.sample.design.ui.base.BaseFragment
import com.licious.sample.scannersample.databinding.FragmentScannerBinding
import com.licious.sample.scannersample.ui.scanner.viewmodels.ScannerViewModel
import com.licious.sample.scanner.ScannerViewState
import dagger.hilt.android.AndroidEntryPoint

/**
* This Class will scan all qrcode and display it.
*/
@AndroidEntryPoint
class ScannerFragment : BaseFragment<FragmentScannerBinding>() {
private val qrCodeViewModel: ScannerViewModel by viewModels()

private val vibrator: Vibrator by lazy {
requireActivity().getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}

override fun getLogTag(): String = TAG

override fun getViewBinding(): FragmentScannerBinding =
FragmentScannerBinding.inflate(layoutInflater)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
startAnimation()
}

override fun onDestroyView() {
vibrator.cancel()
super.onDestroyView()
}

/**
* Initialise views and and handle click listeners here
*/
private fun initView() {
qrCodeViewModel.startCamera(viewLifecycleOwner, requireContext(), binding.previewView, ::onResult)
}

/**
* Success callback and error callback when barcode is successfully scanned. This method is also called while manually enter barcode
*/
private fun onResult(state: ScannerViewState, result: String?) {
when(state)
{
ScannerViewState.Success -> {
vibrateOnScan()
Toast.makeText(requireContext(), "result=${result}", Toast.LENGTH_SHORT).show()
}
ScannerViewState.Error -> {
Toast.makeText(requireContext(), "error =${result}", Toast.LENGTH_SHORT).show()
}
else -> {
Toast.makeText(requireContext(), "error =${result}", Toast.LENGTH_SHORT).show()
}
}
}

/**
* Animation for the red bar.
*/
private fun startAnimation() {
val animation: Animation = AnimationUtils.loadAnimation(context, com.licious.sample.scanner.R.anim.barcode_animator)
binding.llAnimation.startAnimation(animation)
}

/**
* Vibration mobile on Scan successful.
*/
private fun vibrateOnScan() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(
VibrationEffect.createOneShot(
VIBRATE_DURATION,
VibrationEffect.DEFAULT_AMPLITUDE
)
)
} else {
vibrator.vibrate(VIBRATE_DURATION)
}
} catch (e: Exception) {
e.printStackTrace()
}
}

companion object {
private const val TAG = "QrCodeReaderFragment"
private const val VIBRATE_DURATION = 200L
}
}

So you can get full project from GitHub Link. I hope this article is useful for you to make a basic scan App with jetpack in Android.

--

--