DEV Community

Cover image for Integrating ZXing Android Embedded in a Compose app
Thomas Künneth
Thomas Künneth

Posted on

Integrating ZXing Android Embedded in a Compose app

Jetpack Compose is the preferred UI framework for new Android apps. Its declarative programming model makes writing beautiful user interfaces a breeze. But what if you want to re-use existing code that relies on the traditional View system? Through the years countless wonderful custom components have been developed. There may be Compose versions of them some day. But, no need to wait. Jetpack Compose includes powerful yet easy to use interoperability apis.

In this short article, I show you how to integrate ZXing Android Embedded in a Compose app. The sample app is part of my upcoming book Android UI Development with Jetpack Compose. It will be published by Packt and should be available early 2022. You can find the source code on GitHub.

Screenshot of the ZxingDemo app

ZxingDemo uses the ZXing Android Embedded Barcode scanner library for Android, which is based on the ZXing decoder. It is released under the terms of the Apache-2.0 License, and is hosted on GitHub.

The aim of ZximgDemo is to showcase the integration of Views in a Compose app, not to really provide a useful app. It uses ZXing Android Embedded to continuously scan Barcodes and QR-Codes and prints the result in screen.

To use the library, just add it as an implementation dependency to your module-level build.gradle file.

implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
Enter fullscreen mode Exit fullscreen mode

We need to access the camera, so we need to request permissions. Here's the preferred way to do so:

private lateinit var barcodeView: DecoratedBarcodeView

private val requestPermission =
  registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
    if (isGranted) {
      barcodeView.resume()
    }
  }

override fun onResume() {
  super.onResume()
  requestPermission.launch(Manifest.permission.CAMERA)
}

override fun onPause() {
  super.onPause()
  barcodeView.pause()
}
Enter fullscreen mode Exit fullscreen mode

ActivityResultContracts.RequestPermission replaces the process around overriding onRequestPermissionsResult().

barcodeView is initialized in onCreate(). Let's take a look.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  val beepManager = BeepManager(this)
  val root = layoutInflater.inflate(R.layout.layout, null)
  barcodeView = root.findViewById(R.id.barcode_scanner)
  val formats = listOf(BarcodeFormat.QR_CODE, BarcodeFormat.CODE_39)
  barcodeView.barcodeView.decoderFactory = DefaultDecoderFactory(formats)
  barcodeView.initializeFromIntent(intent)
  val callback = object : BarcodeCallback {
    override fun barcodeResult(result: BarcodeResult) {
      if (result.text == null || result.text == text.value) {
        return
      }
      text.value = result.text
      beepManager.playBeepSoundAndVibrate()
    }
  }
  barcodeView.decodeContinuous(callback)
  setContent {
    val state = text.observeAsState()
    state.value?.let {
      ZxingDemo(root, it)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

barcodeView references a child element inside the component tree I inflated using layoutInflater.inflate(), and assigned to root. The layout (R.layout.layout represents layout.xml) is very simple:

<?xml version="1.0" encoding="utf-8"?>
<com.journeyapps.barcodescanner.DecoratedBarcodeView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/barcode_scanner"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:layout_alignParentTop="true" />
Enter fullscreen mode Exit fullscreen mode

So, the barcode view is provided by DecoratedBarcodeView as one of its children. Once we have obtained a reference to the child, we configure it. You can find more information on the in the ZXing Android Embedded documentation.

The Compose-specific part happens inside setContent {}.

  • We create state using observeAsState()
  • We invoke a composable named ZxingDemo() and pass the value of the state, and root

text is defined like this:

private val text = MutableLiveData("")
Enter fullscreen mode Exit fullscreen mode

It is updated inside callback when the scanner engine has provided a result.

Before we look at ZxingDemo(), let's briefly recap:

  • root represents the scanner ui
  • When the scanner has a result, it updates text (a MutableLiveData instance
  • ZxingDemo() receives the value of a state based on text and the root of the scanner ui

Now it's time to see how the integration is achieved:

@Composable
fun ZxingDemo(root: View, value: String) {
  Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.TopCenter
  ) {
    AndroidView(modifier = Modifier.fillMaxSize(),
      factory = {
        root
      })
    if (value.isNotBlank()) {
      Text(
        modifier = Modifier.padding(16.dp),
        text = value,
        color = Color.White,
        style = MaterialTheme.typography.h4
      )
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We define a Box() with two children, AndroidView() and Text(). AndroidView() receives a factory, which just returns root (the scanner ui). The docs say:

Composes an Android View obtained from factory. The factory block will be called exactly once to obtain the View to be composed, and it is also guaranteed to be invoked on the UI thread. Therefore, in addition to creating the factory, the block can also be used to perform one-off initializations and View constant properties' setting.

You may be wondering why I inflate the object tree in onCreate() instead of inside the factory lambda. Well, configuring the barcode scanner should not be done in a composable because it might be inherently (preparing camera and preview, ...). Also, parts of the the component tree are accessed from the outside (on the activity level), so we need references to children anyway (barcodeView.

You could also provide an update block, which my example doesn't. It runs right after the factory block completes, and it can run multiple times due to recomposition. You can use it to set View properties, depending on state.

The Compose part of my app does not alter state which needs to be applied to the scanner component tree.

Conclusion

As ZxingDemo shows, it is very easy to integrate libraries that provide custom components. You you need to structure your code depends on how the library is setup and configured. Do you plan to use the Jetpack Compose interop apis? Please share your thoughts in the comments.

Discussion (0)