One of the most common and asked-for functions in a mobile application is a QR code scanner. QR codes and bar codes work as an effective way of passing information to people using an app.
Here in this post, we will see how we can build a QR code scanner app using Google ML Kit and Camera X.

What is CameraX?

CameraX is a part of the Jetpack support library. It provides an easy-to-use and consistent API surface which works equally well on most Android devices. It simplifies the app development process for developers by adding new capabilities. Here you don’t have to include any kind of device-specific codes which nullifies device compatibility issues altogether.

 

What is Google ML Kit?

Google ML Kit is a mobile SDK that brings the machine learning expertise of Google to iOS and Android apps. It is an easy-to-use and powerful package from Google that helps developers to come up with personalized solutions that will work smoothly across different devices.

 

What is the QR code scanning API of ML Kit?

The QR code scanning API of ML Kit lets you read encoded data using the most standard QR/barcode code formats. The data will be recognized and parsed automatically by the ML Kit when a user scans the code letting your app respond quickly and smartly.

Let’s create a QR code scanning project

To create a project…

1. Go to Android Studio. Select New Project under File with an Empty Screen template.

2. Now open the AndroidManifest.xml file to add camera permission & camera hardware permission. Here add the below-mentioned code into the manifest tag-

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

3. Open the app/build.gradle file and add a dependency for CameraX & QR code scan by mentioning the below set of codes.

//For barcode scanner(QR Code Scan) 
    implementation 'com.google.mlkit:barcode-scanning:17.1.0' 
 
    //For CameraX 
    implementation("androidx.camera:camera-core:1.2.2") 
    implementation("androidx.camera:camera-camera2:1.2.2") 
    implementation("androidx.camera:camera-lifecycle:1.2.2") 
    implementation("androidx.camera:camera-view:1.2.2") 
 
  To enable databinding, set dataBinding to true for build features within the Android tag as mentioned below: 
 
	buildFeatures { 
        dataBinding = true 
    } 

4. Now add PreviewView in the main activity layout (activity_main.xml).

PreviewView is the custom View that displays the camera feed for the Preview use case of CameraX.

<?xml version="1.0" encoding="utf-8"?> 
	<androidx.coordinatorlayout.widget.CoordinatorLayout 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" 
	    android:background="#000000" 
	    tools:context=".QrScannerActivity"> 
 
	    <androidx.cardview.widget.CardView 
	        android:layout_width="300dp" 
	        android:layout_height="300dp" 
	        android:layout_gravity="center" 
	        app:cardCornerRadius="20dp"> 
 
	        <RelativeLayout 
	            android:layout_width="match_parent" 
	            android:layout_height="match_parent"> 
 
	            <androidx.camera.view.PreviewView 
	                android:id="@+id/preview" 
	                android:layout_width="300dp" 
	                android:layout_height="300dp" 
	                android:layout_centerInParent="true" 
	                android:layout_centerHorizontal="true" /> 
 
	            <androidx.appcompat.widget.AppCompatImageView 
	                android:layout_width="300dp" 
	                android:layout_height="300dp" 
	                android:layout_centerInParent="true" 
	                android:layout_centerHorizontal="true" 
	                android:background="@drawable/background_image" /> 
 
	        </RelativeLayout> 
 
	    </androidx.cardview.widget.CardView> 
 
	</androidx.coordinatorlayout.widget.CoordinatorLayout>

5. The next step is to check camera permission to use cameraX for QR Code Scan is available or not. If it is not granted, we must request it in our codes.

class MainActivity : AppCompatActivity() { 
 
		private lateinit var binding: ActivityMainBinding 
 
 
	    override fun onCreate(savedInstanceState: Bundle?) { 
	        super.onCreate(savedInstanceState) 
	         
	        binding = ActivityQrScannerBinding.inflate(layoutInflater) 
        	setContentView(binding.root) 
 
	        if (isCameraPermissionGranted()) { 
	            // startCamera 
	        } else { 
	            ActivityCompat.requestPermissions( 
	                this, 
	                arrayOf(Manifest.permission.CAMERA), 
	                PERMISSION_CAMERA_REQUEST 
	            ) 
	        } 
	    } 
 
	    override fun onRequestPermissionsResult( 
	        requestCode: Int, 
	        permissions: Array<String>, 
	        grantResults: IntArray 
	    ) { 
	        if (requestCode == PERMISSION_CAMERA_REQUEST) { 
	            if (isCameraPermissionGranted()) { 
	                // start camera 
	            } else { 
	                Log.e(TAG, "no camera permission") 
	            } 
	        } 
	        super.onRequestPermissionsResult(requestCode, permissions, grantResults) 
	    } 
 
	    private fun isCameraPermissionGranted(): Boolean { 
	        return ContextCompat.checkSelfPermission( 
	            baseContext, 
	            Manifest.permission.CAMERA 
	        ) == PackageManager.PERMISSION_GRANTED 
	    } 
 
	    companion object { 
	        private val TAG = MainActivity::class.java.simpleName 
	        private const val PERMISSION_CAMERA_REQUEST = 1 
	    } 
	} 


