Closed8
Firebase Auth + Flutter Web + go_router + riverpod で認証周りの画面遷移やってみる
❯ 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
やりたいこと
- Navigator 2.0対応すること
- Webアプリで動作すること
- 未ログインの場合はログインページに自動的に遷移すること
- Hot restartやブラウザリロードしても認証情報が取得できて適切な画面に遷移すること
- 直リンクでアクセスしたが未ログインの場合、ログインページにリダイレクトしてログイン成功したら直リンクしようとしたページにリダイレクトすること
go_routerのドキュメント
go_router | Flutter Package
-
MaterialApp.router
にGoRouter
を渡す。 -
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,
);
デフォルトだとURLパスに#
入るが、それを消す。
runApp
より前に設定する
GoRouter.setUrlPathStrategy(UrlPathStrategy.path);
あるいはGoRouter
のコンストラクタで指定する。
GoRouter(
...
urlPathStrategy: UrlPathStrategy.path,
...
ただし、今回は後述のようにGoRouter
をProvider
で包む場合はホットリスタートでエラーになるので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."
-
FirebaseAuth
のauthStateChanges
のUser
でサインイン状態を判定する。
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;
}
-
GoRouter
をProvider
で包んで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;
},
),
);
-
refreshListenableProvider
でListenable
をまとめ、ref.listen
でルーティングを更新する。 -
Listenable.merge
無しで直接ValueNotifier
でもいいが、複数のListenable
でルーティング更新したい場合に便利だと思う。 -
GoRouter
のコンストラクタのrefreshListenable
を利用すると、ref.watch
によってGoRouter
のインスタンスが再生成されてlocation
が初期化され、from
が消えてしまうため利用しない。 -
routeInformationParser
とrouterDelegate
をref.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
を使えないところが微妙。
そもそもGoRouter
をProvider
で包むのがあまり良くないのか?でもisSignedIn
欲しいし...
サインイン状態だけProvider
以外の方法で管理するのもどうなんだろうか
追加してくれるって
v2.0.0で早速実装してくれた
サンプルとして https://kingu.dev で挙動確認できるようにした
GoRouter周りはこれまでスクラップに記載した内容より複雑になっているが基本的には同じ
少なくとも https://github.com/KoheiKanagu/KoheiKanagu.github.io/commit/9057d92763c6fadb50fe2ec32c5af4da99378f99 の時点ではサポートしている
Firebase Hostingを使っているが、rewrites
の設定をしないとディープリンクで404になった時、Firebase Hostingの404ページが表示されてしまった。
他のホスティングサービスでも同じような設定はしないとダメかも?
このスクラップは2021/10/09にクローズされました