Closed8

Firebase Auth + Flutter Web + go_router + riverpod で認証周りの画面遷移やってみる

kingukingu
❯ flutter --version
Flutter 2.5.2 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 3595343e20 (14 hours ago)2021-09-30 12:58:18 -0700
Engine • revision 6ac856380f
Tools • Dart 2.14.3
pubspec.yaml
  firebase_auth: 3.1.2
  go_router: 1.1.3
  hooks_riverpod: 1.0.0-dev.7
kingukingu

やりたいこと

  • Navigator 2.0対応すること
  • Webアプリで動作すること
  • 未ログインの場合はログインページに自動的に遷移すること
  • Hot restartやブラウザリロードしても認証情報が取得できて適切な画面に遷移すること
  • 直リンクでアクセスしたが未ログインの場合、ログインページにリダイレクトしてログイン成功したら直リンクしようとしたページにリダイレクトすること
kingukingu
  • MaterialApp.routerGoRouterを渡す。
  • ErrorPageは遷移先のページが見つからなかった時に遷移。要は404
  • observersもサポートしている。
final myRouter = GoRouter(
  debugLogDiagnostics: kDebugMode,
  observers: [
    AppLifecycleObserver(),
    FirebaseAnalyticsObserver(analytics: FirebaseAnalytics()),
  ],
  routes: [
    GoRoute(
      path: '/',
      pageBuilder: (context, state) => MaterialPage<void>(
        key: state.pageKey,
        child: Container(
          color: Colors.red,
        ),
      ),
    ),
  ],
  errorPageBuilder: (context, state) => const MaterialPage<void>(
    child: ErrorPage(),
  ),
);

...
    return MaterialApp.router(
      routeInformationParser: myRouter.routeInformationParser,
      routerDelegate: myRouter.routerDelegate,
    );
kingukingu

デフォルトだとURLパスに#入るが、それを消す。
runAppより前に設定する

GoRouter.setUrlPathStrategy(UrlPathStrategy.path);

あるいはGoRouterのコンストラクタで指定する。

  GoRouter(
...
    urlPathStrategy: UrlPathStrategy.path,
...

ただし、今回は後述のようにGoRouterProviderで包む場合はホットリスタートでエラーになるのでGoRouter.setUrlPathStrategyが無難。

════════ Exception caught by widgets library ═══════════════════════════════════
The following ProviderException was thrown building MyApp(dirty, dependencies: [UncontrolledProviderScope], state: _ConsumerState#80439):
An exception was thrown while building Provider<GoRouter>#023ad.

Thrown exception:
Assertion failed: org-dartlang-sdk:///flutter_web_sdk/lib/_engine/engine/window.dart:25:10
!_isUrlStrategySet
"Cannot set URL strategy more than once."
kingukingu
  • FirebaseAuthauthStateChangesUserでサインイン状態を判定する。
final signProvider = StateNotifierProvider<SignProvider, SignProviderState>(
  (ref) => SignProvider(
    ref.read,
    FirebaseAuth.instance,
  ),
);

class SignProvider extends StateNotifier<SignProviderState> {
  SignProvider(
    this._read,
    this._auth,
  ) : super(SignProviderState()) {
    if (kIsWeb) {
      _auth.setPersistence(Persistence.LOCAL);
    }

    _auth.authStateChanges().listen(
      (event) {
        state = state.copyWith(
          firebaseUser: AsyncValue.data(event),
        );
      },
    );
  }

  final Reader _read;
  final FirebaseAuth _auth;

  Future<void> signIn(String id, String password) =>
      _auth.signInWithEmailAndPassword(
        email: id,
        password: password,
      );

  Future<void> signOut() => _auth.signOut();
}


class SignProviderState with _$SignProviderState {
  factory SignProviderState({
    (AsyncValue<User?>.loading()) AsyncValue<User?> firebaseUser,
  }) = _SignProviderState;

  SignProviderState._();

  late final bool isSignedIn = firebaseUser.data?.value != null;
}
  • GoRouterProviderで包んでsignProviderを利用できるようにする。
  • redirectでは、fromにサインイン前にアクセスしたlocationを保持してサインインページにリダイレクト。
  • サインイン完了した時、fromがあればfromにリダイレクト。
  • なぜUri.parse(state.location).queryParameters?となるがstate.paramsは常に空?なので使えない(https://github.com/csells/go_router/issues/59 仕様なのかバグなのかよく分からないので聞いてる)
final myRouter = Provider<GoRouter>(
  (ref) => GoRouter(
    ...
    redirect: (state) {
      final isSignedIn = ref.read(signProvider).isSignedIn;
      final goingToSignIn = state.subloc == '/signin';

      final params = Uri.parse(state.location).queryParameters;
      final from = params['from'] ?? '';

      if (!isSignedIn && !goingToSignIn) {
        return '/signin?from=${state.location}';
      }

      if (isSignedIn && goingToSignIn) {
        return from.isNotEmpty && from != '/' ? from : '/';
      }

      return null;
    },
  ),
);
  • refreshListenableProviderListenableをまとめ、ref.listenでルーティングを更新する。
  • Listenable.merge無しで直接ValueNotifierでもいいが、複数のListenableでルーティング更新したい場合に便利だと思う。
  • GoRouterのコンストラクタのrefreshListenableを利用すると、ref.watchによってGoRouterのインスタンスが再生成されてlocationが初期化され、fromが消えてしまうため利用しない。
  • routeInformationParserrouterDelegateref.watch(myRouter)に変更する。
final refreshListenableProvider = Provider(
  (ref) => Listenable.merge(
    [
      ValueNotifier(ref.watch(signProvider).isSignedIn),
    ],
  ),
);
  
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen(
      refreshListenableProvider,
      (_) => ref.read(myRouter).refresh(),
    );

    return MaterialApp.router(
      routeInformationParser: ref.watch(myRouter).routeInformationParser,
      routerDelegate: ref.watch(myRouter).routerDelegate,
    );

あんまり最適解な感じはしない。
特にGoRouterコンストラクタのrefreshListenableを使えないところが微妙。
そもそもGoRouterProviderで包むのがあまり良くないのか?でもisSignedIn欲しいし...
サインイン状態だけProvider以外の方法で管理するのもどうなんだろうか

kingukingu

サンプルとして https://kingu.dev で挙動確認できるようにした
GoRouter周りはこれまでスクラップに記載した内容より複雑になっているが基本的には同じ

少なくとも https://github.com/KoheiKanagu/KoheiKanagu.github.io/commit/9057d92763c6fadb50fe2ec32c5af4da99378f99 の時点ではサポートしている

Firebase Hostingを使っているが、rewritesの設定をしないとディープリンクで404になった時、Firebase Hostingの404ページが表示されてしまった。
他のホスティングサービスでも同じような設定はしないとダメかも?

このスクラップは2021/10/09にクローズされました