LoginSignup
12
10
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

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

Last updated at Posted at 2024-01-09

はじめに

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

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

結果

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

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

q01.jpg

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

q02.jpg

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

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

q03.jpg

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

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

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

q04.jpg

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

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

q05.jpg

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

q06.jpg

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

q07.jpg

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

q08.jpg

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

q09.jpg

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

準備

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

インストール

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

flutter pub add mobile_scanner
flutter pub add url_launcher

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

url_launcherの使い方についてこれらの記事も参考に。

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

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

パッケージの比較についてはこの記事に書いてあります。

その中でmobile_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.INTERNETandroid.permission.QUERY_ALL_PACKAGESはウェブサイトにアクセスするために必要となります。

pubspec.yml

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

  url_launcher: ^6.2.3
  mobile_scanner: ^3.5.6

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

libの中の.dartファイル

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

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

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

ページの切り替えはNavigator.of(context)を使います。これについてこれらの記事にも参考。

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

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はこのコードのタイプ

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

オブジェクト 意味
.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

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

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

終わりに

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

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

12
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10