🐶

Flutter+Firebaseで課員の勤怠管理アプリを開発した話

7 min read

こんにちは、普段はメーカーでFlutterアプリ開発を行なっています。@Ysuke1018です🐶
どうでもいい話ですが、2021年1月1日にFlutterを触り始めたので、もうすぐ1年になろうとしています!!私は完全に独学でFlutterの勉強をしましたが、Flutter界隈の方々の記事やツイートを漁りまくってここまで来ました🐶
この1年はあっという間でしたし、Flutterの進化の速さには驚かされると同時に、Dartの書き心地の良さにはもう虜です!
(組み込み開発にはもう戻りたくない...。C言語はもうやだ...。ReactもいいけどCSS苦手人間...。)

タイトルにもある通り、FlutterとFirebaseで自分が所属している課の勤怠管理アプリを開発した記事を書いてみようと思います。

記事を書くのは初めてなので、誤植等ありましたらご指摘お願いします。
(その他、お手柔らかにどうぞ...🙏)

なぜ開発したのか??

勤怠管理は恐らく、どこの企業さんでも専用の勤怠管理システムを利用しているのではないかと思います。実際、私が所属している企業でも勤怠管理システムがあり、普段はそれを利用しています。

11月末ごろの話...

(部長) { 12月から2ヶ月間、この課だけフレックスタイム制のトライアルが始まります。}
(課員の脳内) { まじっ!!?うちの会社が時代に追いつこうとしてる...なんか裏がありそう。}
(部長) { あくまでもトライアルなので勤怠管理システムは対応していません。各自管理するように!}

とまあこんな感じの会話がありました。
フレックスタイム制の導入を検討している経緯は教えてもらえませんでしたが、通勤に車で1時間半程度かけている私としては、出退社時刻に裁量が持てるのは非常に嬉しい話でした🤗

ここで小さな問題

1. (課長)コアタイムは11時〜15時と定められているが、課員の出退社時刻をある程度把握したい。
2. (全員)現在、何時間勤務しているのか?残業時間はどれくらいか各々把握しなければならない。
3. スプレッドシートだと、会社のネットワークに入らないと開けない(自宅だと開けない)。
4. ↑個人のGoogleアカウントを会社のPCで利用することは禁じられている。

+個人的に遊びでしか使ったことがないFirebaseの知見を高めたいという考えがあったので、開発の提案をしました。
また、最近発表されたFlutterFire CLIを使ってみたいというのもありました。
基本的に私が所属している会社では、新しいことに挑戦している人を全面的に応援してくれるので、快諾してくれました✨ありがたい会社です。文句は言っちゃいけません。(お給料上げてください)

使用した主なライブラリ

  • cloud_firestore(勤怠データ等の保存)
  • firebase_auth(認証まわり)
  • flex_color_scheme(デザインが苦手な人でもいい感じの配色にしてくれるやつ)
  • hooks_riverpod(各種Providerで状態管理やDI)
  • flutter_hooks(最近あんまり使わなくなったけど一応入れている)
  • freezed(データクラス作る)
  • go_router(Navigator2.0?Routerと呼ぶんだっけ?に対応)※Web版も作ったので使用
  • shared_preferences(ちょっとしたデータ保存)
  • state_notifier(状態管理で使用)
  • table_calendar(カレンダーをサクっと作れる)
  • pedantic_mono(静的解析)

だいたいこんな感じです。特に珍しいものは使ってないと思います。RiverpodはFlutterの状態管理やDIの最有力候補になっているのではないでしょうか!?
私は各種Providerにかなり依存した書き方をしてるので、これなしでは開発できないかもしれません。

私がflutter_hooksをあまり使用しなかったのは、useXXXの恩恵を感じなくなってきたためです。
Future型の関数はFutureProviderを使用してAsyncValueで受け取る方が快適に感じます✨
Hookを自作していた時期もありましたが、そこまでしてHookにしなきゃいけない理由ってなんだ?と疑問に感じるようになったためです。(効果的なHookの使用方法を模索中です)

画面構成

※個人名などが出てる部分は隠しております。ご了承ください。

ログイン画面

何はともあれ、ログイン!
Firebase Authenticationを利用して、メールアドレスとパスワードのログイン画面です。

ログイン画面

ログインボタンを押したときに、入力のバリデートと誤操作防止用に薄〜く黒い画面を全面に出してボタンやフォームをタップできないようにしています。

ローディング画面

Stackの中に入れていたものをボタンのタップと同時に表示するだけの簡単な実装です。


final isShowIndicatorProvider = StateProvider.autoDispose((_) => false);

class LoginPage extends ConsumerWidget {
  const LoginPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final isShowIndicator = ref.watch(isShowIndicatorProvider);

    return Stack(
      children: [
	// 入力フォームとかボタンとか
        const LoginBody(),
	// ログインボタンをタップするとisShowIndicatorがtrueになる
        isShowIndicator ? const LoadingIndicator() : const SizedBox.shrink(),
      ],
    );
  }
}

勤怠情報(カレンダー)

