How to integrate Flutter ZXing into your mobile app

Kevin September 5, 2024 13 mins read
app store

Flutter ZXing is a plugin for scanning and generating barcodes using the ZXing barcode scanning library. The plugin is implemented using the Dart FFI (Foreign Function Interface) and the zxing-cpp library and allows you to integrate barcode scanning and generation functionalities into your Flutter apps.

In this tutorial, we’ll walk you through the process of setting up a Flutter project and implementing an Android barcode scanner using flutter_zxing.

Prerequisites

Before you begin, ensure you have the following tools and components installed:

  1. Android Studio or Visual Studio Code: You need an Integrated Development Environment (IDE) to write and run your Flutter code. Android Studio is recommended for its support for Android development.
  2. Android device or emulator: You will need a physical Android device or an Android emulator set up in your IDE to test your application.

Setting up the Flutter environment

To start, you have to install the Flutter SDK:

  1. Visit the Flutter installation guide.
  2. Select your operating system from the available options.
  3. Choose Android as your development platform.
  4. Follow the instructions under Download and Install to download and unzip the Flutter SDK. When unzipping the file, pay attention to any path restrictions mentioned in the guide.
  5. If you’re a Windows user, after unzipping the SDK, make sure to update your Windows Path Variable as detailed in the guide.

Next, you’ll need to set up your Android development environment:

  1. Visit the Flutter guide for Android setup.
  2. Follow the instructions specific to your situation—whether you’re a new or existing Android Studio user. Choose the guide that best suits you.
  3. Agree to the Flutter Android licenses by following the license agreement instructions.
  4. Finally, verify your development setup to ensure everything is configured correctly.

Creating the app

The following steps cover all the steps to create the Flutter barcode scanner app.

Step 1: Create the project

Before diving into the barcode scanner functionality, let’s start by setting up a basic Flutter project. This will lay the groundwork for all the features we’ll add later on. Follow these steps to get your project up and running:

  1. Open your terminal and access the directory where you want to create the app.
  2. Create a new Flutter project by running the following command:
