🦝

【Flutter】webでQRコードリーダをつかう

2022/05/30に公開

はじめに

flutter_webでai_barcodeとflutter_riverpodを使って、自分で作成したQRコードを読み取れるアプリを作成します。

android,iosでQRコードを読み取る記事はたくさんあるのですが、flutter_webでの記事は少なかったのでまとめてみました(しかしバグ多め?)

レベル感:riverpodが少しわかるflutter初心者

最終的なlibディレクトリ内の構造

lib
├── app_barcode_scanner_widget.dart
├── error.dart
├── generated_plugin_registrant.dart//自動生成
├── main.dart
├── qr_content.dart
├── qr_detail.dart
└── qr_scan.dart

やること

pubspec.yamlの編集

https://pub.dev/packages/ai_barcode
https://pub.dev/packages/flutter_riverpod

pubspec.yaml
  ai_barcode: ^3.0.1
  flutter_riverpod: ^1.0.3

上記を追加してください

flutter pub getを忘れずに

web/jsQR.jsの追加

https://github.com/pdliuw/ai_barcode/blob/master/example/web/jsQR.js
上記のファイルをjsQR.jsという名でwebディレクトリに追加してください。

web/index.htmlの編集

  <script src="jsQR.js" type="application/javascript"></script>

bodyタグの間に上記のスクリプトタグを追加してください。

libディレクトリ内の編集

import 'package:flutter_web_qr_code/error.dartなどのflutter_web_qr_codeの部分は、自分のプロジェクトの名前に置き換えてください。

lib/error.dart
import 'package:flutter/material.dart';

class ErrorPage extends StatelessWidget {
  ErrorPage({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    final Object? message = ModalRoute.of(context)?.settings.arguments;
    final Size size = MediaQuery.of(context).size;
    double shortestSide = size.shortestSide;

    return Scaffold(
        appBar: AppBar(
          automaticallyImplyLeading: false,
        ),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Center(
              child: Container(
                  child: message != null
                      ? Text(message as String,
                          textAlign: TextAlign.center,
                          style: TextStyle(
                            fontSize: shortestSide / 19,
                          ))
                      : Text(
                          'エラー',
                          textAlign: TextAlign.center,
                          style: TextStyle(
                            fontSize: shortestSide / 19,
                          ),
                        )),
            ),
            ElevatedButton(
                onPressed: () async {
                  await Navigator.of(context).pushNamed('/top');
                },
                child: const Text('トップに戻る'))
          ],
        ));
  }
}

無効のQRコードを読み取ったときに遷移するエラーページです。

lib/app_barcode_scanner_widget.dart
import 'package:ai_barcode/ai_barcode.dart';
import 'package:flutter/material.dart';

//ai_barcodeのexampleを参考に最小化したコードスキャンカメラウィジェット
class BarcodeScannerWidget extends StatefulWidget {
  BarcodeScannerWidget(this.resultCallback);
  Function(String result) resultCallback;
  
  _BarcodeScannerWidgetState createState() => _BarcodeScannerWidgetState();
}

class _BarcodeScannerWidgetState extends State<BarcodeScannerWidget> {
  late ScannerController _scannerController;

  
  void initState() {
    super.initState();

    _scannerController = ScannerController(scannerResult: (result) {
      widget.resultCallback(result);
    }, scannerViewCreated: () {
      _scannerController.startCamera();
      _scannerController.startCameraPreview();
    });
  }

  
  void dispose() {
    super.dispose();
    _scannerController.stopCameraPreview();
    _scannerController.stopCamera();
  }

  
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Expanded(
          child: scanCamera(),
        )
      ],
    );
  }

  Widget scanCamera() {
    return PlatformAiBarcodeScannerWidget(
      platformScannerController: _scannerController,
    );
  }
}

ai_barcodeのexampleを参考に最小化したコードスキャンカメラウィジェットです。

lib/qr_content.dart
class QRContent {
  QRContent(
    this.id,
    this.name,
  );
  String name;
  int id;
}

List<QRContent> qrContentList = [
  QRContent(0, 'いぬ'),
  QRContent(1, 'ねこ'),
  QRContent(2, 'さる'),
  QRContent(3, 'とり'),
  QRContent(4, 'きじ')
];

QRコードを読み取ったときに表示させる内容。idを読み取って、そのindexをもつqrContentListのnameやidを表示させます。

lib/qr_scan.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_web_qr_code/app_barcode_scanner_widget.dart';
import 'package:flutter_web_qr_code/qr_content.dart';

const String password = 'password123';

final qrContentsProvider = StateProvider<QRContent>((ref) {
  return qrContentList[0];
});

final isScanedProvider = StateProvider<bool>((ref) {
  return false;
});

