M5StackとFirebaseとFlutterを連携してスマートロックみたいなものを作る！（おまけ）

# できあがるもの
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">M5StackとFlutterをFirebase経由させてコラボさせるスマートロック<a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://twitter.com/hashtag/Flutter?src=hash&amp;ref_src=twsrc%5Etfw">#Flutter</a> <a href="https://twitter.com/hashtag/Firebase?src=hash&amp;ref_src=twsrc%5Etfw">#Firebase</a> <a href="https://t.co/nNjIdM0gtb">pic.twitter.com/nNjIdM0gtb</a></p>&mdash; きしー (@bigface0202) <a href="https://twitter.com/bigface0202/status/1342123310734196736?ref_src=twsrc%5Etfw">December 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# はじめに
前回までの記事でM5StackとFirebaseを組み合わせてスマートロックみたいなものが完成しました。
最終章であるこの記事では、Flutterというフレームワークを使ってスマホから入退場状況を見れるような
管理者用アプリを作製していきます。
ちなみにこの実装はおまけみたいなものなので、無くてもスマートロックは動きます。

過去記事：
[M5StackとFirebaseを連携してスマートロックみたいなものを作る！①](https://qiita.com/bigface00/items/87346c31d12346f85716)
[M5StackとFirebaseを連携してスマートロックみたいなものを作る！②](https://qiita.com/bigface00/items/29511d3a4425da5a499c)
[M5StackとFirebaseを連携してスマートロックみたいなものを作る！③](https://qiita.com/bigface00/items/e92f3a12ed0162c17d84)
[M5StackとFirebaseを連携してスマートロックみたいなものを作る！④](https://qiita.com/bigface00/items/d59ba7ccffce99ce259a)

# Flutterとは？
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/ab97177e-a9f4-9f45-1e6c-51a152d8d84a.png" width=300>
[Flutter](https://flutter.dev/)はGoogle先生が開発しているマルチプラットフォーム開発フレームワークの1種で、
Dartという言語で書いていきます。
マルチプラットフォームなので、<b>iOS、Android、Webアプリの3つを同時並行で作っていくことが可能</b>です。
今のところWebアプリ開発も徐々に充実しつつあり、iOS、Androidであればかなり充実しています！

学習するようになった経緯としては、Web系は全然知らない自分でしたが、ある日「スマホアプリ作りたい！」と思い立ち色々探した結果、「FlutterできればiOS, Android網羅できて最強じゃん！」ってなって学習を初めた次第です。
最近はKotlinも学習をし始めていますが、UIを別ファイルで作るあたりなどがなんか慣れないですね笑
余談が過ぎましたが、ESP32とFlutterをコラボさせる動画も結構あります。
特に<b>[That Project](https://www.youtube.com/channel/UCRr2LnXXXuHn4z0rBvpfG7w)</b>さんのチャンネルはめちゃくちゃ参考になりました。

# 環境構築
環境構築は本記事では取り扱いませんので、下記記事を是非参考にしてください。

## Flutterの環境整備
Flutterの環境構築は下記URLを参考にしてください。
[Flutter 開発環境構築手順 (2019年 保存版)](https://qiita.com/tomy0610/items/896dc8ec9ba95c33194f)

## Firebaseにアプリを登録
Firebaseに作っていくアプリを登録する必要があります。
下記URLなどを参考に登録してください。
[Flutterに初めてのFirebase導入(Firebase Analytics)](https://qiita.com/kasa_le/items/31a7b858b1969e75016c)

## ライブラリ
- [firebase_database](https://pub.dev/packages/firebase_database)

## ファイル構成
- main.dart
- user_overview.dart

# コード
## main.dart
```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読み出し部分
```dart:user_overview.dart
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が便利すぎて感動しました。

### データ構造部分
```dart:RDBで変化があった部分がここで成型される
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内の変化をキャッチ
```dart:各種イベント
@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```な数を知れればいいので、こういう形になっています。

## 描画部分
### 入場者とトータルの人数を描画
```dart:入場者数と登録者数の可視化
     Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            buildStatistics(
              enteredNum.toString(),
              'ACTIVE',
              Colors.red,
            ),
            buildStatistics(
              users.length.toString(),
              'TOTAL',
              Colors.orange,
            )
          ],
        ),
```
```dart:buildStatistics
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```が結構好きなので、つい多用してしまいます。

### ユーザー全体の情報を可視化
```dart:FirebaseAnimatedListの使いやすさに感動
         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使いたいと思います！
皆さん良いお年を！
# 参考にしたページ
- [FlutterでFirebase Realtime Databaseを使ってみた](https://dev.classmethod.jp/articles/flutter_firebase-realtime-database1/)
- [【TODOアプリ作成】Flutter・Firebaseでゼロからアプリをつくる。](https://ueqareer.net/3056)