はじめに
宣言的に実装することで、コードの可読性とナビゲーションロジックの直感性を向上させることができます。
本記事では、auto_routeを使用した認証状態に基づくナビゲーションの宣言的なハンドリングについて紹介します。
1つのパターンとして参考にしていただければ幸いです。
留意点
ここでは、auto_route自体の詳しい説明や使い方は省きます。
auto_routeについて知りたい方はReadmeや下記の記事がわかりやすいので、参考にしてみてください。
では、やっていきます。
認証状態を定義する
/// ユーザーの認証状態を表すenum
enum AuthenticationState {
/// 認証確認中ステータス。Stateの初期値。
checking,
/// 未認証ステータス
unAuthenticated,
/// 認証済ステータス
authenticated;
factory AuthenticationState.from(String? uid) {
if (uid == null) {
return AuthenticationState.unAuthenticated;
} else {
return AuthenticationState.authenticated;
}
}
}
Point
ポイントは、認証状態を単なるフラグではなく、enumを使用して表現していることです。この方法には以下のような利点があります。
- 可読性の向上:
- コードを見た開発者がアプリの認証状態の変化を直感的に把握しやすくなります。これにより、ナビゲーションロジックの理解や保守がより簡単になります。
- 拡張性:
- Enumの利用により、新しい認証状態の追加が容易になります。
- 各enum値に特定のプロパティや振る舞いを割り当てることも可能で、それぞれの認証状態に固有のロジックを組み込むことができます。
[発展]
特定の状態にだけプロパティを持たせたい場合は、enum
ではなくsealed class
の使用も検討してみてください。これにより、更に柔軟な構造を定義できます。
サンプルコード
sealed class AuthState {}
class Checking extends AuthState {}
class UnAuthenticated extends AuthState {}
@freezed
class Authenticated extends AuthState with _$Authenticated {
const factory Authenticated({
required 認証時のみ使えるプロパティ hoge,
}) = _Authenticated;
}
認証状態を監視するためのStateNotifierを作成する
/// アプリケーション全体で認証状態を管理するためのProvider
final authStateProvider =
StateNotifierProvider<AuthenticationStateNotifier, AuthenticationState>(
(ref) => AuthenticationStateNotifier());
/// 認証状態を管理する StateNotifier
class AuthenticationStateNotifier extends StateNotifier<AuthenticationState> {
AuthenticationStateNotifier() : super(AuthenticationState.checking);
void updateState(AuthenticationState state) async {
this.state = state;
}
}
認証状態を確認するためのクラスを作成する
final authProvider = Provider((ref) => AuthenticationController(ref: ref));
class AuthenticationController {
AuthenticationController({
required Ref ref,
}) : _ref = ref;
final Ref _ref;
/// 認証状態を確認し、更新するメソッド
Future<void> update() async {
// uidを Remote or Localから取得する
final uid = await _ref.read(authRepositoryProvider).getCurrentUserUid();
// uidをもとにAuthenticationStateを判別する
final authState = AuthenticationState.from(uid);
// State更新
_ref.read(authStateProvider.notifier).updateState(authState);
}
}
App内でのハンドリング
void main() {
runApp(
const ProviderScope(
overrides: [],
child: App(),
),
);
}
class App extends ConsumerStatefulWidget {
const App({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _AppState();
}
class _AppState extends ConsumerState<App> {
@override
void initState() {
super.initState();
// 画面表示後に実行されるコールバック
WidgetsBinding.instance.addPostFrameCallback((_) {
// 認証状態の初期化および更新を行う
ref.read(authProvider).update();
});
}
@override
Widget build(BuildContext context) {
final router = ref.watch(appRouterProvider);
return MaterialApp.router(
routerConfig: router.config(),
builder: (context, child) {
// 認証状態に応じて適切なウィジェットを表示
// もしupdate()に遅延処理がない場合は、ケース分けせずchildを返すでもOK
switch (ref.watch(authStateProvider)) {
case AuthenticationState.checking:
// update()に時間がかかるとその間、画面が真っ白になってしまうのでインジケータを表示する
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
default:
// それ以外の状態のハンドリングは、routerに任せる
return child!;
}
},
);
}
}
routerクラス内で認証状態に応じて表示する画面をハンドリングする
final appRouterProvider = Provider((ref) {
return AppRouter(ref);
});
/// アプリのルート構成とナビゲーションガードを定義するクラス
@AutoRouterConfig(replaceInRouteName: 'Page,Route')
class AppRouter extends _$AppRouter implements AutoRouteGuard {
AppRouter(this.ref);
final Ref ref;
@override
List<AutoRoute> get routes => [
// 未認証時に表示するルート
AutoRoute(
path: '/auth',
page: AuthenticationRootRoute.page,
children: [
AutoRoute(
initial: true,
path: 'login',
page: LoginRoute.page,
),
AutoRoute(
path: 'signUp',
page: SignUpRoute.page,
),
],
),
// 認証済み時に表示するルート
AutoRoute(
path: '/',
page: RootRoute.page,
children: [
AutoRoute(
path: 'home',
page: HomeRoute.page,
),
],
),
];
/// ルートガードのロジックを定義するメソッド
/// 認証状態に基づいて、ユーザーがアクセスしようとしているルートを許可または制限する
/// また、ユーザーがアクセスしようとしているページが適切かどうかを判断し、必要に応じてリダイレクトする
@override
void onNavigation(NavigationResolver resolver, StackRouter router) {
// ユーザーが遷移しようと試みているルートの名前を取得する
final routeName = resolver.route.name;
// 現在の認証状態とルート名を用いてルートをハンドリングする
switch (ref.watch(authStateProvider)) {
// 未認証の場合
case AuthenticationState.unAuthenticated:
if (_isUnauthenticatedRoute(routeName)) {
// 現在のルートが未認証ユーザー向けのものなので、そのままアクセスを許可する
resolver.next();
return;
}
// 未認証ユーザーが認証が必要なページにアクセスしようとしているので
// 認証画面にリダイレクトする
router.replace(const AuthenticationRootRoute());
// 認証済みの場合
case AuthenticationState.authenticated:
if (_isUnauthenticatedRoute(routeName)) {
// 認証済みユーザーが未認証用のページにアクセスしようとしているので
// ホーム画面にリダイレクトする
router.replace(const RootRoute());
return;
}
// その他のケースでは、そのままアクセスを許可する
resolver.next();
// 認証状態確認中の場合
case AuthenticationState.checking:
// Appクラスで認証状態が確定するまで待機するため、特にアクションを取る必要はない
// (通常はここには到達しない)
}
}
/// 現在のルートが未認証ユーザー向けのものであるかをチェックするメソッド
bool _isUnauthenticatedRoute(String routeName) {
return [
AuthenticationRootRoute.name,
LoginRoute.name,
SignUpRoute.name,
].contains(routeName);
}
}
要件を満たすうえでは、Stateのみのハンドリングで十分かもしれませんが、
セキュリティ・ロジック漏れのリスクも考慮してrouterName
で二重チェックをしています。
補足
より厳密なアプローチとして、AuthenticationState.checking
状態の際に表示する専用の画面(スプラッシュ画面のようなもの)を導入することを検討してみてください。
アプリ内での認証状態に関するロジックを、App内でswitch文を使わずに、ルートガード内に完全に集約することで、更にコードの見通しを良くし、ロジックの一元化を図ることができます。
以上
認証状態に基づくナビゲーションを宣言的かつ効果的にハンドリングすることができました。
auto_routeはルートガードが簡単に作成できるので便利ですね。
今後もNavigation2.0
, go_router
, auto_route
それぞれのメリデメをおさえたうえで技術選択できるように動向を追っていきたいと思います!