How to Implement a Flutter QR Code Scanner Plugin from Scratch
Tags: flutter, android, qrcode, scanner
Published: 2021-11-02 01:58:09

If you want to create a Flutter camera application, a quick way is to install the official [Camera plugin](https://pub.dev/packages/camera). However, the plugin is still under development, and there are no vision APIs available yet. This article will show you how to implement a custom Flutter camera view widget by wrapping an Android native camera view which supports QR code scanning.

## Installation 
- [Dynamsoft Camera Enhancer](https://www.dynamsoft.com/camera-enhancer/docs/introduction/):

    ```gradle
    allprojects {
        repositories {
            google()
            jcenter()
            maven {
                url "https://download.dynamsoft.com/maven/dce/aar"
            }
        }
    }
    dependencies {
        implementation 'com.dynamsoft:dynamsoftcameraenhancer:2.0.0@aar'
    }
    ```

- [Dynamsoft Barcode Reader](https://www.dynamsoft.com/barcode-reader/overview/):

    ```gradle
    allprojects {
        repositories {
            google()
            jcenter()
            maven {
                url "https://download2.dynamsoft.com/maven/dbr/aar"
            }
        }
    }
    
    dependencies {
        implementation 'com.dynamsoft:dynamsoftbarcodereader:8.8.0@aar'
    }
    ```
    
    [![dbr trial license](https://img.shields.io/badge/Get-30--day%20FREE%20Trial%20License-blue)](https://www.dynamsoft.com/customer/license/trialLicense/?product=dbr)

## Creating A Simple Flutter Widget from Android TextView
In this section, let's follow [Flutter's tutorial](https://flutter.dev/docs/development/platform-integration/platform-views?tab=android-platform-views-java-tab) to host an Android `TextView` in Flutter widget with `PlatformView`. 

1. Create a new Flutter plugin project:

    ```bash
    flutter create --org com.dynamsoft --template=plugin --platforms=android -a java flutter_qrcode_scanner
    ```

2. On the Dart side, we can see the following code in `lib/flutter_qrcode_scanner.dart`:

    ```dart

    import 'dart:async';
    
    import 'package:flutter/services.dart';
    
    class FlutterQrcodeScanner {
      static const MethodChannel _channel = MethodChannel('flutter_qrcode_scanner');
    
      static Future<String?> get platformVersion async {
        final String? version = await _channel.invokeMethod('getPlatformVersion');
        return version;
      }
    }

    ```

    Delete the code snippet above, and define a stateful widget as follows:
    
    ```dart
    class ScannerView extends StatefulWidget {
      const ScannerView({Key? key}) : super(key: key);
      @override
      State<StatefulWidget> createState() => _ScannerViewState();
    }
    
    class _ScannerViewState extends State<ScannerView> {
      @override
      void initState() {
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        const String viewType = 'com.dynamsoft.flutter_qrcode_scanner/nativeview';
        final Map<String, dynamic> creationParams = <String, dynamic>{};
    
        return AndroidView(
          viewType: viewType,
          creationParams: creationParams,
          creationParamsCodec: const StandardMessageCodec(),
        );
      }
    }    
    ```

3. On the platform side, we create `NativeView` class which extends `io.flutter.plugin.platform.PlatformView` to provide a reference to `TextView`:

    ```java
    import android.content.Context;
    import android.graphics.Color;
    import android.view.View;
    import android.widget.TextView;
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import io.flutter.plugin.platform.PlatformView;
    import java.util.Map;
    
    class NativeView implements PlatformView {
        @NonNull private final TextView textView;
     
         NativeView(@NonNull Context context, int id, @Nullable Map<String, Object> creationParams) {
             textView = new TextView(context);
             textView.setTextSize(72);
             textView.setBackgroundColor(Color.rgb(255, 255, 255));
             textView.setText("Rendered on a native Android view (id: " + id + ")");
         }
     
         @NonNull
         @Override
         public View getView() {
             return textView;
         }
     
         @Override
         public void dispose() {}
    }
    ``` 
    
    Then create a `NativeViewFactory` class to initialize `NativeView`:
    
    ```java
    import android.content.Context;
    import android.view.View;
    import androidx.annotation.Nullable;
    import androidx.annotation.NonNull;
    import io.flutter.plugin.common.BinaryMessenger;
    import io.flutter.plugin.common.StandardMessageCodec;
    import io.flutter.plugin.platform.PlatformView;
    import io.flutter.plugin.platform.PlatformViewFactory;
    import java.util.Map;

    class NativeViewFactory extends PlatformViewFactory {
      @NonNull private final BinaryMessenger messenger;

      NativeViewFactory(@NonNull BinaryMessenger messenger) {
        super(StandardMessageCodec.INSTANCE);
        this.messenger = messenger;
      }

      @NonNull
      @Override
      public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
        final Map<String, Object> creationParams = (Map<String, Object>) args;
        return new NativeView(context, id, creationParams);
      }
    }
    ```

    In `FlutterQrcodeScannerPlugin.java` file, we register the factory class:
    
    ```java
    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
      channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "flutter_qrcode_scanner");
      channel.setMethodCallHandler(this);
      flutterPluginBinding.getPlatformViewRegistry().registerViewFactory(
          "com.dynamsoft.flutter_qrcode_scanner/nativeview",
          new NativeViewFactory(flutterPluginBinding.getBinaryMessenger()));
    }
    ```

Now, the plugin is ready to be used. We change the code of `example/lib/main.dart` to make it work:

```dart
import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_qrcode_scanner/flutter_qrcode_scanner.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: ScannerView(),
        ),
      ),
    );
  }
}
```

Run the example:

```
flutter run
```

![custom Flutter text widget from Android TextView](https://www.dynamsoft.com/codepool/img/2021/10/flutter-custom-widget-android-native-view.jpg)

## Implementing a Flutter Camera Widget from Android Camera View
If the code above can work for you, the next step is to replace the text view with a camera view. Here we use Dynamsoft Camera Enhancer as an example.

1. Configure `android/build.gradle`:
    
    ```gradle
    rootProject.allprojects {
        repositories {
            google()
            mavenCentral()
            maven {
                url "https://download.dynamsoft.com/maven/dce/aar"
            }
        }
    }
    
    android {
        compileSdkVersion 30

        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }

        defaultConfig {
            minSdkVersion 21
        }
    }

    dependencies {
        implementation 'com.dynamsoft:dynamsoftcameraenhancer:2.0.0@aar'
    }
    ```

    The minimum Android SDK version needs to be changed to 21 or higher. 

2. In `NativeView.java`, import the camera package:
    
    ```java
    import com.dynamsoft.dce.*;
    ```

    Change `TextView` to `DCECameraView`. With `CameraEnhancer`, we can start and stop the camera preview:
    
    ```java
    private final DCECameraView cameraView;
    private CameraEnhancer cameraEnhancer;

    NativeView(BinaryMessenger messenger, @NonNull Activity context, int id,
            @Nullable Map<String, Object> creationParams) {
        this.context = context;

        cameraEnhancer = new CameraEnhancer(context);
        cameraView = new DCECameraView(context);
        cameraEnhancer.setCameraView(cameraView);
        try {
            cameraEnhancer.open();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    ```

    In order to switch camera status along with activity lifecycle, we implement `Application.ActivityLifecycleCallbacks`:
    
    ```java
    class NativeView implements PlatformView, Application.ActivityLifecycleCallbacks {
        @Override
        public void onActivityResumed(Activity activity) {
            try {
                cameraEnhancer.open();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onActivityPaused(Activity activity) {
            try {
                cameraEnhancer.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    ```
3. You may have noticed that we use `Activity` instead of `Context` in `NativeView` class. So the `NativeViewFactory` class needs to be changed as well:
    
    ```java
    class NativeViewFactory extends PlatformViewFactory {
      @NonNull private final BinaryMessenger messenger;
      @NonNull private Activity activity;

      NativeViewFactory(@NonNull BinaryMessenger messenger, Activity activity) {
        super(StandardMessageCodec.INSTANCE);
        this.messenger = messenger;
        this.activity = activity;
      }

      @NonNull
      @Override
      public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
        final Map<String, Object> creationParams = (Map<String, Object>) args;
        return new NativeView(messenger, activity, id, creationParams);
      }
    }
    ```
4. Finally, in `FlutterQrcodeScannerPlugin` class, we monitor activity status and pass the activity reference to `NativeViewFactory` instance:
    
    ```java
    public class FlutterQrcodeScannerPlugin implements FlutterPlugin, ActivityAware {
        private Activity activity;
        private FlutterPluginBinding flutterPluginBinding;

        @Override
        public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
          this.flutterPluginBinding = flutterPluginBinding;
        }

        @Override
        public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
          this.flutterPluginBinding = null;
        }

        @Override
        public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) {
          bind(activityPluginBinding);
        }

        @Override
        public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) {
          bind(activityPluginBinding);
        }

        @Override
        public void onDetachedFromActivityForConfigChanges() {
          activity = null;
        }

        @Override
        public void onDetachedFromActivity() {
          activity = null;
        }

        private void bind(ActivityPluginBinding activityPluginBinding) {
          activity = activityPluginBinding.getActivity();
          flutterPluginBinding.getPlatformViewRegistry().registerViewFactory(
              "com.dynamsoft.flutter_qrcode_scanner/nativeview",
              new NativeViewFactory(flutterPluginBinding.getBinaryMessenger(), activity));
        }
    }
    ```

A Flutter camera view widget is done. But before running the example, we have to change the minimum SDK version to 21 in `example/android/build.gradle`.

## Turning Flutter Camera Plugin into Flutter QR Code Scanner
Generally, we can take camera frames from preview callback function and do further image processing. To save developer's time, a good camera widget should provide vision functionalities. In the following paragraph, you will see how to integrate Dynamsoft Barcode Reader SDK into the current Flutter camera view widget step by step.

1. The same to using the camera SDK, we add the barcode package to `build.gradle`:
    
    ```gradle
    rootProject.allprojects {
        repositories {
            google()
            mavenCentral()
            maven {
                url "https://download.dynamsoft.com/maven/dce/aar"
            }
            maven {
                url "https://download.dynamsoft.com/maven/dbr/aar"
            }
        }
    }
    
    android {
        compileSdkVersion 30

        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }

        defaultConfig {
            minSdkVersion 21
        }
    }

    dependencies {
        implementation 'com.dynamsoft:dynamsoftcameraenhancer:2.0.0@aar'
        implementation 'com.dynamsoft:dynamsoftbarcodereader:8.8.0@aar'
    }
    ```

2. Dynamsoft Camera Enhancer and Dynamsoft Barcode Reader SDK can co-work well. We put them into a `QRCodeScanner.java` file to implement real-time QR code scanning:
    
    ```java
    public class QRCodeScanner {
        private CameraEnhancer mCameraEnhancer;
        private BarcodeReader reader;
        private Activity context;
        private DCECameraView cameraView;
        private DetectionHandler handler;

        public interface DetectionHandler {
            public void onDetected(List<Map<String, Object>> data);
        }

        public QRCodeScanner(Activity context, DCECameraView cameraView) {
            this.context = context;
            this.cameraView = cameraView;
            mCameraEnhancer = new CameraEnhancer(context);
            mCameraEnhancer.setCameraView(cameraView);
            cameraView.setOverlayVisible(true);

            DCESettingParameters dceSettingParameters = new DCESettingParameters();
            dceSettingParameters.cameraInstance = mCameraEnhancer;
            dceSettingParameters.textResultCallback = mTextResultCallback;

            try {
                // mCameraEnhancer.open();
                reader = new BarcodeReader();
                reader.SetCameraEnhancerParam(dceSettingParameters);
                PublicRuntimeSettings settings = reader.getRuntimeSettings();
                settings.barcodeFormatIds = EnumBarcodeFormat.BF_QR_CODE;
                reader.updateRuntimeSettings(settings);
            } catch (Exception e) {
                // TODO: handle exception
            }
        }

        TextResultCallback mTextResultCallback = new TextResultCallback() {
            @Override
            public void textResultCallback(int i, TextResult[] results, Object userData) {
                if (results != null) {
                    final List<Map<String, Object>> ret = new ArrayList<Map<String, Object>>();
                    for (TextResult result: results) {
                        final Map<String, Object> data = new HashMap<>();
                        data.put("format", result.barcodeFormatString);
                        data.put("text", result.barcodeText);
                        data.put("x1", result.localizationResult.resultPoints[0].x);
                        data.put("y1", result.localizationResult.resultPoints[0].y);
                        data.put("x2", result.localizationResult.resultPoints[1].x);
                        data.put("y2", result.localizationResult.resultPoints[1].y);
                        data.put("x3", result.localizationResult.resultPoints[2].x);
                        data.put("y3", result.localizationResult.resultPoints[2].y);
                        data.put("x4", result.localizationResult.resultPoints[3].x);
                        data.put("y4", result.localizationResult.resultPoints[3].y);
                        data.put("angle", result.localizationResult.angle);
                        ret.add(data);
                    }

                    if (handler != null) {
                        handler.onDetected(ret);
                    }
                }
            }
        };

        public void setDetectionHandler(DetectionHandler handler) {
            this.handler = handler;
        }

        public void startScan() {
            try {
                // mCameraEnhancer.open();
                cameraView.setOverlayVisible(true);
                reader.StartCameraEnhancer();
            } catch (Exception e) {
                // TODO: handle exception
            }
        }

        public void stopScan() {
            try {
                // mCameraEnhancer.close();
                cameraView.setOverlayVisible(false);
                reader.StopCameraEnhancer();
            } catch (Exception e) {
                // TODO: handle exception
            }
        }

        public void setLicense(String license) {
            try {
                reader.initLicense(license);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    ```
    
    To activate the QR code detection API, a [valid license](https://www.dynamsoft.com/customer/license/trialLicense?product=dbr) is required.

3. As a QR code detected, we need a way to send the results from Java to Dart. The `MethodChannel` class is used to send and receive messages between Dart and native code. Therefore, we add the following code to `NativeView.java`:
    
    ```java
    class NativeView implements PlatformView, MethodCallHandler, Application.ActivityLifecycleCallbacks, QRCodeScanner.DetectionHandler {
        @Override
        public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
            switch (call.method) {
            case "startScanning":
                qrCodeScanner.startScan();
                result.success(null);
                break;
            case "stopScanning":
                qrCodeScanner.stopScan();
                result.success(null);
                break;
            case "setLicense":
                final String license = call.argument("license");
                qrCodeScanner.setLicense(license);
                result.success(null);
                break;
            case "setBarcodeFormats":
                final int formats = call.argument("formats");
                qrCodeScanner.setBarcodeFormats(formats);
                result.success(null);
                break;
            default:
                result.notImplemented();
            }
        }
    }
    ```
    
    When QR code is detected, the `onDetected` callback function will be triggered. One thing you have to know is the function is called by a worker thread, whereas the method channel communication has to be invoked in the main thread. To achieve this, we use `runOnUiThread` to send messages to the main thread:
    
    ```java
    @Override
    public void onDetected(List<Map<String, Object>> data) {
        context.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                methodChannel.invokeMethod("onDetected", data);
            }
        });
    }
    ```
4. On the Dart side, we define a `ScannerViewController` in `flutter_qrcode_scanner.dart` to receive QR code detection results:
    
    ```dart
    class _ScannerViewState extends State<ScannerView> {
      ScannerViewController? _controller;

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

      @override
      Widget build(BuildContext context) {
        const String viewType = 'com.dynamsoft.flutter_qrcode_scanner/nativeview';
        final Map<String, dynamic> creationParams = <String, dynamic>{};

        return AndroidView(
          viewType: viewType,
          onPlatformViewCreated: _onPlatformViewCreated,
          creationParams: creationParams,
          creationParamsCodec: const StandardMessageCodec(),
        );
      }

      void _onPlatformViewCreated(int id) {
        _controller = ScannerViewController(id);
        widget.onScannerViewCreated(_controller!);
      }
    }

    class ScannerViewController {
      late MethodChannel _channel;
      final StreamController<List<BarcodeResult>> _streamController =
          StreamController<List<BarcodeResult>>();
      Stream<List<BarcodeResult>> get scannedDataStream => _streamController.stream;

      ScannerViewController(int id) {
        _channel =
            MethodChannel('com.dynamsoft.flutter_qrcode_scanner/nativeview_$id');
        _channel.setMethodCallHandler((call) async {
          switch (call.method) {
            case 'onDetected':
              if (call.arguments != null) {
                List<BarcodeResult> data = fromPlatformData(call.arguments as List);
                _streamController.sink.add(data);
              }
              break;
          }
        });
      }
    }
    ```

    The functions for invoking native methods include:
    
    ```dart
    Future<void> startScanning() async {
      await _channel.invokeMethod('startScanning');
    }

    Future<void> stopScanning() async {
      await _channel.invokeMethod('stopScanning');
    }

    /// Apply for a 30-day FREE trial license: https://www.dynamsoft.com/customer/license/trialLicense
    Future<void> setLicense(String license) async {
      await _channel.invokeMethod('setLicense', {'license': license});
    }
    ```

So far, the Flutter QR code scanner plugin has been completely finished. Here is the full example of a simple QR code scanner:

```dart
import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_camera_qrcode_scanner/flutter_qrcode_scanner.dart';
import 'package:flutter_camera_qrcode_scanner/dynamsoft_barcode.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ScannerViewController? controller;
  String _barcodeResults = '';

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('QR Code Scanner'),
        ),
        body: Stack(children: <Widget>[
          ScannerView(onScannerViewCreated: onScannerViewCreated),
          Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Container(
                height: 100,
                child: SingleChildScrollView(
                  child: Text(
                    _barcodeResults,
                    style: TextStyle(fontSize: 14, color: Colors.white),
                  ),
                ),
              ),
              Container(
                height: 100,
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: <Widget>[
                      MaterialButton(
                          child: Text('Start Scan'),
                          textColor: Colors.white,
                          color: Colors.blue,
                          onPressed: () async {
                            controller!.startScanning();
                          }),
                      MaterialButton(
                          child: Text("Stop Scan"),
                          textColor: Colors.white,
                          color: Colors.blue,
                          onPressed: () async {
                            controller!.stopScanning();
                          })
                    ]),
              ),
            ],
          )
        ]),
      ),
    );
  }

  void onScannerViewCreated(ScannerViewController controller) {
    setState(() {
      this.controller = controller;
    });
    controller.setLicense('LICENSE-KEY');
    controller.startScanning(); // auto start scanning
    controller.scannedDataStream.listen((results) {
      setState(() {
        _barcodeResults = getBarcodeResults(results);
      });
    });
  }

  String getBarcodeResults(List<BarcodeResult> results) {
    StringBuffer sb = new StringBuffer();
    for (BarcodeResult result in results) {
      sb.write(result.format);
      sb.write("\n");
      sb.write(result.text);
      sb.write("\n\n");
    }
    if (results.isEmpty) sb.write("No QR Code Detected");
    return sb.toString();
  }

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}
```

![Flutter QR code scanner](https://www.dynamsoft.com/codepool/img/2021/10/flutter-qr-code-scanner.jpg)

## Download Flutter QR Code Scanner from pub.dev
[https://pub.dev/packages/flutter_camera_qrcode_scanner](https://pub.dev/packages/flutter_camera_qrcode_scanner)

## Source Code
[https://github.com/yushulx/flutter_qrcode_scanner](https://github.com/yushulx/flutter_qrcode_scanner)