[Flutter]バーコードを読み取れるようにする(flutter_zxing)

![スクリーンショット 2024-07-03 15.33.27（2）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/672609/368aad5e-7c43-d4d3-66ee-02c47f6280f4.png)


:::note warn
記事の内容に間違いがあるかもしれません。僕自身もまだまだ学習中なため、コメントをお気軽にください🙇
:::


# はじめに

https://pub.dev/packages/mobile_scanner

バーコードの読み取りで有名ないPackagesというと、[mobile_scanner](https://pub.dev/packages/mobile_scanner)のイメージがあると思っています。
しかし、mobile_scannerをしても、iOSとAndroidで読み取れないものがあったりしたため、アプリに使用する上では採用するかどうかが悩ましかったです。
そのため、別のPackagesを検討しました。


# flutter_zxingについて

https://pub.dev/packages/flutter_zxing

- ZXing (Zebra Crossing) バーコード スキャン ライブラリを使用して、QRコードやバーコードを読み取れる
- QRコードの生成もできる

以下の２つを使用して実装されている
- <b>Dart FFI (Foreign Function Interface) </b>
- <b>zxing-cppライブラリ</b>


### Dart FFI (Foreign Function Interface) 
- Dart言語からCやC++などを呼び出すための仕組み
- `zxing-cpp`がC++で書かれており、それを呼び出すためのもの


### zxing-cppライブラリ

https://github.com/zxing-cpp/zxing-cpp

- C++で実装されている
- 線形バーコード（1Dバーコード）やマトリックスバーコード（2Dバーコード）の画像の処理ができる
- 様々な形式のバーコードの読み取りと書き込みができる


線形バーコードやマトリックスバーコードについては以下に例を掲載しています。

| 線形バーコード | マトリックスバーコード | 
|:-----------:|:------------:|
| <img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/672609/7f234083-8a36-d95f-6fa8-6c6306714397.jpeg"> | <img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/672609/0b5e971a-6fb9-8745-2020-8d07b6ffabf1.png">  | 


## サポートしているフォーマット

| Linear product　| Linear industrial | Matrix | 
|:-----------:|:------------:|:------------:|
| UPC-A | 	Code 39 |QR Code | 
| UPC-E | 	Code 93 | Micro QR Code| 
| EAN-8 | 	Code 128 | 	rMQR Code| 
| EAN-13 | Codabar | 	Aztec | 
| DataBar | DataBar Expanded | 	DataMatrix| 
| - | ITF | - | 
| - | - | MaxiCode (partial)| 

## サポートされているプラ​​ットフォーム
Android (最低 API レベル 21)
iOS (iOS 11.0以上)
MacOS (最低 OSX 10.15) (ベータ版)
Linux (動作しますが、カメラはサポートされていません) (アルファ版)

:::note warn
警告
lutter_zxing は Dart FFI (Foreign Function Interface) 機能に依存しており、現在はモバイルおよびデスクトップ プラットフォームでのみ使用可能です
:::


# バーコードを読み取れるようにする


### １、Packagesを導入
```
dependencies:
  flutter_zxing: ^1.5.2
```

### ２、初期設定

<b>iOS</b>

```plist:info.plist
<key>NSCameraUsageDescription</key>
<string>バーコードの読み取りのために使用</string>
```

<b>Android</b>

```gradle:build.gradle
  minSdkVersion 21
```

### ３、読み取る実装を記載してみる

サンプルコードを参考にさせていただきました。

```dart:main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_zxing/flutter_zxing.dart';

import 'widgets/debug_info_widget.dart';
import 'widgets/scan_result_widget.dart';
import 'widgets/unsupported_platform_widget.dart';

void main() {
  zx.setLogEnabled(kDebugMode);
  // print zxing version
  debugPrint('ZXing version:  ${zx.version()}');
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

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

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Zxing Example',
      debugShowCheckedModeBanner: false,
      home: DemoPage(),
    );
  }
}

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  Uint8List? createdCodeBytes;

  Code? result;
  Codes? multiResult;

  bool isMultiScan = false;

  bool showDebugInfo = true;
  int successScans = 0;
  int failedScans = 0;

  @override
  Widget build(BuildContext context) {
    final isCameraSupported = defaultTargetPlatform == TargetPlatform.iOS ||
        defaultTargetPlatform == TargetPlatform.android;
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const TabBar(
            tabs: [
              Tab(text: 'Scan Code'),
              Tab(text: 'Create Code'),
            ],
          ),
        ),
        body: TabBarView(
          physics: const NeverScrollableScrollPhysics(),
          children: [
            if (kIsWeb)
              const UnsupportedPlatformWidget()
            else if (!isCameraSupported)
              const Center(
                child: Text('Camera not supported on this platform'),
              )
            else if (result != null && result?.isValid == true)
              ScanResultWidget(
                result: result,
                onScanAgain: () => setState(() => result = null),
              )
            else
              Stack(
                children: [
                  ReaderWidget(
                    onScan: _onScanSuccess,
                    onScanFailure: _onScanFailure,
                    onMultiScan: _onMultiScanSuccess,
                    onMultiScanFailure: _onMultiScanFailure,
                    onMultiScanModeChanged: _onMultiScanModeChanged,
                    onControllerCreated: _onControllerCreated,
                    isMultiScan: isMultiScan,
                    scanDelay: Duration(milliseconds: isMultiScan ? 50 : 500),
                    resolution: ResolutionPreset.high,
                    lensDirection: CameraLensDirection.back,
                    flashOnIcon: const Icon(Icons.flash_on),
                    flashOffIcon: const Icon(Icons.flash_off),
                    flashAlwaysIcon: const Icon(Icons.flash_on),
                    flashAutoIcon: const Icon(Icons.flash_auto),
                    galleryIcon: const Icon(Icons.photo_library),
                    toggleCameraIcon: const Icon(Icons.switch_camera),
                    actionButtonsBackgroundBorderRadius:
                        BorderRadius.circular(10),
                    actionButtonsBackgroundColor: Colors.black.withOpacity(0.5),
                  ),
                  if (showDebugInfo)
                    DebugInfoWidget(
                      successScans: successScans,
                      failedScans: failedScans,
                      error: isMultiScan ? multiResult?.error : result?.error,
                      duration: isMultiScan
                          ? multiResult?.duration ?? 0
                          : result?.duration ?? 0,
                      onReset: _onReset,
                    ),
                ],
              ),
            if (kIsWeb)
              const UnsupportedPlatformWidget()
            else
              ListView(
                children: [
                  WriterWidget(
                    messages: const Messages(
                      createButton: 'Create Code',
                    ),
                    onSuccess: (result, bytes) {
                      setState(() {
                        createdCodeBytes = bytes;
                      });
                    },
                    onError: (error) {
                      _showMessage(context, 'Error: $error');
                    },
                  ),
                  if (createdCodeBytes != null)
                    Image.memory(createdCodeBytes ?? Uint8List(0), height: 400),
                ],
              ),
          ],
        ),
      ),
    );
  }

  void _onControllerCreated(_, Exception? error) {
    if (error != null) {
      // Handle permission or unknown errors
      _showMessage(context, 'Error: $error');
    }
  }

  _onScanSuccess(Code? code) {
    setState(() {
      successScans++;
      result = code;
    });
  }

  _onScanFailure(Code? code) {
    setState(() {
      failedScans++;
      result = code;
    });
    if (code?.error?.isNotEmpty == true) {
      _showMessage(context, 'Error: ${code?.error}');
    }
  }

  _onMultiScanSuccess(Codes codes) {
    setState(() {
      successScans++;
      multiResult = codes;
    });
  }

  _onMultiScanFailure(Codes result) {
    setState(() {
      failedScans++;
      multiResult = result;
    });
    if (result.codes.isNotEmpty == true) {
      _showMessage(context, 'Error: ${result.codes.first.error}');
    }
  }

  _onMultiScanModeChanged(bool isMultiScan) {
    setState(() {
      this.isMultiScan = isMultiScan;
    });
  }

  _showMessage(BuildContext context, String message) {
    ScaffoldMessenger.of(context).hideCurrentSnackBar();
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  _onReset() {
    setState(() {
      successScans = 0;
      failedScans = 0;
    });
  }
}
```

# 最後に
ちゃんとしたバーコードの読み取りはなかなか難しいなと感じました👀
今後も色々と調査してみたいところですね。