flutter create barcode_scanner
    cd barcode_scanner
      flutter run
        ZXing Flutter tutorial
        ZXing Flutter tutorial

        Step 2: Install the Flutter ZXing plugin

        With your basic Flutter app up and running, it’s time to start building the barcode scanner functionality. To do this, you’ll use the Flutter ZXing plugin. Follow these steps to install the plugin and configure your app:

        1. Open your project in Android Studio. Once it is open, switch to the Project Files view to locate the necessary folders and files easily.
        ZXing Flutter tutorial
          dependencies:  
            flutter:  
              sdk: flutter  
            cupertino_icons: ^1.0.8  
            flutter_zxing: ^1.7.0
            flutter pub get

            Step 3: Configure the app

            Now that the basics are set up, it’s time to configure the app as a barcode scanner. Start by opening the main.dart file in the lib folder. Delete the default Flutter code to begin building your barcode scanner app from scratch.

            3.1: Import the packages and create the main function

            The first step in configuring your app is to import the required packages and set up the main() function:

            import 'package:flutter/foundation.dart';  
            import 'package:flutter/material.dart';  
            import 'package:flutter_zxing/flutter_zxing.dart';
            
            void main() {  
              runApp(const MyApp());  
            }

            3.2: Create the MyApp class

            Next, you’ll create the MyApp class, which is a stateless widget that serves as the root of your application. This class returns a MaterialApp widget, providing the basic structure and theme for your app.

            1. MaterialApp: A wrapper that includes app title, debugShowCheckedModeBanner, and home settings.
            2. home: Points to the DemoPage widget, which will be the main screen of the app.

            Below is the code to set up the MyApp class:

            class MyApp extends StatelessWidget {  
              const MyApp({super.key});
            
              @override  
              Widget build(BuildContext context) {  
                return const MaterialApp(  
                  title: 'Flutter ZXing Barcode Scanner',  
                  home: DemoPage(),  
                );  
              }  
            }

            3.3. Create the DemoPage widget

            The DemoPage widget is the core of the barcode scanner app, handling its main functionality. In Flutter, when a widget needs to manage a mutable state, you use a StatefulWidget along with its corresponding State class. In this case, the mutable state is necessary to track the scanned barcode, which determines whether to display the barcode scan result.

            The DemoPage setup consists of:

            1. DemoPage StatefulWidget: Manages the state of the app’s main screen.
            2. _DemoPageState: Contains the logic and functions behind the main screen, including tracking the barcode scan result. The result is stored as a nullable Code object.

            The following table lists the methods within the _DemoPageState class:

            MethodDescriptionFunctionality
            build(BuildContext)The primary method for building the widget tree. It checks the platform support and either shows the scanner interface or an unsupported platform message.The build method is called whenever setState is invoked, triggering a rebuild of the widget tree. It constructs the UI based on the current state. If the platform supports the camera, it displays either the ScanResultWidget (if a valid scan result exists) or the scanner interface (by calling _buildScanner). If not, it displays the UnsupportedPlatformWidget. ScanResult and UnsupportedPlatform are external widgets which you’ll create in the following tutorial sections.
            _isCameraSupported()Helper method to check if the current platform supports the camera.Returns a boolean value indicating whether the current platform supports camera functionality. It specifically checks for iOS and Android platforms, which are supported by the flutter_zxing library.
            _buildScanner()Helper method that returns the ReaderWidget, which is used to scan barcodes.Constructs the scanner interface using the ReaderWidget. It configures the scanner with parameters such as resolution, lens direction, and scan format. It also provides callback methods (onScan, onScanFailure, onControllerCreated) to handle scan results and errors. The ReaderWidget is provided by the flutter_zxing library. For a list of all configurations available, access the GitHub repository.
            _onControllerCreated(_, Exception? error)Callback method triggered when the scanner controller is created. Handles any errors that occur during the controller’s creation.Called when the ReaderWidget‘s controller is created. If an error occurs during creation, such as permission issues, it calls _showMessage to display an error message to the user.
            _onScanSuccess(Code? code)Callback method that is called when a barcode is successfully scanned. It updates the result state and triggers a UI rebuild.Triggered when a barcode is successfully scanned. It updates the result variable with the scanned code data and calls setState to rebuild the widget tree. The UI is then updated to display the scan result using ScanResultWidget.
            _onScanFailure(Code? code)Callback method that handles scan failures. It updates the result state and displays an error message if available.Triggered when a scan attempt fails. It updates the result variable with the failed code data and calls setState to rebuild the widget tree. If the failed code contains an error message, it is displayed to the user via _showMessage.
            _showMessage(BuildContext, String message)Helper method that displays a SnackBar with a provided message.Displays a message to the user related to errors or important information. It first hides any currently visible SnackBar and then shows a new SnackBar with the specified message. This ensures that the user is notified of any issues or updates during scanning.

            The following code block presents the code for DemoPage and _DemoPageState:

            class MyApp extends StatelessWidget {  
             const MyApp({super.key});
            
             @override  
             Widget build(BuildContext context) {  
               return const MaterialApp(  
                 title: 'Flutter ZXing Barcode Scanner',  
                 debugShowCheckedModeBanner: false,  
                 home: DemoPage(),  
               );  
             }  
            }
            
            class DemoPage extends StatefulWidget {  
             const DemoPage({super.key});
            
             @override  
             State<DemoPage> createState() => _DemoPageState();  
            }
            
            class _DemoPageState extends State<DemoPage> {  
             Code? result;
            
             @override  
             Widget build(BuildContext context) {  
               if (kIsWeb || !_isCameraSupported()) {  
                 return const UnsupportedPlatformWidget();  
               }
            
               return Scaffold(  
                 appBar: AppBar(title: const Text('Scan Code')),  
                 body: result != null && result?.isValid == true  
                     ? ScanResultWidget(  
                   result: result,  
                   onScanAgain: () => setState(() => result = null),  
                 )  
                     : _buildScanner(),  
               );  
             }
            
             bool _isCameraSupported() {  
               return defaultTargetPlatform == TargetPlatform.iOS ||  
                   defaultTargetPlatform == TargetPlatform.android;  
             }
            
             Widget _buildScanner() {  
               return ReaderWidget(  
                 onScan: _onScanSuccess,  
                 onScanFailure: _onScanFailure,  
                 onControllerCreated: _onControllerCreated,  
                 resolution: ResolutionPreset.high,  
                 lensDirection: CameraLensDirection.back,  
                 codeFormat: Format.*any*,  
                 showGallery: false,  
                 cropPercent: 0.7,  
                 toggleCameraIcon: const Icon(Icons.*switch\_camera*),  
                 actionButtonsBackgroundBorderRadius: BorderRadius.circular(10),  
               );  
             }
            
             void _onControllerCreated(_, Exception? error) {  
               if (error != null) {  
                 _showMessage(context, 'Error: $error');  
               }  
             }
            
             void _onScanSuccess(Code? code) {  
               setState(() {  
                 result = code;  
               });  
             }
            
             void _onScanFailure(Code? code) {  
               setState(() {  
                 result = code;  
               });  
               if (code?.error?.isNotEmpty == true) {  
                 _showMessage(context, 'Error: ${code?.error}');  
               }  
             }
            
             void _showMessage(BuildContext context, String message) {  
               ScaffoldMessenger.*of*(context).hideCurrentSnackBar();  
               ScaffoldMessenger.*of*(context).showSnackBar(  
                 SnackBar(content: Text(message)),  
               );  
             }  
            }

            For this tutorial, the following configurations are used to define the ReaderWidget behavior:

            ConfigurationDescription
            resolution: ResolutionPreset.highSets the camera resolution to high.
            lensDirection: CameraLensDirection.backConfigures the camera to use the back camera.
            codeFormat: Format.anyAllows the scanner to detect any barcode format.
            showGallery: falseDisables the option to select images from the gallery for scanning. It only allows scanning barcodes directly using the camera.
            cropPercent: 0.7Defines the percentage of the camera preview used for scanning. Increasing this value will increase the app screen area used to identify barcodes.
            toggleCameraIcon: const Icon(Icons.switch_camera)Sets the icon used for toggling between front and back cameras
            actionButtonsBackgroundBorderRadius: BorderRadius.circular(10)Applies a border radius of 10 pixels to the background of action buttons

            Access the GitHub repository for a list of all available ReaderWidget configurations.

            3.4 Create the ScanResult widget

            The _DemoPageState build method relies on the ScanResultWidget to display the scan result in the app. This widget also includes a Scan Again button to allow users to restart the scanning process.

            To create the ScanResultWidget, start by creating a new folder named widget inside the lib directory. Inside this folder, add a new file named scan_result_widget.dart. This file will define how the scan result is presented to users. It’s important to note that since the ScanResultWidget is stateless, it doesn’t manage any internal state. Everything it displays is passed in through its properties.

            After creating the file, define the ScanResultWidget class:

            class ScanResultWidget extends StatelessWidget {  
              const ScanResultWidget({  
                super.key,  
                this.result,  
                this.onScanAgain,  
              });
            
              final Code? result;  
              final Function()? onScanAgain;
            }

            In the above code, the result is a final property of type Code? holding the scan result, and onScanAgain is a final property of type Function()?, which is a callback function triggered when the user presses the Scan Again button.

            Next, add the components that will make up the UI. Below is the complete code for the ScanResultWidget, including its basic UI configuration:

            import 'package:flutter/material.dart';  
            import 'package:flutter_zxing/flutter_zxing.dart';
            
            class ScanResultWidget extends StatelessWidget {  
              const ScanResultWidget({  
                super.key,  
                this.result,  
                this.onScanAgain,  
              });
            
              final Code? result;  
              final Function()? onScanAgain;
            
              @override  
              Widget build(BuildContext context) {  
                return Center(  
                  child: Padding(  
                    padding: const EdgeInsets.all(20.0),  
                    child: Column(  
                      mainAxisAlignment: MainAxisAlignment.center,  
                      children: [  
                        Text(  
                          result?.format?.name ?? '',  
                          style: Theme.of(context).textTheme.headlineSmall,  
                        ),  
                        const SizedBox(height: 20),  
                        Text(  
                          result?.text ?? '',  
                          style: Theme.of(context).textTheme.titleLarge,  
                        ),  
                        const SizedBox(height: 20),  
                        Text(  
                          'Inverted: ${result?.isInverted}  Mirrored: ${result?.isMirrored}',  
                          style: Theme.of(context).textTheme.bodyMedium,  
                        ),  
                        const SizedBox(height: 40),  
                        ElevatedButton(  
                          onPressed: onScanAgain,  
                          child: const Text('Scan Again'),  
                        ),  
                      ],  
                    ),  
                  ),  
                );  
              }  
            }

            There are two main elements in the above code:

            1. Text: Display the barcode format, the scanned text, and additional information.
            2. ElevatedButton: This button allows users to scan again by triggering the onScanAgain callback.

            3.5 Create the Unsupported Platform widget

            The _DemoPageState build method uses the UnsupportedPlatformWidget to inform the user if the current device and platform do not support camera usage. To create the UnsupportedPlatformWidget, create a new file named unsupported_platform_widget.dart.dart inside the widgets folder.

            Here’s what your lib folder structure will look like after adding the file:

            /lib  
            |--- main.dart  
            |--- /widgets  
            |------ scan\_result\_widget.dart  
            |------ unsupported\_platform\_widget.dat

            To build the UnsupportedPlatformWidget, use the following code. This widget will display a message to users indicating that their platform isn’t supported, with the message centered on the screen and styled according to the app’s theme.

            import 'package:flutter/material.dart';
            
            class UnsupportedPlatformWidget extends StatelessWidget {  
             const UnsupportedPlatformWidget({super.key});
            
             @override  
             Widget build(BuildContext context) {  
               return Center(  
                 child: Text(  
                   'This platform is not supported yet.',  
                   style: Theme.of(context).textTheme.titleLarge,  
                 ),  
               );  
             }  
            }

            Step 4: Testing the app

            With the main.dart file updated and the scan_result_widget.dart and unsupported_platform_widget.dart files created, you’re ready to test your barcode scanner app. Follow these steps to get started:

            1. Connect your Android device to your computer or configure an emulator.
            2. Select your device or emulator in Android Studio.
            3. In the Run/Debug selector, choose the main.dart file.
            ZXing Flutter tutorial

              The GIF below demonstrates how the barcode scanner app works. Since the ReaderWidget is configured with Format.any, the app successfully identifies both Code 128 and EAN-13 barcode formats. The scanning area is set to 70% of the screen width, which restricts barcode detection to this area. You can adjust this setting through the ReaderWidget configurations. In addition, the GIF shows that the app will not provide a result if more than one barcode is within the scanning area. It only identifies a barcode when a single barcode is visible.

              If you change Format.any to Format.code128, the app will exclusively identify Code 128 barcodes. The following GIF demonstrates this behavior. Notice how the app no longer reads EAN-13 barcodes and is quicker at detecting Code 128 barcodes.

              🎉 And that’s it! You’ve successfully added barcode scanning functionalities to your Flutter app.

              Disadvantages of using the ZXing barcode scanner library

              ZXing provides decent performance for basic barcode scanning tasks but sometimes struggles with more challenging scenarios. Its most notable drawbacks as a barcode scanning solution include:

              • Scanning accuracy and speed: ZXing struggles with scanning poorly lit or damaged barcodes. It often exhibits low recognition rates for smaller barcodes and can fail to decode them altogether.
              • Compatibility issues: ZXing may not perform reliably across all devices, particularly newer models. This has led to frustration among developers who require consistent performance across different platforms.
              • Lack of active development: As an open-source project, ZXing has seen limited updates and maintenance in recent years. This stagnation can lead to unresolved bugs and compatibility issues, making it less reliable for commercial applications.
              • Integration complexity: Integrating ZXing into your app can be cumbersome, especially for developers who may not be familiar with its architecture. This can lead to longer development times and increased chances of bugs during implementation.

              We developed the Scanbot Barcode Scanner SDK as a commercial solution to help enterprises overcome these hurdles presented by free barcode scanning software. Our goal was to have a developer-friendly solution available for a wide range of platforms that consistently delivers high-quality results – even in challenging circumstances.

              Integrating the SDK into your Flutter app only takes a few minutes. Don’t believe us? Then give it a try yourself by heading over to our step-by-step tutorial! Or take a look at our Flutter example app for an implementation similar to this ZXing one.

              Should you have any questions, feel free to reach out to us on Slack or MS Teams or send us an email via sdksupport@scanbot.io