swampの忘備録

エンジニアが、情報系のイベント行ったときとかプログラミングなどの情報工学について忘備録として書くつもりです。

FlutterでQRコードやバーコードを読み取る方法

このブログは、「Challenge! スマホアプリ勉強会Vol.3」で作成予定のアプリについての解説ページです。

QRコードやバーコードの読み取りを行うアプリをFlutterで作る方法について解説します。

今回利用するパッケージ

今回は、barcode_scan 1.0.0というパッケージを使ってQRコードの読み取りを実現します。
pub.dev

QRコード以外にもバーコードの読み取りも可能にしているようです。

準備

まず、Androidおよびiphoneのアプリ内でカメラのパーミッションを許可するコードを書きます。

Android

android/app/src/main/AndroidMainifest.xmlを以下のように編集します。2つの記述を追加するだけです。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.third_app">
    
    <!-- 次の記述を追加する -->
    <uses-permission android:name="android.permission.CAMERA" /> 

    <!-- 英語のコメント -->
    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="third_app"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- 英語のコメント -->
            <meta-data
                android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
                android:value="true" />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <!-- 次の記述を追加する -->
        <activity android:name="com.apptreesoftware.barcodescan.BarcodeScannerActivity"/> 

    </application>
</manifest>

android/build.gradleを以下のように編集します。自動的に生成されている場合もあります。その際は編集の必要はありません。
Kotlinで書かれているパッケージなのでこのような処理が必要となります。

buildscript {
    // 1.2.31以前だったら次を変更
    ext.kotlin_version = '1.2.71'
    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        // 記述がなければ次を追加
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

rootProject.buildDir = '../build'
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
    project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

android/app/build.gradleを以下のように編集します。自動的に生成されていることもあります。その際は編集の必要はありません。

/*
省略
*/

apply plugin: 'com.android.application'
// 次の記述がなければ追加
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 28

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    lintOptions {
        disable 'InvalidPackage'
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.third_app"
        minSdkVersion 16
        targetSdkVersion 28
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}

flutter {
    source '../..'
}

dependencies {
 // 次の記述がなければ追加
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    

AndroidMainifest.xmlandroid/build.gradle、android/app/build.gradleは以下のような位置にあります。

f:id:swamptk:20200114015532p:plain

ios

ios/Runner/info.plistにカメラの説明を以下のように追加します。

<key>NSCameraUsageDescription</key>
<string>Camera permission is required for barcode scanning.</string>

pubsec.ymlの編集

pubsec.ymlを以下のように編集します。

name: third_app
description: A new Flutter application.

# 英語の文章

version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  # 以下を追加する
  barcode_scan: ^0.0.3

dev_dependencies:
  flutter_test:
    sdk: flutter

# 以下省略

編集後は、packages getボタンを必ず押してください。

f:id:swamptk:20200115024754p:plain

QRアプリのプログラミング

いよいよアプリのプログラミングをしていきます。
lib/main.dartを以下のように編集します。
重要な点には、コメントを付けていますので合わせてお読みください。

import 'package:flutter/material.dart';
// 必要なQRコードを読み取る際に必要なpackageをインポート
import 'package:flutter/material.dart';
import 'package:barcode_scan/barcode_scan.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'QR scaner And QR Maker',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'QR scaner And QR Maker'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // 読み取り結果を格納するString型の変数readData
  String readData = "";

  /// QR及びバーコードスキャンを行うメソッドscan()
  /// Future:非同期処理を実現する
  Future scan() async {
    try {
      // String型のcodeにBarcodeScanner.scan()の結果を代入
      // await:非同期対応の要素のキーワード
      String code = await BarcodeScanner.scan();
      // readDataに読み取ったデータを格納する
      setState(() => this.readData = code);
    }
    // 例外処理:プラグインが何らかのエラーを出したとき
    on PlatformException catch (e) {
      // エラーコードがBarcodeScanner.CameraAccessDeniedであるときは、
      // このアプリにカメラ機能のパーミッションを許可していない状態であることを示す
      if (e.code == BarcodeScanner.CameraAccessDenied) {
        setState(() {
          // readDataにエラー内容を代入
          this.readData = 'カメラのパーミッションが有効になっていません。';
        });
      }
      // その他の場合は不明のエラー
      else {
        setState(() => this.readData = '原因不明のエラー: $e');
      }
    }
    // 意図しない入力、操作を受けたとき
    on FormatException{
      setState(() => this.readData = '読み取れませんでした (スキャンを開始する前に戻るボタンを使用しました)');
    } catch (e) {
      setState(() => this.readData = '不明なエラー: $e');
    }
  }


    @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'QRボタンを押すと読み取りを開始します',
            ),
            Text(
              // 読み取り結果readDataを表示
              '$readData',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      // ボタンを用意する
      floatingActionButton: FloatingActionButton(
        // onPressed:ボタンを押すことでscan()という関数が実行される
        onPressed: scan,
        tooltip: 'QR SCAN',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

実行結果

プログラムを実行すると、以下のような画面が現れます。
f:id:swamptk:20200118121146j:plain

下側のボタンを押すと、以下のようにカメラ画面が表示されます。
f:id:swamptk:20200118121307j:plain

QRコードを読み取ると、トップの画面に戻り読み取った結果が表示されます。
f:id:swamptk:20200118121424j:plain

バーコードの読み取りもできます。
f:id:swamptk:20200118121510j:plain