flutter 界隈のみなさまこんにちは、師走はいかがお過ごしでしょうか。
この頃は急に寒くなり部屋からも布団からも出れない私はワイルドエリアでポケモンとキャンプをし愛犬ラクライを愛でる幸せなアウトドア生活を送っております。バッジ集めるのとかなんかどうでもよくなってきており、ただ野外でカレーを作る日々。ラクライはいいやつなので毎日だいたい同じカレーを食べさせても満面の笑みで頬張ります。まさかお母さんも送り出した娘がワイルドエリアで野生化し野宿と放浪を繰り返してるとは思っていないでしょう。ライバルのホップもいいやつなので今もきっとどこかの道端で主人公を待ちつづけているのでしょう。幻のポケモン?そんなイベントあったっけ?ポケモン図鑑…?ずいぶん昔に薪の足しにしたような?
…さて、本題に入ります。
今回、初 flutter, 初 flutter web を触ってみたので、その知見をまとめたいと思います。
概要
今回作ったアプリの大まかな構成は以下の通りです:
- Flutter (devチャンネル)
- Web/Android/iOS 対応
- BLoC with RxDart
-
firebase
- firestore
- auth (SMS認証)
- hosting
- CI/CD: GithubActions subosito/flutter-action, w9jds/firebase-action
次の章から、それぞれに関する知見を、深ぼっていきたいと思います。
デプロイ周り
今回は firebase hosting へ Github Actions を用いてアプリをデプロイしました。
subosito/flutter-action が便利で、PRごとに flutter analyze
で静的解析にかけたり、master に入ったコードを各環境用に flutter build
を実行したりできます。
flutter build
の結果を w9jds/firebase-action を使って firebase hosting
にあげれば、誰でも簡単無料で flutter web 製のサイトを公開することができます。
ルーティング
今回は fluro を使ってルーティングを行いました。
パスパラメータ、クエリパラメータに対応しているため、ブラウザの location
とも相性が良いです。
そのままでは web 用のビルドに含められないため、フォークして使用しました。
(web ビルドで依存が認められていない、 dart:io
を取り除いたバージョン)
https://github.com/theyakka/fluro/pull/132
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.
firebase
flutter web の firebase 対応は罠が多いです。
まず、flutter には iOS/Android/Web でユニバーサルな firebase ライブラリが現時点で存在しません。
flutter で firebase を利用する方法を一つ一つ見ていきましょう。
FlutterFire
FlutterFire という iOS/Android 対応の公式プラグインがあります。これが web 対応してくれれば万々歳なのですが、現時点で対応はしていません。
(ただ、執筆時点ですごい勢いで web 関連のコードが追加されていっているため、そのうち firestore なども web 対応されそうです)
https://github.com/FirebaseExtended/flutterfire/pull/1555
ひとまず、様子見が必要です。代替策を考えねばなりません。
firebase
Web 向けの firebase ラッパーです。JS
版の firebase を interop
を駆使して Dart でラップしたものです。
flutter web で firebase を扱うならこれが一番ですが、如何せん Web 用なので Android/iOS ではビルドが通りません。辛い。
crossfire
上の二つのライブラリから iOS/Android, Web 間で共通のAPIだけ抜き出し、再定義した crossfire というライブラリがあります。
これを使えば Android/iOS/Web みんなで共通のコードベースを共有できる…と思いきや、対応APIがめちゃ少ないです。
auth に限ってはカスタムトークンのみの対応。(自前で認証作る人向けですね)
(AndroidX
に対応したのが crossfire_flutter 3.0.0
からなのですが、依存が crossfire 2.0.0
なので、誤って crossfire 2.1.0
入れると痛い目見ます)
というわけで、コードベース共有は諦めました。適切に抽象化して各プラットフォームで実装するしかないです。
プロジェクトを分ける
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
で作成します)
.
├── 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
の競合問題を解決します。
import 'dart:async';
class Credential {
final String userID;
Credential(this.userID);
}
abstract class AuthRepository {
Future<Credential> fetch();
}
実装側
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をマウスで操作するとボタンが消えるっぽい?
VS Codeが便利
普段は vim を使っていますが、coc-flutter
がバギーなので flutter 開発するなら VS Code が安定そう
以上
ここ数ヶ月の flutter web に関する知見でした。私はワイルドエリアに帰ろうと思います。ごきげんよう。