ログインすると、ボトムナビゲーションで3つのページが開けます。
初期表示は勤怠情報を入力したり閲覧したりするページです。
画面はざっくりとこんな感じです。

  • 上部にカレンダー
  • 下部に選択している日付の課員の勤怠情報が表示されます。
  • 出社時刻〜退社時刻 勤務時間(休憩時間除く)という表示
  • 右下の+アイコンから日付・出社時刻・退社時刻の選択ダイアログを表示します。
  • また、下部に表示されている自身のスケジュールをタップすると編集ができるようにしています。

ダイアログに表示されている変更ボタンをタップするとFlutter標準のDatePickerとTimePickerを使用しています。
保存ボタンをタップするとFirestoreに勤怠情報が保存されるといった簡単なものです。

作成・編集・削除ができるのは、当然ですが自分自身のものだけです。
AuthenticationのIDをFirestoreに保存して、ログイン中のユーザー情報と比較して一致している場合に作成・編集・削除ができるといった感じです。

勤怠実績

勤怠実績の画面では、上部の「ユーザー」「年」「月」を選択すると、Firestoreに保存されている勤怠データ1ヶ月分から現在の勤務時間や残業時間とその詳細が表示できるようにしています。
ここの実装、地味に苦労しました笑。出退社時間によって休憩時間の取得時間が変わるので条件分岐がすごいことに...。

実装を簡単に説明すると

  1. ユーザー・年・月のデータクラスをfreezedで作成し、StateNotifierを継承したクラスを作る
  2. 上の3つのキーを監視して、値に変更があったときにFutureProviderでFirestoreからデータを取得する
  3. 取得したデータの変更をキーにして、勤務時間や残業時間を計算するFutureProviderを用意する

みたいな形で作りました。
正しい実装なのかはわかりませんが、自分的に1番シンプルな実装になったと思ってます。

結果画面

詳細画面

アプリの設定

ここは特別なものはないと思います🐶
ダークモードとか、flex_color_schemeのブレンドモードのレベルいじったりできて、SharedPreferenceに保存します。

設定画面

驚いたこと

Firebaseは遊びでしか使ってこなかったので、今回、「誰かが使う」アプリに使用したのは実質初めてになりました。

Firestoreからフィルターかけてデータ取得するのどうやるんだろうな〜
まあとりあえずやってみるか〜

ってな感じで書いたのが以下のコード(お恥ずかしい)

/// 実績画面で選択中のユーザー、年、月の値と一致するデータをFirestoreから取得する
final userWorkTimeListProvider =
    FutureProvider.autoDispose<QuerySnapshot<Map<String, dynamic>>>(
  (ref) async {
    ref.maintainState = true;

    // 監視するStateたち
    final user = ref.watch(
      recordControllerProvider.select((s) => s.selectedUser),
    );
    final year = ref.watch(
      recordControllerProvider.select((s) => s.selectedYear),
    );
    final month = ref.watch(
      recordControllerProvider.select((s) => s.selectedMonth),
    );

    // 選択中の****年**月1日
    final beginningOfMonth = DateTime(year, month, 1);
    // 選択中の****年翌月1日
    final beginningOfNextMonth = DateTime(year, month + 1, 1);
    // Firestoreのインスタンス(一応、シングルトンで定義してるやつです)
    final instance = FirebaseService().firestore;

   // scheduleコレクションに保存されているユーザーの勤怠スケジュールを1ヶ月分取得する
    final data = await instance
        .collection('schedule')
        .where(
          'schedule',
          isGreaterThanOrEqualTo: Timestamp.fromDate(beginningOfMonth),
        )
        .where(
          'schedule',
	  isLessThan: Timestamp.fromDate(beginningOfNextMonth),
        )
        .where(
          'user',
          isEqualTo: user,
        )
        .get();

    return data;
  },
);

Firestoreの設計って難しいですよね...何が正解なのか全くわかりません。この程度のアプリなら問題ないかもしれませんが、規模が大きくなってくると大変そうだなぁと感じました。
FlutterもDartもFirestoreも好きなので、しっかり勉強していきたいです💪


ざっくりとですが、全体像はこんな感じです。
StateNotifierやfreezedクラスの使い方などは、いろいろな記事があるので、特に実装部分は載せません。

それにしても、すっかりDartが心地よく感じる脳みそになってしまいました🐶
シングルトンクラスを作るときに使うClassName._()はいまだに慣れないですし、分かっているつもりだけど腹落ちしないと言った感じですが、Live Templateに登録してしまっているので、そこら辺はもう無視することにしています笑。

おわりに

記事を読んでいただいてありがとうございました🐶
正直、何も参考になる情報はなかったと思いますが...。
とりあえず、自分の備忘録だと思ってアウトプットすることにしました。
このアプリを使っている部長、課長、課員からの評判も意外とよくて、誰かが使って喜んでもらえるものを作るっていいなぁと感じた次第です。

今後勉強したいこと

  • Firestoreの設計
  • Flutterの進化にしっかり追従
  • Reactも頑張りたいです...一応。
悲しいお知らせ

勤怠システムがもうすぐフレックスタイム対応するらしく、このアプリ不要になりました笑。

それでは皆様、よい年末を〜🐶

Discussion

ログインするとコメントできます