できあがるもの
M5StackとFlutterをFirebase経由させてコラボさせるスマートロック#M5Stack #Flutter #Firebase pic.twitter.com/nNjIdM0gtb
— きしー (@bigface0202) December 24, 2020
はじめに
前回までの記事でM5StackとFirebaseを組み合わせてスマートロックみたいなものが完成しました。
最終章であるこの記事では、Flutterというフレームワークを使ってスマホから入退場状況を見れるような
管理者用アプリを作製していきます。
ちなみにこの実装はおまけみたいなものなので、無くてもスマートロックは動きます。
過去記事:
M5StackとFirebaseを連携してスマートロックみたいなものを作る!①
M5StackとFirebaseを連携してスマートロックみたいなものを作る!②
M5StackとFirebaseを連携してスマートロックみたいなものを作る!③
M5StackとFirebaseを連携してスマートロックみたいなものを作る!④
Flutterとは?
[Flutter](https://flutter.dev/)はGoogle先生が開発しているマルチプラットフォーム開発フレームワークの1種で、 Dartという言語で書いていきます。 マルチプラットフォームなので、iOS、Android、Webアプリの3つを同時並行で作っていくことが可能です。 今のところWebアプリ開発も徐々に充実しつつあり、iOS、Androidであればかなり充実しています!学習するようになった経緯としては、Web系は全然知らない自分でしたが、ある日「スマホアプリ作りたい!」と思い立ち色々探した結果、「FlutterできればiOS, Android網羅できて最強じゃん!」ってなって学習を初めた次第です。
最近はKotlinも学習をし始めていますが、UIを別ファイルで作るあたりなどがなんか慣れないですね笑
余談が過ぎましたが、ESP32とFlutterをコラボさせる動画も結構あります。
特にThat Projectさんのチャンネルはめちゃくちゃ参考になりました。
環境構築
環境構築は本記事では取り扱いませんので、下記記事を是非参考にしてください。
Flutterの環境整備
Flutterの環境構築は下記URLを参考にしてください。
Flutter 開発環境構築手順 (2019年 保存版)
Firebaseにアプリを登録
Firebaseに作っていくアプリを登録する必要があります。
下記URLなどを参考に登録してください。
Flutterに初めてのFirebase導入(Firebase Analytics)
ライブラリ
ファイル構成
- main.dart
- user_overview.dart
コード
main.dart
import 'package:flutter/material.dart';
import './user_overview.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: 'M5Stack Smart Lock',
home: Scaffold(
appBar: AppBar(
title: Text('M5Stack Smart Lock'),
),
body: UserOverview(),
),
);
}
}
main.dartについては特に特記することはありません。
Firebase RDB読み出し部分
import 'package:firebase_database/ui/firebase_animated_list.dart';
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
class UserEntry {
String key;
String date;
String id;
bool entered;
UserEntry(this.date, this.id, this.entered);
UserEntry.fromSnapShot(DataSnapshot snapshot)
: key = snapshot.key,
id = snapshot.value["id"],
date = snapshot.value["date"],
entered = snapshot.value["entered"];
toMap() {
return {
"id": id,
"date": entered,
"entered": entered,
};
}
}
class UserOverview extends StatefulWidget {
@override
_UserOverviewState createState() => _UserOverviewState();
}
class _UserOverviewState extends State<UserOverview> {
final _mainReference = FirebaseDatabase.instance.reference().child('User');
List users = new List();
int enteredNum = 0;
@override
void initState() {
super.initState();
_mainReference.onChildAdded.listen(_onUserAdded);
_mainReference.onChildChanged.listen(_onUserUpdated);
_mainReference.onChildRemoved.listen(_onUserDeleted);
}
_onUserAdded(Event e) {
setState(() {
users.add(new UserEntry.fromSnapShot(e.snapshot));
});
enteredNum = 0;
users.forEach((element) {
if (element.entered) {
enteredNum++;
}
});
}
_onUserUpdated(Event e) {
var oldEntriesValue =
users.singleWhere((user) => user.id == e.snapshot.value['id']);
setState(() {
users[users.indexOf(oldEntriesValue)] =
new UserEntry.fromSnapShot(e.snapshot);
});
enteredNum = 0;
users.forEach((element) {
if (element.entered) {
enteredNum++;
}
});
}
_onUserDeleted(Event e) {
var deletedEntriesValue =
users.singleWhere((user) => user.id == e.snapshot.value['id']);
setState(() {
users.removeAt(users.indexOf(deletedEntriesValue));
});
enteredNum = 0;
users.forEach((element) {
if (element.entered) {
enteredNum++;
}
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
buildStatistics(
enteredNum.toString(),
'ACTIVE',
Colors.red,
),
buildStatistics(
users.length.toString(),
'TOTAL',
Colors.orange,
)
],
),
Divider(
color: Colors.grey,
),
Expanded(
child: FirebaseAnimatedList(
defaultChild: CircularProgressIndicator(),
query: _mainReference,
itemBuilder: (_, snapshot, Animation<double> animation, int index) {
return Column(
children: [
ListTile(
leading: CircleAvatar(
radius: 26,
foregroundColor: Colors.white,
backgroundColor: snapshot.value['entered']
? Colors.red
: Colors.blueAccent,
child: Text(
snapshot.value['entered'] ? 'In' : 'Out',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
'UID:${snapshot.value['id']}',
style: TextStyle(fontSize: 20),
),
subtitle: Text(
'登録日:${snapshot.value['date']}',
style: TextStyle(fontSize: 16),
),
),
Divider(
color: Colors.grey,
),
],
);
},
),
),
],
);
}
Padding buildStatistics(
String circleText, String caption, Color circleColor) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: circleColor,
foregroundColor: Colors.white,
child: Text(
circleText,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
Text(
caption,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}
firebase_databaseが便利すぎて感動しました。
データ構造部分
class UserEntry {
String key;
String date;
String id;
bool entered;
UserEntry(this.date, this.id, this.entered);
UserEntry.fromSnapShot(DataSnapshot snapshot)
: key = snapshot.key,
id = snapshot.value["id"],
date = snapshot.value["date"],
entered = snapshot.value["entered"];
toMap() {
return {
"id": id,
"date": entered,
"entered": entered,
};
}
}
FirebaseのRDB内で何かしら変化があったときDataSnapshot
を受け取り、
変化があった部分のみをMap
(json)の形式に変換してリターンするクラスです。
Firebase RDB内の変化をキャッチ
@override
void initState() {
super.initState();
_mainReference.onChildAdded.listen(_onUserAdded);
_mainReference.onChildChanged.listen(_onUserUpdated);
_mainReference.onChildRemoved.listen(_onUserDeleted);
}
_onUserAdded(Event e) {
setState(() {
users.add(new UserEntry.fromSnapShot(e.snapshot));
});
enteredNum = 0;
users.forEach((element) {
if (element.entered) {
enteredNum++;
}
});
}
_onUserUpdated(Event e) {
var oldEntriesValue =
users.singleWhere((user) => user.id == e.snapshot.value['id']);
setState(() {
users[users.indexOf(oldEntriesValue)] =
new UserEntry.fromSnapShot(e.snapshot);
});
enteredNum = 0;
users.forEach((element) {
if (element.entered) {
enteredNum++;
}
});
}
_onUserDeleted(Event e) {
var deletedEntriesValue =
users.singleWhere((user) => user.id == e.snapshot.value['id']);
setState(() {
users.removeAt(users.indexOf(deletedEntriesValue));
});
enteredNum = 0;
users.forEach((element) {
if (element.entered) {
enteredNum++;
}
});
}
RDB内でレコードの追加・削除、レコード内フィールド値の変更(今回の場合入退場)があった場合に各種_onUserAdded
, _onUserDeleted
、_onUserUpdated
がコールされるようになります。
ここで、enteredNum
に現在の入場者の人数を計算して格納していますが、計算する前に毎回enteredNum
をリセットしています。
というのも、ここの挙動が1個レコードが作られるたびにコールされるので3つのレコードがあった場合、レコード1でコール、レコード1・2でコール、レコード1・2・3でコールされてしまうため、それぞれのコールでenteredNumが累積和されてしまうのです。
そのため、最後のコールでのentered
がtrue
な数を知れればいいので、こういう形になっています。
描画部分
入場者とトータルの人数を描画
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
buildStatistics(
enteredNum.toString(),
'ACTIVE',
Colors.red,
),
buildStatistics(
users.length.toString(),
'TOTAL',
Colors.orange,
)
],
),
Padding buildStatistics(
String circleText, String caption, Color circleColor) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: circleColor,
foregroundColor: Colors.white,
child: Text(
circleText,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
Text(
caption,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
入場者と登録者の数を描画する部分です。ここで入場者には前出のenteredNum
、登録者にはusers
の大きさを利用しています。
個人的にCircleAvatar
が結構好きなので、つい多用してしまいます。
ユーザー全体の情報を可視化
FirebaseAnimatedList(
defaultChild: CircularProgressIndicator(),
query: _mainReference,
itemBuilder: (_, snapshot, Animation<double> animation, int index) {
return Column(
children: [
ListTile(
leading: CircleAvatar(
radius: 26,
foregroundColor: Colors.white,
backgroundColor: snapshot.value['entered']
? Colors.red
: Colors.blueAccent,
child: Text(
snapshot.value['entered'] ? 'In' : 'Out',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
'UID:${snapshot.value['id']}',
style: TextStyle(fontSize: 20),
),
subtitle: Text(
'登録日:${snapshot.value['date']}',
style: TextStyle(fontSize: 16),
),
),
Divider(
color: Colors.grey,
),
],
);
},
),
ここで使っているFirebaseAnimatedList
は指定したquery
からAnimatedListを作ってくれます。
defaultChild
にはRDB読込中に表示するWidgetを指定することが可能です。非常に使いやすいです。
ただ今回の場合users
に同じ情報が入っているので、FirebaseAnimatedList
を使ったのは冗長だったかもしれません。
使い方として載せておきたかったので、とりあえず使いました笑
おわりに
これでM5Stackを使ったスマートロックとFlutterによるAdminアプリが作製できました。
せっかくFirebase使ってますし、Adminログインとユーザーの削除機能も作りたいですね。
あとは、IDだけじゃ判別がつかないので「M5Stack経由でカード登録」→「Admin側で名前などの属性情報も追加で登録」みたいな形にすると、
もっと使いやすくなると思います。
あと何気に今日でAdventカレンダー最終日です。たくさん書いたおかげでこの12月で、特にFlutter力が上がった気がします😂
会社ではあまりFlutter使う機会はないですが、個人的にめちゃくちゃ面白くなってきているので来年もたくさんFlutter使いたいと思います!
皆さん良いお年を!