FlutterでFirebaseUIを使うプラグイン(非公式)作成の話

6 min read読了の目安(約5900字

AndroidとiOSとFlutterを書いているkoji-1009です。
最近仕事用の端末をM1 Macbook Proにしました。環境構築が大変。


Flutterでちょっとした規模の個人アプリを作ろうとすると、詰まってしまうのがFirebaseへのログイン機能ではないかなと思います。
メールアドレス認証だけであっても、メールアドレスの正規表現によるチェックが難しかったり、Android/iOS端末と強調してパスワードを管理してくれる機能が欲しくなり、数時間かかることもしばしば。ある機能を実現したいのに、そ2歩手前ぐらいであれこれと悩まされることがよくあります。

AndroidやiOS、WebのSPAであればFirebaseUIという素晴らしいライブラリがFirebaseから提供されているので、これを導入することでなんとかなったりします。カスタマイズ性は低いのですが、Privacy PolicyやTerms Of Serviceのリンクを表示してくれるなど、十分な機能が利用可能です。
思いがけず嬉しいのは、GoogleやMicrofost、GitHubアカウントの連携です。個人アプリでこれができる時代、すごい。

https://firebase.google.com/docs/auth/web/firebaseui
https://firebase.google.com/docs/auth/android/firebaseui
https://firebase.google.com/docs/auth/ios/firebaseui

さて、Flutterでアプリを作る話に戻ります。
FlutterでFirebaseUIは、2021年2月中旬現在公式サポートされていません。

https://github.com/FirebaseExtended/flutterfire/issues/789

Issueを追っていくと要望が上がりつつも後回しのなかの後回しになっているようで、サクッと対応できる状況にありません。
そんなわけで、「各プラットフォームのFireabseUIを呼び出す」ライブラリを作ったので紹介します。

はじめに

  • 本記事は「ライブラリをこんな感じで作ったよ!」という感じの記事です
  • 同じ方法で作られているライブラリ、ちょっと違った方法で作られたライブラリなどがありますが、他との優位/劣位点をあげるための記事ではありません
  • 「お、こんな感じなら作れそうじゃん!」と思ってもらえれば、とっても嬉しいです

※PRやIssueお待ちしてます

https://github.com/koji-1009/flutter_auth_ui

Android/iOS対応

MethodChannelを利用して、Android/iOSのFirebaseUIを呼び出すシンプルな構成をとっています。
DartのライブラリからKotlin/Swiftの世界にリクエストを投げつけ、その後はそれぞれのOS用のSDKを呼び出す実装です。

https://pub.dev/packages/flutter_auth_ui

特徴としては「FirebaseUIの呼び出しとSignin/Login処理のみ」を担当するようにしています、実質的に呼び出し処理のみにフォーカスしたライブラリです。
そのため、FirebaseUIにあるログアウト処理などには対応しておらず、そういったアカウントの操作はfirebase_authを通じてDartの世界で対応してもらうことを想定しています。

実装

Firebaseの各ライブラリがSingletonパターンで実装されていることを利用しています。
Singletonであるため、どの箇所から呼び出しをしても同一のインスタンスへのアクセスが出来ますし、同一のインスタンスに状態を反映することができます。

これをライブラリで利用すると、Kotlinで呼び出したFirebaseUIのログイン状態が、Dartで呼び出したfirebase_authに反映されている、という状況を作ることができます。
このため、ライブラリはKotlin/Swiftの世界でSignin/Login処理を呼び出すだけで、そのフローが成功したかどうかを厳密にハンドリングする必要がありません。Signin/Login処理が完了した後に、Kotlin/SwiftではなくDartでuser情報をfirebase_auth経由で取得できるのであれば、ログイン済みのユーザーとして扱うことができます。
結果としてflutter_auth_uiライブラリが担当する範囲を非常に狭く保つことができ、個人でメンテナンスしてもそんなに辛くならなくしています。

特徴

  • Android/iOSのプラットフォームに馴染んだUIを提供することができています
    • iOSではモーダルによるログインステップの表示なり、iOSらしい体験が提供できていると思います
    • SIWAもネイティブのAPIを呼び出している(様子)
    • Androidは次のバージョンで最新のMaterialライブラリを導入するようなので、乞うご期待状態
  • Dartが読めれば、何をしているのかが大体わかるようになっています
    • Flutterアプリエンジニアが読み下せるライブラリであることは大切なのではないかなと思っています
    • PRは常に歓迎😃
  • GitHub/Microsoft/Yahooログインが選択可能になっています
    • FirebaseUIがOAuthProviderに対応してくれれば、セットアップだけで利用できるようになります
    • 各プラットフォームのロゴを用意しなくて大丈夫です😆

Web対応

Android/iOSアプリ対応をしてから1年ほどかかってしまったのですが、flutter webの対応も動くところまで進んだので公開しました。

https://pub.dev/packages/flutter_auth_ui_web

Webで利用するためには index.html にfirebase-app.jsと共にfirebase-ui-auth.jsライブラリを追加する必要があります。利用する際にはexampleを参考にしてみてください。
話がそれるのですが、flutter webにモバイルアプリエンジニアが取り組むにあたり、このライブラリのインポートが一番戸惑うのではないかという気がしています。いい感じのライブラリ管理ツールが出て欲しいのですが、動的にJSのライブラリをインポートするのはよくないだろうな〜という気がするのでちょっと諦め気味です。

実装

HTMLの操作に不慣れなこともあり、手探り状態です。
このため、なんらかの変更などが今後入ることが多々あり得ます。


前提となる話として、firebaseui-webをflutter webの画面の中から直接呼び出すことは、下記のStackOverflowの回答のように困難です。

https://stackoverflow.com/questions/62284511/how-to-add-firebase-pre-built-auth-ui-for-flutter-web/62284779#62284779

While it may technically be possible to integrate FirebaseUI auth sign ins into Flutter for web, I doubt it'll be a smooth integration.

FirebaseUI comes with a sign-in UI for use in traditional DOM based web apps. But Flutter for Web is not a traditional web UI, as it actually renders its output in Web GL instead of traditional DOM elements.


この問題を回避するため、flutter_auth_ui_webではDOMの要素を処理の開始/終了時に操作しています。
※HTMLの操作的により良い方法があったら教えてください!

  • firebaseui-webの処理を開始するタイミング
    • firebaseui-webの要素をDOMに追加
    • flutter webの要素を非表示
  • 処理を完了するタイミング
    • firebaseui-webの要素をDOMから削除
    • flutter webの要素を表示

コードとしてはflutter_auth_ui_web.dart#66のあたりです。

// add div element instead of 'firebaseui-auth-container' div
final containerDiv = html.Element.div();
html.window.document.documentElement.append(containerDiv);

// get flutter web's main view
final fltGlassPane = html.window.document
    .getElementsByTagName('flt-glass-pane')[0] as html.Element;

// watch back event, if not, we cannot support back key
html.window.addEventListener('popstate', (event) {
  containerDiv.remove();
  fltGlassPane.style.visibility = 'visible';
});

final completer = Completer();
final callbacks = Callbacks(
  signInSuccessWithAuthResult: allowInterop((authResult, redirectUrl) {
    completer.complete(auth().currentUser != null);
    html.window.history.back();

    return false;
  }),
  signInFailure: allowInterop((error) {
    completer.completeError(error);
    html.window.history.back();
  }),
  uiShown: allowInterop(() {
    fltGlassPane.style.visibility = 'hidden';
  }),
);

Android/iOS対応のように、Dartからアクセスしたfirebase-appとfirebaseui-webからアクセスしたfirebase-appのインスタンスが同一(っぽい)ことを利用しています。
テストアプリでみている限り動作しているっぽいので、個人アプリのflutter web対応と合わせて実動作のチェックをこれからしていく予定です。あと、DartでJSから読めるオブジェクトの生成をサボっているところがあるので、もうちょっと拡張します。

特徴

  • flutter_auth_uiとセットで使うとflutter web対応がスムーズ
  • firebaseui-webの呼び出しについては変更していないため、firebaseui-webの更新に対応しやすい

おわりに

非公式のライブラリを作って紹介しておいてなんですが、本当に良いのはFirebase公式のサポートを待つことではないかな、と考えています。

Flutterのマルチプラットフォームであれば「Flutterのwidgetで構成された、FirebaseUIライブラリ」を利用するのが一番です。というのも、Flutterのwidgetで構成されていないと、マルチプラットフォームでUIの差異が意図しないところで発生しやすくなるなと直感的に思います。
ただ、個人開発でこの方法を取るのは非常にコストが高く、リスキーです。いくらFirebaseAuthで実装ができるので簡略になるとはいえ、セキュリティホールがないように個人で実装し切るのは厳しいものがあり、かつFirebaseAuthのアップデートに追従し続けるのは困難です。

flutter_auth_uiは利用する方が使いやすいこと以上に、作者である自分の開発がしやすいことを目標にしています。というのも、Android/iOS向けに開発した時には個人で「何か作りたいな〜」ぐらいの気持ちでFlutterに向き合っていた時でしたし、今も「次に作る個人アプリ、flutter webの対応してみたいな〜」ぐらいの気持ちで向き合っているからです。

いろんな人が、それぞれの得意分野でFlutterを盛り上げられればいいなと思っています!