はじめに
お久しぶりです。とりかつ(torikatsu923)です。
Flutterで開発をしているとflutterfire x riverpod x go_router の構成をとることがよくあります。 この構成でGoRouterの認証ガードを作ってみたらハマリポイントがあることがわかりました。 今回はその落とし穴と回避方法について紹介します。
FlutterFire x riverpod x go_routerの落とし穴
FlutterFireでは認証状態はonAuthStateChanged()
でStream<User?>
として取り出すことができます。
ここにriveropdのStreamProvider
を組み合わせると、認証状態の変更を監視するのが容易になります。
しかし、この方法には少しだけ問題があります。
例えばルーティングをgo_routerで行なっており、GoRoute
のredirect
に認証ガードを設定したい時があります。
// router provider // ... GoRoute( path: '/home', redirect: (state) { // Stream<User?> to User? final user = ref.read(authProvider.select((v) => v.value); if (user == null) return '/signin'; return null; } ) // ...
この方法はうまくいきそうですが、認証状態の変更を伴う画面遷移で問題が起きます。 例えば以下のようなサインイン処理を考えます。
// sign in screen // emit when user pressed sign in button. Future<void> onSubmit() async { // ... await FirebaseAuth.instance .signInWithEmailAndPassword(email: email, password: password); context.go('/home'); }
これは一見うまくいきそうですがうまくいきません。
FirebaseAuthのauthStateChanged()
は最新の状態が反映されるまでに若干タイムラグがあります。
それゆえ、サインインして/home
へ遷移した際のonAuthStateChanged
の値は未認証のままとなります。
ここでGoRouterのredirect
でuserが取れず認証されていないと判断され、/signin
にリダイレクトされます。
このままではユーザは認証したのに画面遷移されない...となってしまいます。
回避方法
これを回避するためにはauthStateChagne()
の更新を待ってから画面遷移する必要があります。
一般にstreamで次の値を待つには.first
を使用します。しかし、StreamProviderはBroadcastStreamのため、.first
で取得できる値は最新の値ではなく最新のキャッシュです。
そのため、次の値を待つためのextensionを準備します。
extension StreamExt<T> on Stream<T> { Future<T> next() { final _completer = Completer<T>(); final sub = listen(null); sub.onData((e) { sub.cancel(); _completer.complete(e); }); return _completer.future; } }
これで値の更新を待ってから画面遷移する準備ができました。
final user = _ref.read(authStreamProvider.stream).next();
authStateChagned
同じ値が流れてくることがあります。
例えば現在の値がnull
で次にまたnull
が流れてきたり、現在がUser
のときに次にUser
が流れてくるということがあります。
これは値の更新を待って画面遷移したとしても、再度nullが渡ってくることがあると言うことです。
これを回避するため、以下のように値の変更があったときだけ流すようStreamを変換します。
final _prev = StateProvider<User?>((ref) => null); final authStreamProvider = StreamProvider<User?>( (ref) => FirebaseAuth.instance.authStateChanges().transform( StreamTransformer.fromHandlers( handleData: ((data, sink) { if (data != ref.read(_prev)) { sink.add(data); ref.read(_prev.notifier).update((_) => data); } }), ), ), );
やっていることは以下の通りです。
- Streamの直前の値をキャッシュする
- Streamに値が流れてきた時に値が変更されているかチェックする
- 値の変更があればsink.add
する
以上で認証状態の更新を待って正しく画面遷移することができるようになりました。
// onSubmit in sign in screen // ... final user = _ref.read(authStreamProvider.stream).next(); await _ref.read(authenticatorProvider).signin( email: state.email.text, password: state.password.text, ); if (await user == null) { throw AppError.unknown(); } else { _ref.read(routerProvider.notifier).go('/home'); }
おわりに
今回の記事ではFlutterで人気な構成での落とし穴とその回避方法を紹介しました。 この記事が誰かの助けになれば幸いです。