FlutterでQRコードとバーコードを読み取る簡単なスマホアプリを実装する

# はじめに

この記事ではFlutterを使って**QRコード**と**バーコード**をスキャンして結果を表示するスマホアプリを作る方法について解説します。

コードをスキャンするためにここでは[**mobile_scanner**](https://pub.dev/packages/mobile_scanner)というパッケージを使います。これは比較的に新しいパッケージで人気が高まってきていますが、qiitaではまだこれに関する記事がないので自分で書くことにしました。

# 結果

今回どんなものを作るかわかるように、まずはできた結果から見せておきます。

`.apk`ファイルに出力してAndroidのスマホにアプリを入れてインストールして起動したらこうなります。

![q01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/20927c3d-992f-430e-d388-5d1fffa55c2a.jpeg)

そしてボタンを押したらスキャンの画面に入ります。そこで初めての場合はまずカメラにアクセスする権限が求められます。Androidの場合こういう画面になります。

![q02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/0fe5dc57-903a-2419-c1be-81aa964ad429.jpeg)

許可したらカメラを使うことができます。

次はQRコードやバーコードへカメラを向けるだけです。

![q03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/1e4480dd-e49e-52b0-d190-537265c69d50.jpeg)

カメラの画面の下にあるのはズームを調整するためのスライダーと3つのボタンがあります。左からフラッシュを制御するボタン、カメラをオンオフにするボタン、前後のカメラを切り替えるボタン。

例えば今回私がいつも飲んでいる<ruby>雪印<rt>ゆきじるし</rt></ruby>コーヒーを取り出してバーコードを狙ってみました。

そうしたらこういう画面に入ります。

![q04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/86dcc840-e53c-160c-2cb8-3dd19b85ea07.jpeg)

これはバーコードの下に書いた番号と同じ商品としての番号です。会計する時に使うコード。上にあるproductというのはコードの種類を示すのです。これもコードの中にある情報に入っています。

次に同じコーヒーにあるQRコードを狙ってみます。

![q05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/618b3e12-fa0b-b0ac-6416-1a851850f486.jpeg)

これはURLだからこういう結果が出てきます。

![q06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/ece65ec0-ba63-1b0a-f36a-74e3d47ce1d8.jpeg)

コードがURLである場合こんな風に表示するように書いたから、このリンクを押したらウェブサイトのページに入ることになります。

![q07.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/a23d010c-bcea-6338-f535-15cc115c0c60.jpeg)

その他にも例えばgoogleでqrと検索して適当に試してみたら……

![q08.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/8ee96bcd-0b36-69db-1ecf-958f33a66d7c.jpeg)

こういう誰かの連絡先の情報が入っているQRコードも見つけました。

![q09.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/59e584a4-bcb8-5f3c-3383-f20bedd61f36.jpeg)

今回作ったアプリの説明は以上です。次はこんなものを作るためにどうやって書くか説明していきます。


# 準備

Flutterのインストールや環境設定や初期プロジェクトの作成については他の色んな記事に説明が書いてあるので省略します。ここではパッケージのインストールから始めます。

## インストール

まずFlutterプロジェクトを作成した後プロジェクトのパスに入ってパッケージをインストールするコマンドを実行します。
```sh
flutter pub add mobile_scanner
flutter pub add url_launcher
```

今回使うのは主役である[mobile_scanner](https://pub.dev/packages/mobile_scanner)の他に、読み取ったウェブサイトのURLにアクセスできるための[url_launcher](https://pub.dev/packages/url_launcher)というパッケージも必要となります。

url_launcherの使い方についてこれらの記事も参考に。
- [Flutter アプリでURLを開く方法](https://qiita.com/KKoichi51/items/0f6c2649640b7ff5f3b0)
- [【Flutter】結局、url_launcherはどう実装すべきなのか。](https://qiita.com/kakutouma/items/c97d949bd72479504b97)
- [\[Flutter\] 正規表現を使って、文字列の中のURLの部分だけをリンクに置き換える](https://qiita.com/Hiiisan/items/f0bbc5715fab7e6787ad)

## コードを読み取るパッケージのついて

実はQRコードやバーコードを読み取るためのFlutterパッケージは意外とたくさんあります。

パッケージの比較についてはこの記事に書いてあります。
- [\[Flutter\]QRコードリーダーパッケージ8選📗 それぞれのメリデメを知らべてみた](https://zenn.dev/tko_kasai/articles/a6abcefb8e9a3f)
- [Flutter で QRコードリーダー](https://zenn.dev/seya/articles/6e15432cfe327d)

その中でmobile_scannerは比較的に新しいのに利用者が多くて、これから伸びていく見込みがあるので、今回はこれを使うことにしました。

その他のパッケージでの実装の例はこれらの記事にあります。
- [FlutterでQRコードリーダー／履歴管理アプリを作ってみた](https://qiita.com/oreha_gao/items/f9b368aafeeacfcf58f2)（[qr_code_scanner](https://pub.dev/packages/qr_code_scanner)）
- [FlutterでQRコードスキャナーを使ってみる](https://qiita.com/shinbey221/items/b582d067f6852370d797)（[barcode_scan](https://pub.dev/packages/barcode_scan)）
- [FlutterでQRコードを読むアプリを作ってみる](https://qiita.com/fukuit/items/29651dcdd20f1aacd4fb)（barcode_scan）
- [Flutterでバーコードをスキャンする (2022/05/02)](https://qiita.com/Limitex/items/58f15911ed645f40522a)（[barcode_scan2](https://pub.dev/packages/barcode_scan2)）
- [いつの間にか、バーコードスキャンするflutter_barcode_scannerが使えなくなっていたので、barcode_scan2を使う](https://qiita.com/cygnu/items/6c95007a8139e2fccc5a)
- [専門学生がFlutterを独学で初めて3ヶ月でリリースした話](https://qiita.com/utasan_com/items/8024b860191dd69fdcc6)（[flutter_barcode_scanner](https://pub.dev/packages/flutter_barcode_scanner)）

## 環境

Flutterはクロスプラットフォームなので開発環境はかなり自由です。WindowsでもMacでも同じようにコードを書きますし、書いたものはAndroidでもiOSでも使います。

コードを読み取るパッケージの中では環境に依存するものもありますが、今回使うmobile_scannerはどの環境どのプラットフォームでも同じように使えるので心配する必要ありません。

それでも微妙に違いがあるので、今回私が試す時に使う環境について書いておきます。
- Windows 11
- Android 13
- Flutter 3.16.5
- Dart 3.2.3
- mobile_scanner 3.5.6
- url_launcher 6.2.2

今回はWindowsで書いてAndroidで使用しますが、Macで書いてiOSで使うことも同じように書きます。勿論違うところも多少あると思いますが、私はiOSを持っていないので、試すすべはありません。だから主に「Windowsで書いてAndroidで使う」場合だけ解説します。


# 実装

プロジェクトを作ったら色んなファイルが現れますが、今回触れるのは一部だけです。ここでは書き換えるファイルと追加するファイルのことだけ説明します。

## AndroidManifest.xml

まずはAndroidの場合、スマホのカメラを利用する権限とウェブサイトにアクセスする権限が必要です。そのために`android\app\src\main\AndroidManifest.xml`というファイルにこの部分を追加する必要があります。

```android\app\src\main\AndroidManifest.xml
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
```

`android.permission.CAMERA`はカメラを使うために必要で、`android.permission.INTERNET`と`android.permission.QUERY_ALL_PACKAGES`はウェブサイトにアクセスするために必要となります。

## pubspec.yml

次は使うパッケージを表記するファイルである`pubspec.yml`ですが、もしパッケージのインストールが完成したら、こんなコードが書いてあるはずです。（ただし後ろにある数字はインストールの時点の最新なバージョンに変わる）

```yml
  url_launcher: ^6.2.3
  mobile_scanner: ^3.5.6
```

もしこれがなければこの場で追加すればいいです。VSCodeで編集した場合もしまだパッケージがなければ自動的にインストールされることになります。

## libの中の`.dart`ファイル

最初からある`main.dart`も編集して、それに加えて`scanner.dart`と`scandata.dart`も加えます。

アプリは3ページから構成されます。

|ファイル|役目|
| --- | --- |
|main.dart|最初のページ|
|scanner.dart|コードスキャナーのページ|
|scandata.dart|スキャンのデータを表示するページ|

ページの切り替えは`Navigator.of(context)`を使います。これについてこれらの記事にも参考。
- [FlutterのNavigatorで画面遷移](https://qiita.com/granoeste/items/19c119ffc36a016e6223)
- [FlutterのNavigatorの使い方と仕組み](https://qiita.com/heavenosk/items/9e43298955a371221393)
- [Flutter 画面遷移について](https://qiita.com/motoki418/items/188ac56475ae7d48dcdf)
- [FlutterでNavigatorでの遷移で戻り値を受け取る](https://qiita.com/kasa_le/items/6344d987fa52c0dfddb1)
- [Flutterの画面遷移の基本](https://qiita.com/kazutxt/items/6de15b9a6858e8f948b6)
- [【Flutter】タブ内のボタンでページ遷移（画面遷移）](https://qiita.com/maffy_chen/items/038d9fda8c5ab17229a6)
- [【Flutter】Navigator.of(context) から理解する 3つのツリー](https://zenn.dev/chooyan/articles/77a2ba6b02dd4f)
- [いちから始めるFlutterモバイルアプリ開発 Chapter 15](https://zenn.dev/heyhey1028/books/flutter-basics/viewer/navigation)

以下のコードの中ではできるだけ解説を書いています。

### main.dart

プログラムのメインな部分。ページの中にはただ「押したらスキャナーのページ切り替える」という大きなボタンしかない。

```lib\main.dart
import 'package:flutter/material.dart';
import 'package:sukyan/scanner_simple.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'コードスキャナー',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.greenAccent),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'コードスキャナー'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: ElevatedButton(
          // 押したらスキャンの画面に入るボタン
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(
                builder: (context) => const ScannerWidget(),
              ),
            );
          },
          style: ElevatedButton.styleFrom(
            elevation: 20, // ボタンが画面から浮かぶ高さ（影で現す）
            fixedSize: const Size.fromHeight(300), // ボタンの大きさ
            backgroundColor: const Color(0xFFAADDCC), // ボタンの背景の色
            side:
                const BorderSide(color: Color(0xFF44AA66), width: 6), // ボタンの枠線
          ),
          child: const Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(
                Icons.qr_code_scanner_sharp, // QRスキャンのアイコン
                size: 120,
              ),
              Text(
                'スキャンを始める',
                style: TextStyle(fontSize: 36),
              )
            ],
          ),
        ),
      ),
    );
  }
}

```

### scanner.dart

コードスキャナーのページとその動作を決めるコード。

```lib\scanner.dart
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:sukyan/scandata.dart';

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

  @override
  State<ScannerWidget> createState() => _ScannerWidgetState();
}

class _ScannerWidgetState extends State<ScannerWidget>
    with SingleTickerProviderStateMixin {
  // スキャナーの作用を制御するコントローラーのオブジェクト
  MobileScannerController controller = MobileScannerController();
  bool isStarted = true; // カメラがオンしているかどうか
  double zoomFactor = 0.0; // ズームの程度。0から1まで。多いほど近い

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color(0xFF66FF99), // 上の部分の背景色
        title: const Text('スキャンしよう'),
      ),
      backgroundColor: Colors.black,
      body: Builder(
        builder: (context) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // カメラの画面の部分
              SizedBox(
                height: MediaQuery.of(context).size.width * 4 / 3,
                child: MobileScanner(
                  controller: controller,
                  fit: BoxFit.contain,
                  // QRコードかバーコードが見つかった後すぐ実行する関数
                  onDetect: (scandata) {
                    setState(() {
                      controller.stop(); // まずはカメラを止める
                      // 結果を表す画面に切り替える
                      Navigator.of(context).pushReplacement(
                        MaterialPageRoute(
                          builder: (context) {
                            // scandataはスキャンの結果を収める関数であり、これをデータ表示ページに渡す
                            return ScanDataWidget(scandata: scandata);
                          },
                        ),
                      );
                    });
                  },
                ),
              ),
              // ズームを調整するスライダー
              Slider(
                value: zoomFactor,
                // スライダーの値が変えられた時に実行する関数
                onChanged: (sliderValue) {
                  // sliderValueは変化した後の数字
                  setState(() {
                    zoomFactor = sliderValue;
                    controller.setZoomScale(sliderValue); // 新しい値をカメラに設定する
                  });
                },
              ),
              // 下の方にある3つのボタン
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  // フラッシュのオン／オフを操るボタン
                  IconButton(
                      // アイコンの表示はオン／オフによって変わる
                      icon: ValueListenableBuilder<TorchState>(
                        valueListenable: controller.torchState,
                        builder: (context, state, child) {
                          switch (state) {
                            // オフしている場合、オンにする
                            case TorchState.off:
                              return const Icon(
                                Icons.flash_off,
                                color: Colors.grey,
                              );
                            // オンしている場合、オフにする
                            case TorchState.on:
                              return const Icon(
                                Icons.flash_on,
                                color: Color(0xFFFFDDBB),
                              );
                          }
                        },
                      ),
                      iconSize: 50,
                      // ボタンが押されたら切り替えを実行する
                      onPressed: () => controller.toggleTorch()),
                  // カメラのオン／オフのボタン
                  IconButton(
                    color: const Color(0xFFBBDDFF),
                    // オン／オフの状態によって表示するアイコンが変わる
                    icon: isStarted
                        ? const Icon(Icons.stop) // ストップのアイコン
                        : const Icon(Icons.play_arrow), // プレイのアイコン
                    iconSize: 50,
                    // ボタンが押されたらオン／オフを実行する
                    onPressed: () => setState(() {
                      isStarted ? controller.stop() : controller.start();
                      isStarted = !isStarted;
                    }),
                  ),
                  // アイコン前のカメラと裏のカメラを切り替えるボタン
                  IconButton(
                    color: const Color(0xFFBBDDFF),
                    icon: ValueListenableBuilder<CameraFacing>(
                      // アイコンの表示は使っているカメラによって変わる
                      valueListenable: controller.cameraFacingState,
                      builder: (context, state, child) {
                        switch (state) {
                          // 前のカメラの場合
                          case CameraFacing.front:
                            return const Icon(Icons.camera_front);
                          // 後ろのカメラの場合
                          case CameraFacing.back:
                            return const Icon(Icons.camera_rear);
                        }
                      },
                    ),
                    iconSize: 50,
                    onPressed: () {
                      if (isStarted) {
                        controller.switchCamera();
                      }
                    },
                  ),
                ],
              ),
            ],
          );
        },
      ),
    );
  }
}

```

`MobileScanner`のウィジェットの中の`onDetect`で書いた関数はコードを見つけた時に実行したいことです。ここでは`scandata`がスキャンで得られたデータを収めるオブジェクトであり、このデータを受け取ってデータを表示する新しいページを作ってそのページに切り替えるのです。

### scandata.dart
`MobileScanner`から渡されたスキャンのデータを受け取って表示するページです。

```lib\scandata.dart
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:url_launcher/url_launcher_string.dart';

class ScanDataWidget extends StatelessWidget {
  final BarcodeCapture? scandata; // スキャナーのページから渡されたデータ
  const ScanDataWidget({
    super.key,
    this.scandata,
  });

  @override
  Widget build(BuildContext context) {
    // コードから読み取った文字列
    String codeValue = scandata?.barcodes.first.rawValue ?? 'null';
    // コードのタイプを示すオブジェクト
    BarcodeType? codeType = scandata?.barcodes.first.type;
    // コードのタイプを文字列にする
    String cardTitle = "[${'$codeType'.split('.').last}]";
    // 読み取った内容を表示するウィジェット
    dynamic cardSubtitle = Text(codeValue,
        style: const TextStyle(fontSize: 23, color: Color(0xFF553311)));

    // タイプがURLである場合
    if (codeType == BarcodeType.url) {
      cardTitle = 'どこかのURL';
      cardSubtitle = InkWell(
        child: Text(
          codeValue,
          style: const TextStyle(
            fontSize: 23,
            color: Color(0xFF1133DD), // 藍色の文字
            decoration: TextDecoration.underline, // 下線
            decorationColor: Color(0xFF1133DD), // 下線の色
          ),
        ),
        // 押したらウェブサイトに入る
        onTap: () async {
          if (await canLaunchUrlString(codeValue)) {
            await launchUrlString(codeValue);
          }
        },
      );
    }

    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color(0xFF66FF99),
        title: const Text('スキャンの結果'),
      ),
      body: Card(
        color: const Color(0xFFBBFFDD),
        elevation: 5,
        margin: const EdgeInsets.all(9),
        child: ListTile(
          title: Text(
            cardTitle,
            style: const TextStyle(fontSize: 26, fontWeight: FontWeight.bold),
          ),
          subtitle: cardSubtitle,
        ),
      ),
    );
  }
}

```

得られたスキャンのデータ（ここでは`scandata`という変数で）は`BarcodeCapture`というオブジェクトであり、その中に`.barcodes`というアトリビュートはコードから読み取った情報が入っています。複数あることもあるのでリストの型になって、ここで`.first`を使うことで一個目を取ります。その中で
- `.rawValue`は読み取った文字列データ
- `type`はこのコードのタイプ

`type`は`BarcodeType`というオブジェクトであり、大体こういうものがあります

|オブジェクト|意味|
| --- | --- |
|.contactInfo|連絡先の情報|
|.email|電子メール|
|.isbn|本のISBN|
|.phone|電話番号|
|.product|商品のコード|
|.sms|SMS内容|
|.text|メッセージ|
|.url|ウェブサイトのURL|
|.wifi|WIFIアクセスポイント|
|.geo|地理座標|
|.unknown|不明|

ここではurlである場合だけ表示が違うものにします。URLを押したらそのウェブサイトに入ることになります。

## デプロイ

コードを書き終わったら次はスマホで使えるようにデプロイします。

Androidの場合これを実行して……

```
flutter build apk
```

そうしたら`app-release.apk`ファイルができてこれをスマホに入れて使います。

インストールした起動したら上述みたいな結果が出るか確認します。

## シンプル版

おまけにmobile_scannerの簡単な使い方を説明するために完結なコードに書き換える版もここに書いておきます。

以上の実装でカメラのところに色んな機能を加えたからコードはかなり長いけど、もしカメラを詳しく弄らなくてもいい簡単なスキャナーならそこまで書かなくてもいいです。

その場合`lib\scanner.dart`をこのように書き換えます。
```lib\scanner.dart
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:sukyan/scandata.dart';

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

  @override
  State<ScannerWidget> createState() => _ScannerWidgetState();
}

class _ScannerWidgetState extends State<ScannerWidget>
    with SingleTickerProviderStateMixin {
  MobileScannerController controller = MobileScannerController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color(0xFF66FF99),
        title: const Text('スキャンしよう'),
      ),
      backgroundColor: Colors.black,
      body: Builder(
        builder: (context) {
          return MobileScanner(
            controller: controller,
            fit: BoxFit.contain,
            onDetect: (scandata) {
              setState(() {
                controller.stop();
                Navigator.of(context).pushReplacement(
                  MaterialPageRoute(
                    builder: (context) {
                      return ScanDataWidget(scandata: scandata);
                    },
                  ),
                );
              });
            },
          );
        },
      ),
    );
  }
}

```

そうしたらスキャナーの画面はこうなります。

![q10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/61daf4f1-cd5b-e62a-ee09-f00426b5aa11.jpeg)

装飾の部分がもうなくてズームすることもできないが、これもコードを見つけたら同じスキャンのデータを表示するページに入ります。

要するにこの機能で一番重要なのは`MobileScanner`のウィジェットを作って、その中に`onDetect`というメソッドの中でコードを見つけたら実行したい処理を表記するということです。


# 終わりに

今までずっとPython関連の記事ばかり書いていたのですが、Pythonとは全く関係ない記事を書くのは今回で初めてです。

FlutterとDart言語は今年1月6日に勉強し始めたばかりで意外と気に入りました。スマホアプリを作るのは初めてでほぼ未経験ですが、案外上手くいけたようです。自分の書いたアプリは実際にスマホで動くところを見たらわくわくして楽しいですね。今後も色々書いていきたいと思っています。