Flutter で QRコードを扱う - ちょっと応用編

Android と iOS アプリを同時に開発できる Flutter です.
前回、基本的な QRコード表示をしましたが、今回は少し変更して、画像のチェックサム表示にしてみようと思います。

前準備

前回のものは、画面に決まったURLを指している QRコードを出すだけでした。

今回は画像のチェックサム (SHA256) を表示させてみようと思います。 単純な表示ではつまらないので、Flutterのコンポーネントも少し使いながら実装することにします。

ソースコードの方は以下に置いてありますので、参考にしてください。

Github - flutter_qr_code

前準備として、pubspec.yamlに画像のアセット登録と、SHA256を計算するためのパッケージを追加します。

dependencies:
  ...
  crypto: ^3.0.1

flutter:
  ...
  assets:
    - images/TECHaas_logo.png



画面の方を追加するので、画面遷移のために Navigator を設定します。 Navigator は、複数画面がある場合に、画面の遷移やスタック (積み重ね) を制御してくれるものです。

設定としては、MaterialApproutes:にルート名 (URLみたいなもの) とその時に表示する Widget を登録します。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (BuildContext context) => MyHomePage(title: 'Flutter QRCode'),
        '/checksum': (BuildContext context) => QRCodeImagePage(title: 'SHA256 digest'),
      },
    );
  }
}

あとは、遷移する時にはNavigator.pushして、戻る時にはNavigator.popするだけです。 端末の戻るボタンはpopと同じ動作になって、普通に画面遷移するアプリになります。

遷移の方法ですが、前回作った画面で左にスワイプすると次画面に行くみたいにしてみます。 画面上の操作を検知するのには、GestureDetectorを使います。

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onHorizontalDragEnd: (DragEndDetails details) {
          if (details.primaryVelocity! > 1.0) {}
          if (details.primaryVelocity! < -1.0) {
            Navigator.pushNamed(context, '/checksum');
          }
        },
        // onTap: () => {Navigator.pushNamed(context, '/checksum')},

onHorizontalDragEndは、横方向のスワイプ動作が終わった時に起きるイベントです。 スワイプ開始時や途中でのイベント検出も可能です。

primaryVelocityの値は、動いた速度になります。 定義としてはin logical pixels per secondなので、1秒あたりになんピクセル動いたかで、 プラス方向は左から右へのスワイプ (X軸方向の値が大きくなる)、マイナスは逆です。 今回は次のページを模しているので、左から右へのスワイプ時に進む方向にします。

Navigator.pushNamedは、クラスメソッド (static) です。 実装は、Navigator.of(context)になっていますので、今のcontextに設定されたNavigatorに対しての処理になります。

この辺最初はちょっと理解しづらいかもしれませんが、Flutter/Dart なら実装のソースコードも参照できます。 ライブラリの中でどんなことをやっているのか、少しずつ見てみると理解が深まると思います。 (vscodeを使っているなら、Goto defintionでソースコードにアクセスできます。 また Flutter は、まだ開発途中ということもあり、あまり複雑な処理はないので、ライブラリのソースも比較的読みやすいです)

コメントになっていますが、onTapを定義して、タップしたら遷移する処理にも変更できます。

画像のチェックサムコードの表示

チェックサム表示画面の方は、上の方から順に見ていきます。

qr_code_image.dartになります。

class _QRCodeImagePageState extends State<QRCodeImagePage> {
  bool _showActionButton = true;
  Image? _image;
  String? _checksum;

まず、ステートの変数です。 StatefulWidgetでは、setState()が呼び出されると、UIが再構築されます。 ここでは、そのUIを制御するための変数を定義しておきます。

続いて、初期化処理のinitStateです。これは widget の表示時に一度だけ呼び出されます。

  @override
  initState() {
    super.initState();
    rootBundle.load('images/TECHaas_logo.png').then((imageData) {
      final Uint8List data = imageData.buffer.asUint8List();
      final Digest digest = sha256.convert(data);
      debugPrint('digest: $digest');

      setState(() {
        _image = Image.memory(data);
        _checksum = digest.toString();
      });
    });
  }

rootBundle.load()は、アセットファイルの読み込みです。 アセットというのはアプリに埋め込んだ画像やデータで、最初に見たpubspec.yamlで定義してあります。 アセットは無闇に多くすると、アプリのサイズが大きくなります。 ユーザのダウンロードサイズも大きくなりますので、あまり大きなファイルやデータは避けた方が良いです。 また、公開アプリに限りますが、アセットの変更でも再度審査が必要になったりしますので、なにを含めるかはよく考えましょう。

rootBundle.load()は、Future<ByteData>を返します。 非同期で実行されて、読み込みが終わったら、ByteData を引数としてthen()が実行されます。

ByteData のチェックサムをとるために、Uint8Listに変換して、sha256.convert()でダイジェストを求めます。

出来上がったものは、setState()でステートの変数に格納します。 また、読み込んだ画像は、Image.memoryで表示用の画像に変換しています。

ここでは、ローカルアセットの読み込みなので、ほぼ一瞬で終わりますが、ネットワークなどの時間のかかる処理でも同じ様なパターンを使います。

それでは実際のUIの構築です。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Stack(
        children: [
          Center(
              child: Container(
            color: Colors.white,
            child: _image,
          )),

まず、Scaffoldは画面の枠組みで、タイトルとかフローティングボタンを使う時に使います。

中身は、body:で設定します。 今回は、Stackで、children:に指定したウィジェットの配列を順々に重ねて描画します。

まず、画像を画面中央に表示します。

次の部分が、チェックサムをQRコードのしたものを表示している部分です。

          if (!_showActionButton && _checksum != null) ...[
            Align(
              alignment: Alignment.bottomRight,
              child: GestureDetector(
                  onTap: () => {
                        setState(() => {_showActionButton = true})
                      },
                  child: Container(
                      padding: EdgeInsets.all(5),
                      child: Container(
                        padding: EdgeInsets.all(5),
                        decoration: BoxDecoration(
                          border: Border.all(color: Colors.black, width: 2.0),
                        ),
                        child: QrImage(
                          size: 200,
                          data: _checksum!,
                          version: QrVersions.auto,
                        ),
                      ))),
            ),
          ],
        ],
      ),

if文のあと、...[ ]はスプレッド演算子と呼ばれていて、Collection (List, Set, Map等)で使うと、要素を追加することができます。

ここでは、上で見たStackchildren:を指定する子ウィジェットの配列に、iftrueの場合、Alignが追加されます。

Alignは、画面のどこかに子要素を寄せる場合に使います。 buttomRight指定なので、child:要素が右下寄せになります。

QrImageがQRコードの実体です。 その周りにBoxDecorationで枠線を引きます。 padding指定はそれぞれ余白を追加する感じです。

残りは、フローティングボタンになります。画面の右下の方に出てくるボタンです。

      floatingActionButton: _showActionButton
          ? FloatingActionButton(
              onPressed: () => {setState(() => _showActionButton = false)},
              tooltip: 'Show Code',
              child: const Icon(Icons.qr_code),
            )
          : null,
    );

QRコード表示のアイコンを置いたボタンで、押されたら、_showActionButtonを切り替えます。 また、_showActionButtonは三項演算で、trueなら、FloatingActionButtonが、falseならnullになります。

これで、_showActionButtonによって非表示切り替えができるようになります。

最初は表示状態ですが、ボタンをタップすると、QRコードのイメージに切り替わり、そこでタップすると、またボタンに変わるという処理をしています。

以上、少し応用を効かせて、単純な表示からボタンでの表示切り替えをするにしてみました。

次回は、QRコードの認識側を作ってみようと思います。