PlatformView + QR cod reader

Of the free available libraries for working with qr codes in Android, the best (in my opinion) is zxing-android-embedded. Often, the UI that this library provides is not enough or you need some other one. This article will discuss how customize UI library zxing-android-embedded to recognize QR codes when using its Flutter project.

The presented article and the code along with it are just a minimal sufficient example to demonstrate the possibilities of “customization” zxing-android to work with it in flutter. The article only touches on Android implementation without touching on IOS.

We will use three main components to interact with this library from the flutter environment. For this we need:

  • PlatformViewLink

  • MethodChannel

  • EventChannel

PlatformViewLink:

Allows you to “throw” the native android screen (View) into your fluttter application. This is convenient in cases where there is a ready-made, proven solution for the native platform, and there is not enough time to redo it for flutter and it is easier to show android activity directly in your flutter application. This is how google maps work in flutter applications. In our case, through PlatformViewLink, we will show the native screen with the camera stream.

MethodChannel:

Provides the ability to call native platform methods (android or ios, etc.) from the flutter environment in and get the result. It should be noted that all method calls are asynchronous. In this project, the MethodChannel will be used to turn the camera backlight on and off.

EventChannel:

Almost the same as MethodChannel, with the only difference that we can subscribe to the stream of events generated in the native environment. The most common case is, for example, “listening” to gps coordinates from the native platform. In this example, EventChannel will be used to send the recognized QR code from the android environment to our flutter application. Of course, to get the result, we could use the MethodChannel, for example, independently requesting data, say every 10 seconds. But this approach does not look very correct in conditions when we have the opportunity to get the result exactly when it is ready.

Let’s create an empty flutter project. In the terminal console of your favorite OS, run the command:

Let’s open main.dart… Add as a home element MaterialApp widget, QrCodePage – a widget that will wrap the main screen in Scaffold and add an AppBar for it:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: QrCodePage(),
    );
  }
}

class QrCodePage extends StatelessWidget {
  QrCodePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("QR code App"),
      ),
      body: PlatformView(),
    );
  }
}

PlatformView – basic Statefulwidge widget. It will be a wrapper over PlatformViewLink and will directly broadcast the video from the camera:

class _PlatformViewState extends State<PlatformView> {
  final MethodChannel platformMethodChannel = MethodChannel('flashlight');
  bool isFlashOn = false;
  bool permissionIsGranted = false;
  String result="";

  void _handleQRcodeResult() {
    const EventChannel _stream = EventChannel('qrcodeResultStream');
    _stream.receiveBroadcastStream().listen((onData) {
      print('EventChannel onData = $onData');
      result = onData;
      setState(() {});
    });
  }

  Future<void> _onFlash() async {
    try {
      dynamic result = await platformMethodChannel.invokeMethod('onFlash');
      setState(() {
        isFlashOn = true;
      });
    } on PlatformException catch (e) {
      debugPrint('PlatformException ${e.message}');
    }
  }

  Future<void> _offFlash() async {
    try {
      dynamic result = await platformMethodChannel.invokeMethod('offFlash');
      setState(() {
        isFlashOn = false;
      });
    } on PlatformException catch (e) {
      debugPrint('PlatformException ${e.message}');
    }
  }

  @override
  void initState() {
    super.initState();
    _handleQRcodeResult();
    _checkPermissions();
  }

  _requestAppPermissions() {
    showDialog(
        context: context,
        builder: (BuildContext context) => AlertDialog(
              title: const Text('Permission required'),
              content: const Text('Allow camera permissions'),
              actions: <Widget>[
                TextButton(
                  onPressed: () {
                    _checkPermissions();
                    Navigator.pop(context, 'OK');
                  },
                  child: const Text('OK'),
                ),
              ],
            ));
  }

  _checkPermissions() async {
    var status = await Permission.camera.status;
    if (!status.isGranted) {
      final PermissionStatus permissionStatus = await Permission.camera.request();
      if (!permissionStatus.isGranted) {
        _requestAppPermissions();
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final String viewType="<platform-view-type>";
    final Map<String, dynamic> creationParams = <String, dynamic>{};
    return result.isEmpty
        ? Stack(
            alignment: Alignment.center,
            children: [
              PlatformViewLink(
                viewType: viewType,
                surfaceFactory: (BuildContext context, PlatformViewController controller) {
                  return Container(
                    child: AndroidViewSurface(
                      controller: controller as AndroidViewController,
                      gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
                      hitTestBehavior: PlatformViewHitTestBehavior.opaque,
                    ),
                  );
                },
                onCreatePlatformView: (PlatformViewCreationParams params) {
                  return PlatformViewsService.initSurfaceAndroidView(
                    id: params.id,
                    viewType: viewType,
                    layoutDirection: TextDirection.ltr,
                    creationParams: creationParams,
                    creationParamsCodec: StandardMessageCodec(),
                  )
                    ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
                    ..create();
                },
              ),
              Align(
                  alignment: Alignment.topCenter,
                  child: ElevatedButton(
                      onPressed: () {
                        if (!isFlashOn) {
                          _onFlash();
                        } else {
                          _offFlash();
                        }
                      },
                      child: isFlashOn ? Text('off flashlight') : Text('on flashlight'))),
              Align(
                alignment: Alignment.center,
                child: Container(
                  height: 200,
                  width: 200,
                  decoration: BoxDecoration(
                      color: Colors.transparent,
                      border: Border.all(
                        color: Colors.blueAccent,
                        width: 5,
                      )),
                ),
              )
            ],
          )
        : Container(
            child: Center(child: Text('QR code result:n$result')),
          );
  }
}

A couple of comments to the code are yours:

  • In the class field, create platformMethodChannel – through this instance we will call native methods (which we will create a little later) in the android environment. The argument in the ‘flashlight’ constructor is a kind of unique ID that must be identical in flutter and native:

  • Methods _onFlash () and _offFlash() call the corresponding method on the Android side of the framework.

  • In some cases, you need to pass parameters to the native environment. For this it is convenient to use creationParams… But in our example, we will not have parameters for passing:

  • As ViewGroup use Stack in order to arrange additional UI elements. In my example, this is a frame in the center of the screen (Container with transparent background and BoxDecoration) and ElevatedButton above it to turn on the backlight.

Let’s take a look at the Android implementation:

V build.gradle the app module (android / app / build.gradle), connect the library. Add to the dependencies section:

In MainActivity, in the method configureFlutterEngine, EventChannel

class MainActivity : FlutterFragmentActivity(), LifecycleOwner, ResultCallback {
    var myEvents: EventChannel.EventSink? = null

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, "qrcodeResultStream")
                .setStreamHandler(object : EventChannel.StreamHandler {
                    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                        myEvents = events
                    }

                    override fun onCancel(arguments: Any?) {
                        myEvents = null
                    }
                })