class QRScanPage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final bool isScaned = ref.watch(isScanedProvider.state).state;
    final QRContent qrContents = ref.watch(qrContentsProvider.state).state;
    final Size size = MediaQuery.of(context).size;
    double shortestSide = size.shortestSide;

    return Scaffold(
      appBar: AppBar(title: const Text('QRコード読み取り')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          ElevatedButton(
              onPressed: () {
                ref.read(isScanedProvider.state).state =
                    false; //flagをfalseにして、読み取ったときの処理がストップしないようにする
              },
              child: const Text('読み取り開始')),
          !isScaned
              ? Padding(
                  padding: EdgeInsets.all(shortestSide * 0.05),
                  child: SizedBox(
                    width: shortestSide * 0.6,
                    height: shortestSide * 0.6,
                    child: BarcodeScannerWidget(
                      (String code) async {
                        if (isScaned) {
                          //ストップを掛けなければ永遠にカメラが止まらず、スキャンしてしまう。
                          //1度スキャンしたらflagをtrueにしてflagがtrueの場合は処理をストップする
                          return;
                        }
                        ref.read(isScanedProvider.state).state = true;
                        List<String> contents = code.split('-');
                        int qrId = int.parse(
                            contents[1]); //qrコードに埋め込まれたid(urlに設定したid)を取得
                        ref.read(qrContentsProvider.state).state = qrContentList[
                            qrId]; //取得したidのqrContentをqrContentListから取得してqrContentsProviderに渡す

                        if (contents.length <= 2 ||
                            contents[2] != password) {
                          //パスワードや、urlのバリデーションを設定することで、
                          //別のQRコードを読み取ったときにエラーページに行くようにする
                          await Navigator.of(context)
                              .pushNamed('/error', arguments: '無効なQRコードです');
                        }
                        await Navigator.of(context).pushNamed('/qr_detail');
                      },
                    ),
                  ),
                )
              : const Text('読み取り開始ボタンをクリックしてください')
        ],
      ),
    );
  }
}

QRコードをスキャンするページです。

  1. ai_barcodeのバーコードを読み取るカメラは、ストップを掛けなければ永遠にカメラが止まらず、スキャンし続けてしまいます。(バグ?仕様?)1度スキャンしたらflagをtrueにしてflagがtrueの場合は処理をストップさせて対処しています。

  2. String password = 'password123'パスワードを設定しています。読み取るQRコードにこのパスワードが含まれていなければ、無効なQRコードと判定されます。これでセキュリティーの対策をしています(ほぼ意味ないが) passwordは自由に設定して大丈夫です。

  3. QRコードから取得したidのqrContentをqrContentListから取得してqrContentsProviderに渡しています。

  4. パスワードや、urlのバリデーションを設定してあるので、別のQRコードを読み取ったときにはエラーページに行きます。正しいQRコードを読み取った場合は、QRコード詳細ページに遷移します。

lib/qr_detail.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_web_qr_code/qr_scan.dart';
import 'package:flutter_web_qr_code/qr_content.dart';

class QRDetailPage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    QRContent qrContents = ref.watch(qrContentsProvider.state).state;

    return Scaffold(
        appBar: AppBar(
          title: const Text("QRコード詳細"),
        ), //AppBar
        body: Center(
          child: Text('${qrContents.id}:${qrContents.name}',
              style: const TextStyle(fontSize: 30)),
        ));
  }
}

正しくQRコードが読み取れた場合に遷移するページです。

  1. qrコードを読み取ったときに更新した(idに対応するqrContentsを渡しました)qrContentsProviderからqrContentを取得します。

  2. 取得したqrContentのidとnameを表示させます。ここで正しくQRコードを読み取れているかチェックできます。

lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_web_qr_code/error.dart';
import 'package:flutter_web_qr_code/qr_scan.dart';
import 'package:flutter_web_qr_code/qr_detail.dart';

void main() {
  runApp(
    ProviderScope(
      child: QRApp(),
    ),
  );
}

class QRApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (BuildContext context) => QRScanPage(),
        '/qr_detail': (BuildContext context) => QRDetailPage(),
        '/error': (BuildContext context) => ErrorPage(),
      },
      title: 'QRコード',
    );
  }
}

ProviderScopeでriverpodを使えるようにしています。
かんたんなルーティングを設定しています。

QRコードを作成する

https://qr.quel.jp/url.php
こちらで作成します。

[好きなurl]-[id]-[password](例: https://sample-0-password123)でURLに入力し作成するボタンをクリックでQRコードが作成されます。

[好きなurl]

https://qr_sampleにしましょう

[id]

qr_content.dartのqrContentListのindexになります。qrContentListの中身を変更していない場合は0~4の数字にしてください。

[password]

qr_scan.dartString password =の値にしてください。
変更していない場合はpassword123になるはずです。

https://sample-0-password123で作ってみました。

実行する

flutter runでbuildしてください。

Multiple devices found:
macOS (desktop) • macos  • darwin-arm64   • macOS 12.3.1 21E258 darwin-arm (Rosetta)
Chrome (web)    • chrome • web-javascript • Google Chrome 102.0.5005.61
[1]: macOS (macos)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 2

Multiple devices found:と言われた場合は、Chrome(chrome)の数字を入力してください。


最初にqrコードを読み取るカメラが表示されます。写真ではカメラを隠しているので真っ黒ですが、カメラが表示されます。先ほど作成したQRコードをかざしてみましょう。


さきほど作成した(https://sample-0-password123)QRコードをかざすと、qrContentsListのindexが0にあたる、いぬが表示されました!

qrContentsListやpasswordを編集したり、いろいろなqrCodeを作成してみてください。

リポジトリ

https://github.com/maropook/flutter_web_qr_code

参考

https://stackoverflow.com/questions/64283370/possibilities-to-scan-qr-code-in-flutter-web

https://pub.dev/packages/ai_barcode

Discussion