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.
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 View
s 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'
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()
}
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)
}
}
}
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" />
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, androot
text
is defined like this:
private val text = MutableLiveData("")
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
(aMutableLiveData
instance -
ZxingDemo()
receives the value of a state based ontext
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
)
}
}
}
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 fromfactory
. Thefactory
block will be called exactly once to obtain theView
to be composed, and it is also guaranteed to be invoked on the UI thread. Therefore, in addition to creating thefactory
, 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.
Top comments (1)
Could you show me how to integrate zbar into Jetpack Compose, thanks.