        flutterEngine
                .platformViewsController
                .registry
                .registerViewFactory("<platform-view-type>", NativeViewFactory())
    }

    override fun result(result: String) {
        myEvents?.success(result)
    }

    override fun getMyFlutterEngine(): FlutterEngine? = flutterEngine
}

EventChannel.StreamHandler returns us an object EventChannel.EventSink calling on which .success (result) – we pass the event to the flutter framework. In our case, it will be a line with a QR code.

In the method above, we register a factory that can return different Views depending on the arguments passed, but we will not complicate the example and return our only one NativeView:

Let’s take a look at the interface ResultCallbackwhich implements MainActivity:

Method result(result: String) needed to transfer the result (recognized qr code) to MainActivity

method getMyFlutterEngine () – will return us FlutterEngine in our NativeView

The main code will be in NativeView:

class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
    private val textView: TextView
    private val CHANNEL = "flashlight"

    private val rootView: View
    private var barcodeView: DecoratedBarcodeView? = null
    override fun getView(): View {
        return rootView
    }

    override fun dispose() {}

    init {
        (context as LifecycleOwner).lifecycle.addObserver(object : LifecycleObserver {
            @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
            fun connectListener() {
                barcodeView?.resume()
            }

            @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
            fun disconnectListener() {
                barcodeView?.pause()
            }
        })

        rootView = LayoutInflater.from(context.applicationContext).inflate(R.layout.layout, null)

        barcodeView = rootView.findViewById<DecoratedBarcodeView>(R.id.barcode_scanner)
        val formats: Collection<BarcodeFormat> = Arrays.asList(BarcodeFormat.QR_CODE, BarcodeFormat.CODE_39)
        barcodeView?.barcodeView?.decoderFactory = DefaultDecoderFactory(formats)
        barcodeView?.setStatusText("")
        barcodeView?.viewFinder?.visibility = View.INVISIBLE

        barcodeView?.initializeFromIntent(Intent())
        barcodeView?.decodeContinuous(object : BarcodeCallback {
            override fun possibleResultPoints(resultPoints: MutableList<ResultPoint>?) {
                super.possibleResultPoints(resultPoints)
            }

            override fun barcodeResult(result: BarcodeResult?) {
                (context as ResultCallback).result(result?.result?.text ?: "no result")
                barcodeView?.setStatusText(result?.text)
            }
        })

        barcodeView?.resume()
        textView = TextView(context)
        textView.textSize = 36f
        textView.setBackgroundColor(Color.rgb(255, 255, 255))
        textView.text = "Rendered on a native Android view (id: $id) ${creationParams?.entries}"

        val flutterEngine = (context as ResultCallback).getMyFlutterEngine()
        MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CHANNEL)
                .setMethodCallHandler { call, result ->
                    when (call.method) {
                        "onFlash" -> {
                            barcodeView?.setTorchOn()
                            result.success("setTorchOn")
                        }

                        "offFlash" -> {
                            barcodeView?.setTorchOff()
                            result.success("setTorchOff")
                        }
                        else -> {
                            result.notImplemented()
                        }
                    }
                }
    }
}

V init block, subscribe to the activiti life cycle and call resume / pause at barcodeView… Important: that without implementing these methods, you will see a black screen instead of a video stream from the camera:

NativeView inherits from interface PlatformView this obliges us to implement two methods:

V getView we must return View which is the main screen. Need to create layout.xml with the following content:

From it using LayoutInflater we create view and return a link to it in the method getView():

Since our layout contains DecoratedBarcodeView we can find it (get a link to it) with findViewById and configure as needed:

Here we set the supported format of qr codes, set the default result line as empty, remove the standard frame in the center of the screen. We should also dwell on this piece of code:

When the library recognizes the qr code, it passes the result to the callback – barcodeResult(result: BarcodeResult?)… In it, having a link to MainActivity through the general context, we call the method result our ResultCallback and through it we pass the string with the result. And already in the very MainActivity using EventChannel we pass on to the Flutter environment.

The code above is a handler for events dispatched from the flutter environment. Have MethodChannel takes MethodCallHandler using which we find out which method is currently being called and respond to it. In this code, we enable or disable the camera backlight.

A short video with an example of this application:

Application source code

zxing-android-embedded

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *