[Flutter]バーコードを読み取って楽天APIで商品情報を取得してみた。

Flutterでのバーコードスキャン機能実装の個人備忘録です。

 **※楽天APIの登録手順などは省いております 。バーコード読み取りを主軸にしておりますのでご了承ください。** 

個人開発しているアプリ「Modeling Buddy」には自分が持っているプラモや模型道具を自分で登録し一覧化できる機能があります。

| 一覧画面 | 入力画面 |
|:-:|:-:|
|<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3732370/bb0e96fe-3e45-f5a1-abb1-0edb767769e9.png width=120>| <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3732370/32de70c2-3c1b-0039-6dfb-0cce204ebd65.png width=120>
  
今までは手入力しかできなかったのですが、

「バーコードから商品画像、名前などを自動入力できると楽なのでは？:thinking:」

という事で調べてみました。

## パッケージの選定

以下を参考にさせて頂きました。
先駆者にはいつも感謝です。

https://zenn.dev/tko_kasai/articles/a6abcefb8e9a3f#2.mobile_scanner-947likes

今回は仕様的にJANコードを読み取れる＆今は要らないですがQAコード生成もできるみたいなので「[mobile_scanner](https://pub.dev/packages/mobile_scanner)」を採用しました。

## 開発環境＆使うもの
```
flutter doctor
[✓] Flutter (Channel stable, 3.24.0, on macOS 14.6.1 23G93 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.96.4)
``` 

- [mobile_scanner](https://pub.dev/packages/mobile_scanner)
  - 目的：JANコードを取得
- [楽天商品検索API](https://webservice.rakuten.co.jp/documentation/ichiba-item-search#aboutAffili)
  - 目的：JANコードから商品画像、名前を取得。採用理由はたまたま利用登録していたから。
- hooks_riverpod
  - 目的：状態管理

## アプリのバージョン

- Android: min SDK 23
- iOS: min Version 15.5

## 実装

### 1. mobile_scannerのインポート
pubspec.yamlのdependencies内に以下を追加。

```
dependencies:
~~~
mobile_scanner: ^6.0.5 ⭐️これを追加
~~~
```
または、以下コマンドをプロジェクト直下で実行。

```
flutter pub add mobile_scanner
```


### 2.アプリにカメラへの権限を追加
すでに設定済みの場合は読み飛ばしてください。

#### Android
AndroidManifest.xmlに以下を追加してください。
```android\app\src\main\AndroidManifest.xml
    <uses-permission android:name="android.permission.CAMERA" /><uses-permission
```

#### iOS
Info.plistに以下キーを追加してください。
```
<key>NSCameraUsageDescription</key>
<string>カメラを使う理由をここに書く</string>
```
端末の画像を使う場合は以下も追加してください。
(あらかじめ撮影したバーコードを使う場合などは必要だと思います。)

```
<key>NSPhotoLibraryUsageDescription</key>
<string>ギャラリーの写真を使う理由を書く</string>
```

### 3.バーコード読み取り

まず、サンプルで読み取り部分だけ書いてみました。
以下をコピペすればとりあえずの読み取りまでは可能です。
MobileScannerがカメラのプレビューを表示し、カメラ内にバーコードを向けるとコンソールに出力してくれます。

<details><summary>サンプルコード</summary>



```dart
SizedBox(
  height: 400,
  child: MobileScanner(
    controller: MobileScannerController(
      // 同じバーコードを連続でスキャンしない
      detectionSpeed: DetectionSpeed.noDuplicates,
    ),
    onDetect: (capture) {
      final barcodes = capture.barcodes;
      final value = barcodes[0].rawValue;
      if (value != null) {
        print('読み込み結果 $value');
      }
    },
  ),
),
```
</details>
出力結果

```
I/flutter (21104): 読み込み結果 4901777418318 
```

### 4.読み取ったJANコードを楽天APIに渡す

とりあえずボタンをタップしたらAPIを実行。結果から画像と名前を取得し画面に表示するまでをやってみます。

読み取るだけなら上のコードで良いですが、データを別Widgetに渡す必要があるので[公式のサンプル](https://github.com/juliansteenbakker/mobile_scanner/tree/master/example)を参考に以下の実装となりました。


<details><summary>サンプル</summary>

```dart
/// 画面全体のWidget
class BarcodeScannerWithScanWindow extends StatefulHookConsumerWidget {
  const BarcodeScannerWithScanWindow({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() {
    return _BarcodeScannerWithScanWindowState();
  }
}

class _BarcodeScannerWithScanWindowState
    extends ConsumerState<GuideMoldCreatorScreen> {
  final MobileScannerController controller = 
  MobileScannerController();

  final boxFit = BoxFit.contain;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: const Text(
        'With Scan window',
        style: TextStyle(color: Colors.black),
      )),
      backgroundColor: Colors.black,
      body: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          /// カメラプレビュー部分
          SizedBox(
            height: 100,
            child: MobileScanner(
              fit: boxFit,
              controller: controller,
            ),
          ),
          /// スキャン結果
          ScannedBarcode(
            barcodes: controller.barcodes,
          ),
        ],
      ),
    );
  }

  @override
  Future<void> dispose() async {
    super.dispose();
    await controller.dispose();
  }
}

```
</details>

<details ><summary>サンプル</summary>

```dart
/// 読み込み結果
class ScannedBarcode extends HookConsumerWidget {
  const ScannedBarcode({
    super.key,
    required this.barcodes,
  });

  final Stream<BarcodeCapture> barcodes;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return StreamBuilder(
      stream: barcodes,
      builder: (context, snapshot) {
        final scannedBarcodes = snapshot.data?.barcodes ?? [];
        final values = scannedBarcodes.map((e) => e.displayValue).join(', ');

        if (scannedBarcodes.isEmpty) {
          return const Text(
            'Scan something!',
            overflow: TextOverflow.fade,
            style: TextStyle(color: Colors.white),
          );
        }

        return HookBuilder(
          builder: (context) {
            final name = useState('');
            final image = useState('');
            return Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                TextButton(
                  onPressed: values.isNotEmpty
                      ? () async {
                          // 楽天APIを実行。Repository直呼びは今だけ
                          final product = await ref
                              .watch(rekutenRepositoryProvider)
                              .getProducts(
                                scannedBarcodes.last.displayValue!,
                                10,
                              );
                          // 結果を保存 
                          name.value = product.first.name;
                          image.value = product.first.imageUrl.first;
                        }
                      : null,
                  child: Text(
                    values.isEmpty ? 'No display value.' : values,
                    overflow: TextOverflow.fade,
                    style: const TextStyle(color: Colors.white),
                  ),
                ),
                // 取得した画像を表示
                image.value.isNotEmpty
                    ? Image.network(image.value)
                    : const SizedBox.shrink(),
                // 取得した名前を表示
                name.value.isNotEmpty
                    ? Text(
                        name.value,
                        style: const TextStyle(color: Colors.white),
                      )
                    : const SizedBox.shrink(),
              ],
            );
          },
        );
      },
    );
  }
}

```
</details>


読み取ったバーコード情報はMobileScannerController#barcodesに入っています。
StreamですのでそれをScannedBarcodeのStreamBuilderで受け取って楽天APIに渡します。

APIから取得した情報を、画像と名前のuseStateに格納し表示しています。

#### キャプチャ

JANコードをタップで楽天APIを実行しています。

<img width=200 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3732370/ec97153b-c502-0b4c-8245-22e39b327131.gif>


## 懸念点

- カメラがフォーカスを合わせている途中で読み込んでしまい間違ったJANコードがいくつか読み取れた

4901777418318が正しいJANコードだが...

```
I/flutter (21104): 読み込み結果 4901777418318 
I/flutter (21104): 読み込み結果 4901777441859 ←NG
I/flutter (21104): 読み込み結果 4901777418318
I/flutter (21104): 読み込み結果 4901757641859 ←NG
I/flutter (21104): 読み込み結果 4901777418318
I/flutter (21104): 読み込み結果 4901777425859 ←NG
```
今回は楽天APIに渡す手前、できるだけ正確なJANコードが望ましいです。
そのため複数の読み取り結果を溜めて一番多いコードを正確なコードとして扱うなどにした方が良いかもです。



## 最後に

これでバーコードから商品画像の取得までできました。
あとは既存の画面に埋め込んで完成です。




### :satellite:宣伝

「Modeling Buddy」Android、iOSで配信中です！

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3732370/0b6bc1c2-ff26-3650-48f0-57edc6e23e59.png width=400>