Flutterだけでサービスを完結させたい、欲張りな僕みたいなあなたへ。

こんにちは、Flutterエンジニアとしてお仕事をしているyamoriです。

もともと自分は「自分で作ったサービスを世に出したい！」という思いからFlutterを始めました。
プロダクトを作っているうちに、作ること自体も楽しくなってしまい、この記事を書いています。

##1.背景
Flutterでサービスを開発するときに、純粋なCtoCのサービスでない限り、サービス提供側のアクション（たとえばお知らせ画面とかユーザー管理画面とか）が必要になることが多いと思います。
管理画面は、大抵の場合スマホアプリではなくWEBで開発することになると思うのですが、WEBだけReactなど他のフレームワークを作ると学習することが増えてしまいます・・・（それはそれで勉強になるのですが）

この記事は、Flutterの思想である「1つのコードベースから、美しいモバイル、ウェブ、デスクトップ、組み込みアプリケーションを構築、テスト、デプロイする」
`Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase.`
を活かすべく、管理画面もFlutterで作ってしまおうという試みです。
（引用元：https://flutter.dev/）

##2.完成形
完成形は以下のような感じです。
「ユーザーがアプリに登録したとき、その情報を管理画面にも表示する」というユースケースを想定しています。

![スクリーンショット 2021-12-07 17.32.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/53867108-a10a-31da-8917-3b80527a7795.png)


##3.はじめよう
###3-1.Flutterプロジェクトの作成
まずは、Flutterプロジェクトを作成しましょう。
![スクリーンショット 2021-12-07 14.25.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/f5ce3775-bf6f-de75-07c9-14045153da41.png)

ここで、ターミナルで
`flutter run -d chrome`
 と打つことで、chromeでrunすることができます。

![スクリーンショット 2021-12-07 14.28.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/efda3e41-6c6f-4b31-8832-0ca65ca6871f.png)
↑いつものカウントアプリがchromeで表示されました。

###3-2.Firebaseプロジェクトの作成
次に、Firebaseを繋げましょう。
![スクリーンショット 2021-12-07 14.51.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/0987e8dd-b9fc-b0c8-226e-a2549966f891.png)

アプリを追加から、

![スクリーンショット 2021-12-07 14.54.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/bfdcf2a1-bb12-cbb9-9ac9-5a460ca85d18.png)
ウェブを選択します。
![スクリーンショット 2021-12-07 14.56.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/fb7f9e25-f7c1-f361-184a-aa83195016fc.png)

アプリの名前だけ登録し、後のプロセスはスキップしてコンソール画面に戻ります。
![スクリーンショット 2021-12-07 17.34.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/6b353238-6409-c7d7-e7cf-16a3681b0fd1.png)

コンソール画面の左側にある歯車の設定にいきます。

![スクリーンショット 2021-12-07 15.26.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/cb32f9ca-5e1c-020c-c519-30fd5f129086.png)

webプロジェクトのCDNを選択してプロジェクトのweb/index.htmlに設定します。
![スクリーンショット 2021-12-07 17.49.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/9e0c6af4-1c1d-5170-419d-25123c8fd342.png)

その後、
pubspec.yamlファイルに
[FirebaseCore](https://pub.dev/packages/firebase_core)と[CloudFirestore](https://pub.dev/packages/cloud_firestore)のパッケージを追加すれば完了です。（Pub getも忘れずに）

###3-3.WEB画面にデータを表示する
今回、ユーザーがアプリに情報を登録したと仮定して、事前にCloudFirestoreにデータを3つ作っておきます。
![スクリーンショット 2021-12-07 17.23.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/eece552f-2fff-037d-fb5c-3bedb986e9d1.png)


これらを管理画面（WEB）に表示していきます！


今回、ファイルを、Domain, View, ViewModel, Repositoryに分けます。
状態管理には簡易的にriverpodを導入します。（今回のプロジェクトで入れる必要性はないと思いますが慣れているのでいれました）

※注意：2021年12月7日時点ですが、riverpodを導入するとビルドできないという問題がありました。channelをstableではなく、masterにすれば動きました。
https://github.com/rrousselGit/river_pod/issues/889

モデルを管理するDomain層（アプリ側でユーザーが登録した名前とEmailアドレス）

```dart:app_user.dart
import 'package:cloud_firestore/cloud_firestore.dart';
class AppUser {
  AppUser({
    this.name,
    this.email,
  });

  factory AppUser.fromDocumentSnapshot(DocumentSnapshot snap) {
    final Map<String, dynamic> data = snap.data()! as Map<String, dynamic>;
    return AppUser(
      name: data['name'],
      email: data['email'],
    );
  }

  final String? name;
  final String? email;
}
```

Firebaseとのやりとりを行うRepository層

```dart:management_repository.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:sample_management_app/app_user.dart';

class ManagementRepository {
  final _db = FirebaseFirestore.instance;
  final _collectionPath = 'users';

  Future<List<AppUser>> fetchUserList() async {
    try {
      final snapshot = await _db.collection(_collectionPath).get();
      return snapshot.docs.map((e) => AppUser.fromDocumentSnapshot(e)).toList();
    } catch (e) {
      rethrow;
    }
  }
}
```

RepositoryとViewを繋ぐロジックを担当するViewModel

```dart:management_view_model.dart
import 'package:flutter/cupertino.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sample_management_app/management_repository.dart';
import 'app_user.dart';

final userListModel = ChangeNotifierProvider(
      (ref) => ManagementViewModel()..init(),
);

class ManagementViewModel extends ChangeNotifier {
  final ManagementRepository _repository = ManagementRepository();
  List<AppUser> userList = [];

  Future<void> init() async {
    userList = await _repository.fetchUserList();
    notifyListeners();
  }
}
```

WEB画面に描画するView

```dart:management_view.dart
import 'package:flutter/material.dart';
import 'management_view_model.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class ManagementView extends HookConsumerWidget {
  const ManagementView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final vm = ref.watch(userListModel);
    return Scaffold(
      appBar: AppBar(),
      body: ListView.builder(
        itemCount: vm.userList.length,
        itemBuilder: (_, index) {
          final user = vm.userList[index];
          return ListTile(
            title: Text(user.name!),
            trailing: Text(user.email!),
          );
        },
      ),
    );
  }
}

```

main.dartにRiverpodやFirebaseのinitializeの処理をして、ビルドすると、、、
![スクリーンショット 2021-12-07 17.28.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/998564/73c20687-ce73-52ff-d741-52b16352bb75.png)
CloudFirestoreの情報をリストの形で取得することができました！
デザインを整えれば管理画面っぽくなると思います。

##4. 終わりに
Flutter for Webで、管理画面を構築してみました。
Flutter1つで、iOSもAndroidもWebも開発できるのは魅力的ですね。
まだまだ細かいところは実装できない・実装しづらい部分もあるみたいですが、徐々に改善されるのではないかと思っています。

多くの人が使うプロダクトを作りたいなあ。
今年もあと20日ほどですが、良い年末をお過ごしください。