[ExecutorService: The ExecutorService helps in maintaining a pool of threads and assigns them tasks. It also provides the facility to queue up tasks until there is a free thread available if the number of tasks is more than the threads available.]

 

6. Now is the time to implement camera Preview use case.

You need to define a configuration to use a Preview and it is used to create an instance of the use case. You can bind the CameraX lifecycle with the resulting instance once it is created.


ProcessCameraProvider is a singleton which is used to bind the lifecycle of cameras to the lifecycle owner. This way CameraX remains aware of the lifecycle of camera, allowing you to be stress-free about its opening and closing.


Add a Runnable to get cameraProviderLiveData value from cameraProviderFuture. Also, declare camera executor to manage thread.

cameraExecutor = Executors.newSingleThreadExecutor() 
	cameraProviderFuture = ProcessCameraProvider.getInstance(this) 
 
	cameraProviderFuture?.addListener({ 
	            try { 
	                val processCameraProvider = cameraProviderFuture?.get() 
	                //bind camera view here 
	            } catch (e: ExecutionException) { 
	                e.printStackTrace() 
	            } catch (e: InterruptedException) { 
	                e.printStackTrace() 
	            } 
	        }, ContextCompat.getMainExecutor(this))

7. First, bind view with CameraX. After that, bind your cameraSelector and preview object to the processcameraProvider.

[ImageCapture is designed for basic picture capturing. It provides takePicture() function which captures a picture, saves it to memory or a file, and provides image metadata. Pictures are taken in automatic mode once focus is converged.]

[Detecting Barcode: We’ve used ImageAnalysis feature to implement it. It allows us to define a custom class which will implement the ImageAnalysis.Analyzer interface and in turn will be used to call the camera frames that come in.]

val preview = Preview.Builder().build() 
	        val cameraSelector = 
	            CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() 
	        preview.setSurfaceProvider(binding.preview.surfaceProvider) 
	        val imageCapture = ImageCapture.Builder().build() 
	        val imageAnalysis = ImageAnalysis.Builder().setTargetResolution(Size(1280, 720)) 
	            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build() 
	        imageAnalysis.setAnalyzer(cameraExecutor!!, analyzer!!) 
	        processCameraProvider?.unbindAll() 
	        processCameraProvider?.bindToLifecycle( 
	            this, 
	            cameraSelector, 
	            preview, 
	            imageCapture, 
	            imageAnalysis 
	        )

8. The next step is to create custom class for image analysis & get incoming camera frames. Now here we don’t have to worry about managing the camera session state or even disposing of images. Just like with other lifecycle-aware components, binding to our app’s desired lifecycle is enough.

class MyImageAnalyzer(private val activity: Activity) : ImageAnalysis.Analyzer { 
 
	        override fun analyze(image: ImageProxy) { 
	         	//we can analysis images here 
	        } 
	    } 

9. We process incoming frames on ImageProxy and get images from it. We then detect barcode with ML barcode scanner.

To detect barcode, we need to create InputImage from Image. Then, pass the InputImage object to the BarcodeScanner’s process method as explained below:

@SuppressLint("UnsafeOptInUsageError") 
	        private fun scanBarCode(image: ImageProxy) { 
	            val image1 = image.image 
	            if (image1 != null) { 
	                val inputImage = InputImage.fromMediaImage(image1, image.imageInfo.rotationDegrees) 
	                val barcodeScannerOptions = BarcodeScannerOptions.Builder() 
	                    .setBarcodeFormats( 
	                        Barcode.FORMAT_QR_CODE, 
	                        Barcode.FORMAT_AZTEC 
	                    ) 
	                    .build() 
 
	                val scanner = BarcodeScanning.getClient(barcodeScannerOptions) 
	                scanner.process(inputImage) 
	                    .addOnSuccessListener { barcodes -> 
	                        // Task completed successfully 
	                        // ... 
	                        readerBarcodeData(barcodes) 
	                    } 
	                    .addOnFailureListener { 
	                        // Task failed with an exception 
	                        // ... 
	                    }.addOnCompleteListener { 
	                        image.close() 
	                    } 
	            } 
	        } 
 
	        private fun readerBarcodeData(barcodes: List<Barcode>) { 
	            for (barcode in barcodes) { 
	                Log.e( 
	                    "barcode recognize", "QR Code: " + barcode.displayValue 
	                ) //Returns barcode value in a user-friendly format. 
	                Log.e( 
	                    "barcode recognize", "Raw Value: " + barcode.rawValue 
	                ) //Returns barcode value as it was encoded in the barcode. 
	                Log.e( 
	                    "barcode recognize", "Code Type: " + barcode.valueType 
	                ) //This will tell you the type of your barcode 
	                Toast.makeText(activity, barcode.displayValue, Toast.LENGTH_SHORT).show() 
	            } 
	        } 

That’s it!
Now this should allow you to scan QR code using the camera on your Android device. You should be able to capture the QR code, scan it and read the information fed into it.

 

Here is the complete code for creating QR Code Scanner with Google ML Kit and CameraX:

class MainActivity : AppCompatActivity() { 
 
