Flutter始めました のイキオイで、QRコードを読むアプリ(2020年4月末版)を作ってみる。
環境
- macOS Mojave 10.14.6
- Android Studio 3.6.3
なお、flutter --version
の結果は、以下の通り。
% flutter --version
Flutter 1.12.13+hotfix.9 • channel stable •
https://github.com/flutter/flutter.git
Framework • revision f139b11009 (4 weeks ago) • 2020-03-30 13:57:30 -0700
Engine • revision af51afceb8
Tools • Dart 2.7.2
準備
まずは、Android Studioにて、New Flutter Project...
する。
いつもの、ボタンをクリックすると数字がカウントアップされるアプリの雛形が生成される。
やること
- デザインの変更
- ライブラリのimport
- 実機でテスト
デザインの変更
デザインの変更といっても、大したことはしない。
ボタンの変更
ボタンのデザインをカメラにして、tooltipも変えておく。
...
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Scan',
child: Icon(Icons.camera),
),
...
タイトルなどの変更もする。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'QR Scanner',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'QRcode Scanner'),
);
}
}
とりあえず実行してみるが、カメラアイコンのボタンを押すと、ただ数字がカウントアップされるというちぐはぐ状態だ。
ライブラリのimport
QRコード読み取り実装のための準備
barcode_scan を使う。
ドキュメントに従い、Android用の設定と、iOS用の設定を行う。
まずは、カメラシステムを使うための設定。
AndroidManifest.xml
の<manifest...
内に、<uses-permission android:name="android.permission.CAMERA" />
を追記する。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.gr.javaconf.fukuit.qrscanner">
<uses-permission android:name="android.permission.CAMERA" />
...
ドキュメントによると、android/build.gradle
とandroid/app/build.gradle
にkotlinを使う設定を追記すべしということになっているが、デフォルトで記載済になっていた。
おそらく、Android StudioにKotlinプラグインがインストールされているからだと思うけれど、詳細は不明。とにかく、記載済なのだから問題ない。
次に、pubspec.yaml
のdependenciesの項に、barcode_scan
のことを追記する。
dependencies:
...
# https://pub.dev/packages/barcode_scan
barcode_scan: any
...
また、iOSでも使えるようにするために、ios/Runner/Info.plist
にもカメラ使用許可のための記載を追記する。
<dict>
...
<key>NSCameraUsageDescription</key>
<string>Camera permission is required for barcode scanning.</string>
...
</dict>
ここまでやったら、flutter packages get
する。
% flutter packages get
Running "flutter pub get" in qr_scanner... 2.1s
barcode_scan.dartのimport
barcode_scan.dart
をimportして、QRコード読み取り部の実装をする。
とりあえず、ボタンを押したらカメラが起動して、QRコードを読み込んだら、その内容をテキストで表示するだけのアプリとして実装する。
ということで、ひな形のアプリのvoid _incrementCounter()
を_scan()で置き換える方針で。
まずは、barcode_scan.dart
のimport文を追加する。
import 'package:barcode_scan/barcode_scan.dart';
次に、class _MyHomePageState extends State<MyHomePage>
を書き換える。barcode_scanのサンプルコードを参考にする。
class _MyHomePageState extends State<MyHomePage> {
ScanResult scanResult;
Future _scan() async {
try {
var result = await BarcodeScanner.scan();
setState(() => scanResult = result);
} on PlatformException catch (e) {
var result = ScanResult(
type: ResultType.Error,
format: BarcodeFormat.unknown,
);
if (e.code == BarcodeScanner.cameraAccessDenied) {
setState(() {
result.rawContent = 'カメラへのアクセスが許可されていません!';
});
} else {
result.rawContent = 'エラー: $e';
}
setState(() {
scanResult = result;
});
}
}
...
}
上記のような、_scan()
関数を作成する。BarcodeScanner.scan()
の戻り値はScanResult
型で、type
、rawContent
、format
、formatNote
というプロパティを持ち、rawContent
にQRコードの内容が保持されている。
非同期処理を行うための戻り値としてのFuture
型とかasync
キーワードとかに注意。async
するために、dart:async
をimportする。また、この関数内の処理で、例外としてPlatformExceptionをcatchするので、package:flutter/services.dart
をimportする。
ここで、scanResult
にQRコードを読み込んだ結果を保存するようにしたので、scanResult
に読み込んだ結果が入っているときはその結果を、そうでない時は「ボタンを押すとカメラが起動します」的なメッセージを表示したい。
そこで、Widgetの定義をする関数のなかで、if文を使いたい。というか、barcode_scanのサンプルコードもそうなっている。
pubspec.yamlの修正
ここまで来ると、以下のような警告が表示される。
info: The for, if, and spread elements weren't supported until version 2.3.0, but this code is required to be able to run on earlier versions.
このメッセージのとおり、pubspec.yaml
のsdk:">=2.1.2 < 3.0.0"
のところを修正しておく。
...
environment:
sdk: ">=2.3.0 <3.0.0"
...
build.gradleの修正
Android用にbuildしたところ、次のようなエラーが発生した。
...
AndroidManifest.xml as the library might be using APIs not available in 16
Suggestion: use a compatible library with a minSdk of at most 16,
or increase this project's minSdk version to at least 18,
...
AndroidManifest.xmlにminSDKの設定があるのかと思いきや、そのような設定は存在しなかった。
そこで、探してみると、android/app/build.gradle
のdefaultConfig
の中でminSdkVersion
が定義されていたため、これを16から18に変更した。
defaultConfig {
...
minSdkVersion 18
...
}
lib/main.dartの完成
以上を踏まえて、以下のようにlib/main.dart
を編集した。
@override
Widget build(BuildContext context) {
var contentList = <Widget>[
if (scanResult != null)
Card(
child: Column(
children: <Widget>[
ListTile(
title: Text("Result Type"),
subtitle: Text(scanResult.type?.toString() ?? ""),
),
ListTile(
title: Text("RawContent"),
subtitle: Text(scanResult.rawContent ?? ""),
),
ListTile(
title: Text("Format"),
subtitle: Text(scanResult.format?.toString() ?? ""),
),
ListTile(
title: Text("Format note"),
subtitle: Text(scanResult.formatNote ?? ""),
),
],
),
),
ListTile(
title: Text("ボタンを押してカメラを起動してください"),
subtitle: Text("カメラをQRコードに向けてください"),
),
];
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: contentList,
),
floatingActionButton: FloatingActionButton(
onPressed: _scan,
tooltip: 'Scan',
child: Icon(Icons.camera),
),
));
}
これを実行すると、以下のようになる。
実機でテスト
ここまではAVDで実行結果を見ながらコーディングを進めてきたけれど、AVDではカメラを使ったテストができないので、iPhone8実機で試してみる。
「設定」の「プロファイルとデバイス管理」で、自分のサインを信頼した上で、実行する。
読み込むQRコード画像は、python
のqrcode
パッケージを使って作成した1。
実行結果
起動すると、次の様な画面になる。
カメラボタンを押すと、QRコードを読み込むためのカメラが起動する。ちょうど、バーコードを読み込むところのスクリーンショットを撮りたかったんだけれど、なんどやってもタイミングが合わなかった。
読み込んだ結果は、次の様に表示される。
読み取り結果については、contentlist.addAll()
で、QRコードを読み込むたびに履歴として追記していくこともできるんじゃなかろうか(試してない)。
本日のコード
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.gr.javaconf.fukuit.qrscanner">
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name="io.flutter.app.FlutterApplication"
android:label="qrscanner"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
name: qrscanner
description: QRcode Scanner Application
version: 1.0.0+1
environment:
sdk: ">=2.3.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
# https://pub.dev/packages/barcode_scan
barcode_scan: any
dev_dependencies:
flutter_test:
sdk: flutter
# The following section is specific to Flutter.
flutter:
uses-material-design: true
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>qrscanner</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>Camera permission is required for barcode scanning.</string>
</dict>
</plist>
import 'dart:async';
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 {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'QR Scanner',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'QR code Scanner'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScanResult scanResult;
Future _scan() async {
try {
var result = await BarcodeScanner.scan();
setState(() => scanResult = result);
} on PlatformException catch (e) {
var result = ScanResult(
type: ResultType.Error,
format: BarcodeFormat.unknown,
);
if (e.code == BarcodeScanner.cameraAccessDenied) {
setState(() {
result.rawContent = 'カメラへのアクセスが許可されていません!';
});
} else {
result.rawContent = 'エラー: $e';
}
setState(() {
scanResult = result;
});
}
}
@override
Widget build(BuildContext context) {
var contentList = <Widget>[
if (scanResult != null)
Card(
child: Column(
children: <Widget>[
ListTile(
title: Text("Result Type"),
subtitle: Text(scanResult.type?.toString() ?? ""),
),
ListTile(
title: Text("RawContent"),
subtitle: Text(scanResult.rawContent ?? ""),
),
ListTile(
title: Text("Format"),
subtitle: Text(scanResult.format?.toString() ?? ""),
),
ListTile(
title: Text("Format note"),
subtitle: Text(scanResult.formatNote ?? ""),
),
],
),
),
ListTile(
title: Text("ボタンを押してカメラを起動してください"),
subtitle: Text("カメラをQRコードに向けてください")),
];
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
scrollDirection: Axis.vertical,
shrinkWrap: true,
children: contentList,
),
floatingActionButton: FloatingActionButton(
onPressed: _scan,
tooltip: 'Scan',
child: Icon(Icons.camera),
),
));
}
}
次の予定としては、読み込んだQRコードの内容を、どこかにストアして後から読み出せるようにする仕組み作りかな。
-
qrcodeを
% pip install -U qrcode
でインストールしてから、% qr 'こんにちは、世界' > hello.png
して作成した。 ↩