torikatsu.dev

Flutterとかプログラミングとかガジェットとか書きます

【Flutter】flutterfire x riverpod x go_routerで認証ガードをつくるときの落とし穴

はじめに

お久しぶりです。とりかつ(torikatsu923)です。

Flutterで開発をしているとflutterfire x riverpod x go_router の構成をとることがよくあります。 この構成でGoRouterの認証ガードを作ってみたらハマリポイントがあることがわかりました。 今回はその落とし穴と回避方法について紹介します。

FlutterFire x riverpod x go_routerの落とし穴

FlutterFireでは認証状態はonAuthStateChanged()Stream<User?>として取り出すことができます。 ここにriveropdのStreamProviderを組み合わせると、認証状態の変更を監視するのが容易になります。

しかし、この方法には少しだけ問題があります。 例えばルーティングをgo_routerで行なっており、GoRouteredirectに認証ガードを設定したい時があります。

// 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で人気な構成での落とし穴とその回避方法を紹介しました。 この記事が誰かの助けになれば幸いです。