	    private lateinit var binding: ActivityMainBinding 
	    private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null 
	    private var cameraExecutor: ExecutorService? = null 
	    private var analyzer: MyImageAnalyzer? = null 
 
 
	    override fun onCreate(savedInstanceState: Bundle?) { 
	        super.onCreate(savedInstanceState) 
 
	        binding = ActivityMainBinding.inflate(layoutInflater) 
	        setContentView(binding.root) 
	        this.window.setFlags(1024, 1024) 
 
	        if (isCameraPermissionGranted()) { 
	            // startCamera 
	            startCamera() 
	        } else { 
	            ActivityCompat.requestPermissions( 
	                this, 
	                arrayOf(Manifest.permission.CAMERA), 
	                PERMISSION_CAMERA_REQUEST 
	            ) 
	        } 
	    } 
 
	    private fun startCamera() { 
	        cameraExecutor = Executors.newSingleThreadExecutor() 
	        cameraProviderFuture = ProcessCameraProvider.getInstance(this) 
	        analyzer = MyImageAnalyzer(this) 
 
	        cameraProviderFuture?.addListener({ 
	            try { 
	                val processCameraProvider = cameraProviderFuture?.get() 
	                bindPreview(processCameraProvider) 
	            } catch (e: ExecutionException) { 
	                e.printStackTrace() 
	            } catch (e: InterruptedException) { 
	                e.printStackTrace() 
	            } 
	        }, ContextCompat.getMainExecutor(this)) 
	    } 
 
	    private fun bindPreview(processCameraProvider: ProcessCameraProvider?) { 
	        val preview = Preview.Builder().build() 
	        val cameraSelector = 
	            CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() 
	        preview.setSurfaceProvider(binding.preview.surfaceProvider) 
	        val imageCapture = ImageCapture.Builder().build() 
	        val imageAnalysis = ImageAnalysis.Builder().setTargetResolution(Size(1280, 720)) 
	            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build() 
	        imageAnalysis.setAnalyzer(cameraExecutor!!, analyzer!!) 
	        processCameraProvider?.unbindAll() 
	        processCameraProvider?.bindToLifecycle( 
	            this, 
	            cameraSelector, 
	            preview, 
	            imageCapture, 
	            imageAnalysis 
	        ) 
	    } 
 
	    class MyImageAnalyzer(private val activity: Activity) : ImageAnalysis.Analyzer { 
 
	        override fun analyze(image: ImageProxy) { 
	            scanBarCode(image) 
	        } 
 
	        @SuppressLint("UnsafeOptInUsageError") 
	        private fun scanBarCode(image: ImageProxy) { 
	            val image1 = image.image 
	            if (image1 != null) { 
	                val inputImage = InputImage.fromMediaImage(image1, image.imageInfo.rotationDegrees) 
	                val barcodeScannerOptions = BarcodeScannerOptions.Builder() 
	                    .setBarcodeFormats( 
	                        Barcode.FORMAT_QR_CODE, 
	                        Barcode.FORMAT_AZTEC 
	                    ) 
	                    .build() 
 
	                val scanner = BarcodeScanning.getClient(barcodeScannerOptions) 
	                scanner.process(inputImage) 
	                    .addOnSuccessListener { barcodes -> 
	                        // Task completed successfully 
	                        // ... 
	                        readerBarcodeData(barcodes) 
	                    } 
	                    .addOnFailureListener { 
	                        // Task failed with an exception 
	                        // ... 
	                    }.addOnCompleteListener { 
	                        image.close() 
	                    } 
	            } 
	        } 
 
	        private fun readerBarcodeData(barcodes: List<Barcode>) { 
	            for (barcode in barcodes) { 
	                Log.e( 
	                    "barcode recognize", "QR Code: " + barcode.displayValue 
	                ) //Returns barcode value in a user-friendly format. 
	                Log.e( 
	                    "barcode recognize", "Raw Value: " + barcode.rawValue 
	                ) //Returns barcode value as it was encoded in the barcode. 
	                Log.e( 
	                    "barcode recognize", "Code Type: " + barcode.valueType 
	                ) //This will tell you the type of your barcode 
	                Toast.makeText(activity, barcode.displayValue, Toast.LENGTH_SHORT).show() 
	            } 
	        } 
 
	    } 
	     
	    override fun onRequestPermissionsResult( 
	        requestCode: Int, 
	        permissions: Array<String>, 
	        grantResults: IntArray 
	    ) { 
	        if (requestCode == PERMISSION_CAMERA_REQUEST) { 
	            if (isCameraPermissionGranted()) { 
	                // start camera 
	            } else { 
	                Log.e(TAG, "no camera permission") 
	            } 
	        } 
	        super.onRequestPermissionsResult(requestCode, permissions, grantResults) 
	    } 
 
	    private fun isCameraPermissionGranted(): Boolean { 
	        return ContextCompat.checkSelfPermission( 
	            baseContext, 
	            Manifest.permission.CAMERA 
	        ) == PackageManager.PERMISSION_GRANTED 
	    } 
 
	    companion object { 
	        private val TAG = MainActivity::class.java.simpleName 
	        private const val PERMISSION_CAMERA_REQUEST = 1 
	    } 
	}