flutter web で firebase を使ったアプリを作成した知見

flutter 界隈のみなさまこんにちは、師走はいかがお過ごしでしょうか。
この頃は急に寒くなり部屋からも布団からも出れない私はワイルドエリアでポケモンとキャンプをし愛犬ラクライを愛でる幸せなアウトドア生活を送っております。バッジ集めるのとかなんかどうでもよくなってきており、ただ野外でカレーを作る日々。ラクライはいいやつなので毎日だいたい同じカレーを食べさせても満面の笑みで頬張ります。まさかお母さんも送り出した娘がワイルドエリアで野生化し野宿と放浪を繰り返してるとは思っていないでしょう。ライバルのホップもいいやつなので今もきっとどこかの道端で主人公を待ちつづけているのでしょう。幻のポケモン？そんなイベントあったっけ？ポケモン図鑑…？ずいぶん昔に薪の足しにしたような？

…さて、本題に入ります。

今回、初 flutter, 初 flutter web を触ってみたので、その知見をまとめたいと思います。

## 概要

今回作ったアプリの大まかな構成は以下の通りです:

- Flutter (devチャンネル)
- Web/Android/iOS 対応
- BLoC with [RxDart](https://pub.dev/packages/rxdart)
    - [1画面、1BLoC](https://medium.com/flutterpub/effective-bloc-pattern-45c36d76d5fe#850f)
    - 入出力は Sink と ValueObservable
    - [provider](https://pub.dev/packages/provider) の MultiProvider, ProxyProvider 使用
- [firebase](https://pub.dev/packages/firebase)
  - firestore
  - auth (SMS認証)
  - hosting
- CI/CD: GithubActions [subosito/flutter-action](https://github.com/subosito/flutter-action), [w9jds/firebase-action](https://github.com/w9jds/firebase-action)

次の章から、それぞれに関する知見を、深ぼっていきたいと思います。

## デプロイ周り

今回は [firebase hosting](https://firebase.google.com/docs/hosting/?hl=ja) へ Github Actions を用いてアプリをデプロイしました。
[subosito/flutter-action](https://github.com/subosito/flutter-action) が便利で、PRごとに `flutter analyze` で静的解析にかけたり、master に入ったコードを各環境用に `flutter build` を実行したりできます。
`flutter build` の結果を [w9jds/firebase-action](https://github.com/w9jds/firebase-action) を使って `firebase hosting` にあげれば、誰でも簡単無料で flutter web 製のサイトを公開することができます。

## ルーティング

今回は [fluro](https://github.com/theyakka/fluro) を使ってルーティングを行いました。
パスパラメータ、クエリパラメータに対応しているため、ブラウザの `location` とも相性が良いです。
そのままでは web 用のビルドに含められないため、フォークして使用しました。

(web ビルドで依存が認められていない、 `dart:io` を取り除いたバージョン)
https://github.com/theyakka/fluro/pull/132

```dart
import 'package:fluro/fluro.dart' as f;

// メッセージ詳細
router.define(
'/rooms/:room_id//messages/:message_id',
handler: f.Handler(
  handlerFunc: (BuildContext context, Map<String, dynamic> params) =>
        LoginRequiredContainer(
      child: MessageDetailScreen(
        roomID: params['room_id'][0],
        messageID: params['message_id'][0],
      ),
    ),
  ),
  transitionType: f.TransitionType.material,
);
```
こんな形でルーティングの定義ができます。

## ホットリロード

flutter の最大の旨味じゃないかなと感じるホットリロードですが、Web は現時点でホットリロードが効かないです。(ホットリスタートします)
開発は基本 Android か iOS で行った方がストレスが少ないです。

```
Does hot reload work with a web app?
No.
```

https://flutter.dev/docs/development/platform-integration/web#does-hot-reload-work-with-a-web-app

## firebase

flutter web の firebase 対応は罠が多いです。
まず、flutter には iOS/Android/Web でユニバーサルな firebase ライブラリが現時点で存在しません。

flutter で firebase を利用する方法を一つ一つ見ていきましょう。

### [FlutterFire](https://github.com/FirebaseExtended/flutterfire)

[FlutterFire](https://github.com/FirebaseExtended/flutterfire) という iOS/Android 対応の公式プラグインがあります。これが web 対応してくれれば万々歳なのですが、現時点で対応はしていません。

**(ただ、執筆時点ですごい勢いで web 関連のコードが追加されていっているため、そのうち firestore なども web 対応されそうです)**
https://github.com/FirebaseExtended/flutterfire/pull/1555

ひとまず、様子見が必要です。代替策を考えねばなりません。

### [firebase](https://pub.dev/packages/firebase)

Web 向けの firebase ラッパーです。`JS` 版の firebase を `interop` を駆使して Dart でラップしたものです。
flutter web で firebase を扱うならこれが一番ですが、如何せん Web 用なので Android/iOS ではビルドが通りません。辛い。

### [crossfire](https://pub.dev/packages/crossfire)

上の二つのライブラリから iOS/Android, Web 間で共通のAPIだけ抜き出し、再定義した [crossfire](https://pub.dev/packages/crossfire) というライブラリがあります。
これを使えば Android/iOS/Web みんなで共通のコードベースを共有できる…と思いきや、対応APIがめちゃ少ないです。

auth に限ってはカスタムトークンのみの対応。(自前で認証作る人向けですね)

_(`AndroidX` に対応したのが `crossfire_flutter 3.0.0` からなのですが、依存が `crossfire 2.0.0` なので、誤って `crossfire 2.1.0` 入れると痛い目見ます)_
 
というわけで、コードベース共有は諦めました。適切に抽象化して各プラットフォームで実装するしかないです。

### プロジェクトを分ける

[firebase](https://pub.dev/packages/firebase) などの Web 依存(`package:js` や `package:html` など)が `pubspec.yaml` に含まれていると、iOS/Android ビルドがそもそも通りません。
逆に `dart:io` などの Web からの依存が禁止されている依存を持っている場合は、Web のビルドが通りません。

そこで、Web, Native でプロジェクトを分けます。

#### ディレクトリ構成
以下のように、`web`、`mobile` でそれぞれ `flutter create` します。
共通で使用する Widget, Screen や BLoC などは `common` というプロジェクト配下に置いて行きます。
(`common` は `flutter create --template=package` で作成します)

```sh
.
├── analysis_options.yaml
├── common
│   ├── lib
│   │   ├── blocs
│   │   │   ├── auth_bloc.dart
│   │   │   └── hoge_hoge_screen_bloc.dart
│   │   ├── entities
│   │   ├── repositories
│   │   │   ├── auth.dart
│   │   │   ├── user.dart
│   │   │   └── repositories.dart
│   │   ├── run.dart
│   │   └── views
│   │       ├── boundaries
│   │       ├── screens
│   │       │   └── hoge_huga_screen.dart
│   │       └── widgets
│   ├── pubspec.lock
│   └── pubspec.yaml
├── native
│   ├── android
│   ├── ios
│   ├── lib
│   │   └── main.dart
│   │   └── repositories
│   │       ├── auth.dart
│   │       ├── user.dart
│   │       └── repositories.dart
│   ├── pubspec.lock
│   └── pubspec.yaml
└── web
    ├── lib
    │   ├── main.dart
    │   └── repositories
    │       ├── auth.dart
    │       ├── user.dart
    │       └── repositories.dart
    ├── pubspec.lock
    ├── pubspec.yaml
    └── web
        └── index.html
```

そして、`repository` を abstract class で定義し、各プラットフォーム用のプロジェクトで実装することで、 `pubspec.yaml` の競合問題を解決します。

```common/lib/repositories/auth.dart

import 'dart:async';

class Credential {
  final String userID;
  Credential(this.userID);
}

abstract class AuthRepository {
  Future<Credential> fetch();
}
```

実装側

```web/lib/repositories/auth.dart
import 'dart:async';
import 'package:firebase/firebase.dart' as fb;
import 'package:common/repositories/AuthRepository.dart';

class AuthRepositoryImpl implements AuthRepository {
  AuthRepositoryImpl(this.auth) : super() {
    auth.onAuthStateChanged.listen((user) {
      if (user == null) {
        return;
      }
      _currentUser.complete(Credential(user.uid));
    });
  }

  final fb.Auth auth;
  final Completer<Credential> _currentUser = Completer<Credential>();

  Future<Credential> fetch() => _currentUser.future;
}
```

そして、各プラットフォームの `main.dart` で適切に DI を行えば、晴れて iOS/Android/Web 対応の firebase を使ったアプリを作成できました。 


## 細かな気づき

その他、細かな flutter web の気づいた点を書き記しておきたいと思います。

#### `role="button" aria-label="Enable accessibility"` の見えないボタンが存在し、押下すると `Semantic` 系のノードが `DOM` に追加されアクセシビリティが担保される

ちなみに一度UIをマウスで操作するとボタンが消えるっぽい？

<img width="427" alt="スクリーンショット 2019-12-07 1.34.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43622/259a3b4e-914b-790a-0012-a27f6976c292.png">


#### VS Codeが便利

普段は vim を使っていますが、`coc-flutter` がバギーなので flutter 開発するなら VS Code が安定そう

## 以上
ここ数ヶ月の flutter web に関する知見でした。私はワイルドエリアに帰ろうと思います。ごきげんよう。