はじめに
flutterでQRコードを読み取り、そのデータを履歴として管理できるアプリを作ってみたので、自分用の備忘として記録を残します。
(なお筆者はflutterの初学者であるためお手柔らかにご覧いただけますと幸いです。誤っている点の指摘やコメントはウェルカムです!)
開発環境
- Windows 10 Pro 21H2 19044.1826
- Flutter 3.0.1
- Dart 2.17.1
- Android Studio Chipmunk 2021.2.1
画面遷移
本アプリは以下の画面から構成されています。
実装
プロジェクト作成時のテンプレートから変更した点を抜粋して記載します。
なお、今回のアプリでは履歴のデータをfirebase
で管理していますが、そちらの設定は割愛します。
プロジェクト設定
ここには使用するパッケージの設定等を記載します。
dependencies
にfirebase
との連携で必要になるfirebase_core
, firebase_auth
, cloud_firestore
やQRコードの生成・読み込みで必要になるqr_flutter
, qr_code_scanner
、QRコードから読み込んだURLにアクセスする際に必要となるurl_launcher
等を追加しています。
name: qr_scanner_history
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ">=2.17.1 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_slidable:
shared_preferences:
firebase_core: ^1.4.0
firebase_auth: ^1.0.1
cloud_firestore: ^2.4.0
url_launcher: ^6.1.4
cupertino_icons: ^1.0.2
qr_flutter: ^4.0.0
qr_code_scanner: ^1.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
main関数
アプリが起動するとrunApp
が実行されます。
こちらの記載は最小限にしており、以降のファイルにそれぞれの画面の処理を記載しています。
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'app.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
読み込み履歴一覧
ここには、これまでQRコードから読み込んだデータの一覧表示と、QRコード読み取り画面に遷移するためのボタン配置等が記載されています。
import 'package:flutter/material.dart';
import 'dart:developer';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'details.dart';
import 'add.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// 右上に表示される"debug"ラベルを消す
debugShowCheckedModeBanner: false,
// アプリ名
title: 'QR App',
// ダークテーマ
theme: ThemeData.dark(),
// リスト一覧画面を表示
home: QRListPage(),
);
}
}
// リスト一覧画面用Widget
class QRListPage extends StatefulWidget {
@override
_QRListPageState createState() => _QRListPageState();
}
class _QRListPageState extends State<QRListPage> {
String? data;
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: Scaffold(
appBar: AppBar(
title: Text('QR List'),
),
body: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('QR').snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
return ListView(
children: snapshot.data!.docs.map((DocumentSnapshot document) {
return Card(
child: ListTile(
leading: Icon(Icons.label_important),
title: Text(document.get('text')),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailsPage(document.get('text'))),
);
},
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () async {
// 対象のドキュメントを削除
await FirebaseFirestore.instance
.collection('QR')
.doc(document.id)
.delete();
},
)),
);
}).toList(),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final newListText = await Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
// 遷移先の画面としてリスト追加画面を指定
return QRAddPage();
}),
);
},
child: Icon(Icons.add),
),
),
);
}
}
QRコード読み取り
ここにはQRコードを読み取りfirebase
にデータを追加する処理等が記載されています。
import 'package:flutter/material.dart';
import 'dart:developer';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class QRAddPage extends StatefulWidget {
@override
_QRAddPageState createState() => _QRAddPageState();
}
class _QRAddPageState extends State<QRAddPage> {
Barcode? result;
QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
String _text = '';
final myController = TextEditingController();
final upDateController = TextEditingController();
var _selectedvalue;
// データを元に表示するWidget
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('リスト追加'),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 入力されたテキストを表示
Text(_text, style: TextStyle(color: Colors.blue)),
const SizedBox(height: 8),
// テキスト入力
TextField(
// 入力されたテキストの値を受け取る(valueが入力されたテキスト)
onChanged: (String value) {
// データが変更したことを知らせる(画面を更新する)
setState(() {
// データを変更
_text = value;
});
},
),
const Text('赤枠の中にQRコードをかざしてください'),
const SizedBox(height: 8),
Container(
// 横幅いっぱいに広げる
width: double.infinity,
// リスト追加ボタン
child: ElevatedButton(
onPressed: () async {
final date =
DateTime.now().toLocal().toIso8601String(); // 現在の日時
// Firebaseにデータを追加し、"pop"で前の画面に戻る
await FirebaseFirestore.instance
.collection('QR') // コレクションID
.doc() // ドキュメントID
.set({'text': _text, 'date': date});
Navigator.of(context).pop();
},
child: Text('リスト追加', style: TextStyle(color: Colors.white)),
),
),
const SizedBox(height: 8),
Container(
// 横幅いっぱいに広げる
width: double.infinity,
// キャンセルボタン
child: TextButton(
// ボタンをクリックした時の処理
onPressed: () {
// "pop"で前の画面に戻る
Navigator.of(context).pop();
},
child: Text('キャンセル'),
),
),
Expanded(flex: 4, child: _buildQrView(context)),
Expanded(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.toggleFlash();
setState(() {});
},
child: FutureBuilder(
future: controller?.getFlashStatus(),
builder: (context, snapshot) {
// ライトの状態
if (snapshot.data == true) {
return Text('ライト点灯');
} else {
return Text('ライト消灯');
}
},
)),
),
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.flipCamera();
setState(() {});
},
child: FutureBuilder(
future: controller?.getCameraInfo(),
builder: (context, snapshot) {
// カメラの状態
if (snapshot.data != null &&
(describeEnum(snapshot.data!)) ==
'back') {
return Text('アウトカメラ');
} else if (snapshot.data != null &&
(describeEnum(snapshot.data!)) ==
'front') {
return Text('インカメラ');
} else {
return Text('loading ${snapshot.data}');
}
},
)),
)
],
),
],
),
),
)
],
),
),
);
}
Widget _buildQrView(BuildContext context) {
// デバイスの幅や高さを確認し、それに応じてscanAreaとoverlayを変更
var scanArea = (MediaQuery.of(context).size.width < 400 ||
MediaQuery.of(context).size.height < 400)
? 150.0
: 300.0;
// Scannerビューが回転した後、適切にサイズ変更されるようにするために
// Flutter SizeChanged 通知をリスニングし、コントローラを更新する必要があります。
return QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.red,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
);
}
void _onQRViewCreated(QRViewController controller) {
setState(() {
this.controller = controller;
});
controller.scannedDataStream.listen((scanData) {
setState(() {
result = scanData;
_text = scanData.code.toString();
});
});
}
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
log('${DateTime.now().toIso8601String()}_onPermissionSet $p');
if (!p) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('no Permission')),
);
}
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}
履歴詳細/ウェブブラウザ
ここには読み取ったデータとそのデータから生成したQRコードの表示、また読み取ったデータがURLの場合にブラウザでアクセスする処理等が記載されています。
import 'package:flutter/material.dart';
import 'dart:developer';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
class DetailsPage extends StatelessWidget {
@override
DetailsPage(this.name);
String name;
void _opneUrl(String _url,) async {
final url = Uri.parse(_url);
// URLが有効な場合は、「launchUrl」メソッドを実行
if (await canLaunchUrl(url)) {
await launchUrl(
url,
// デフォルトだとアプリ内WebViewになっておりブラウザを起動させたい場合はこの引数が必要
// mode: LaunchMode.externalApplication,
);
// URLが無効の場合はエラーをスロー
} else {
throw 'このURLにはアクセスできません';
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('QR Details'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
child: Card(
child: ListTile(
leading: Icon(Icons.label_important),
title: Text(name),
onTap: () {
_opneUrl(name);
}),
),
),
Container(
width: double.infinity,
color: Colors.grey[200],
child: Column(
children: <Widget>[
QrImage(
data: name,
size: 200,
),
],
),
),
],
));
}
}
さいごに
今回、初めてflutterを触ってみた感想としては、外部パッケージが優秀で簡単な実装でQRコードの読み取り・生成やウェブブラウザでのアクセスができることに驚きました。
まだまだ理解が及んでいない点が多々ありますので、継続して実装や記事の投稿をしていきたいと考えています。
参考文献
Qiita
Zenn
Flutter Package