【Flutter】go_router+RiverpodでLogin前とLogin後で遷移先を変える
はじめに
go_routerとRiverpodを使ったアプリで、Login前とLogin後で遷移先を変えたい(Loginされていなければ、Loginしてから遷移したい)時があって、その時につまずいたことがあったので、備忘録として残しておきます。
ログインしているかどうかはStateNotifierProviderで検知する場合とします。
dependencies:
flutter:
sdk: flutter
freezed_annotation: ^2.0.3
hooks_riverpod: ^2.1.1
go_router: ^5.2.2
dev_dependencies:
flutter_gen_runner: ^5.2.0
build_runner: ^2.3.3
freezed: ^2.3.2
シンプルな例
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends ConsumerWidget {
MyApp({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
ref.listen(userProvider.select((s) => s.isLoggedIn), (_, __) {
router.refresh();
});
return MaterialApp.router(
routerConfig: router,
);
}
}
ここではrouterConfigにrouterをセットしています。
final router = ref.watch(routerProvider);
ref.listen(userProvider.select((s) => s.isLoggedIn), (_, __) {
router.refresh();
});
go_routerではrefreshListenable:に変数を渡すと、その変数の変更を検知してrouterを更新することができます。
しかし、refreshListenable:にはListenableしかセットできません。
Listenableを継承しているChangeNotifierProviderならセットできます。
しかし、今回変更を見たいProviderはStateNotifierProviderなので、そのためにref.listen
でisLoggedInに変更があったらrouter.refresh()
してrouterを更新させています。
class MainPage extends ConsumerWidget {
const MainPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final userController = ref.read(userProvider.notifier);
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
userController.logOut();
},
icon: Icon(Icons.logout))
],
title: Text('メインページ'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/sub');
},
child: Text('subページへ')),
),
);
}
}
class SubPage extends ConsumerWidget {
SubPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final userController = ref.read(userProvider.notifier);
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
userController.logOut();
},
icon: Icon(Icons.logout))
],
title: Text('サブページ'),
),
);
}
}
class LoginPage extends ConsumerWidget {
LoginPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final userController = ref.read(userProvider.notifier);
return Scaffold(
appBar: AppBar(
title: Text('ログインページ'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
userController.logIn();
},
child: Text('ログイン')),
),
);
}
}
MainPageではlogOutとSubPageへの遷移、SubPageではlogOut、LoginPageではlogInができます。
logOut,logIn関数を呼び出すと、isLoggedInが変更されるので、main.dart
で設定したとおり、routerが更新されます。
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
part 'user_controller.freezed.dart';
class UserState with _$UserState {
factory UserState({
(false) bool isLoggedIn,
}) = _UserState;
}
final userProvider =
StateNotifierProvider<UserController, UserState>((ref) => UserController());
class UserController extends StateNotifier<UserState> {
UserController() : super(UserState()) {}
void logOut() {
state = state.copyWith(isLoggedIn: false);
}
void logIn() {
state = state.copyWith(isLoggedIn: true);
}
}
routerでredirectするための変更を検知される値isLoggedInをStateNotifierProviderに持たせています。
変数isLoggedInを変更するためのlogOutとlogInという関数をつくりました。
LoginPageでlogIn,SubPageとMainPageでlogOutを呼び出せるようにしてあります。
//省略
$flutter packages pub run build_runner build --delete-conflicting-outputs
をしてください
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../main.dart';
import '../../user_controller.dart';
final navigatorKey = GlobalKey<NavigatorState>();
final routerProvider = Provider((ref) {
final bool isLoggedIn = ref.watch(userProvider.select((s) => s.isLoggedIn));
return GoRouter(
initialLocation: '/',
navigatorKey: navigatorKey,
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const MainPage();
},
),
GoRoute(
path: '/login',
builder: (BuildContext context, GoRouterState state) {
return LoginPage();
},
),
GoRoute(
path: '/sub',
builder: (BuildContext context, GoRouterState state) {
return SubPage();
},
),
],
redirect: (context, state) {
if (!isLoggedIn) {
return state.subloc == '/login' ? null : '/login';
//ログインしてなかったらloginページに遷移させる
}
return null;
},
);
});
GoRouterはProviderで囲んであります。
redirect:では、!isLoggedIn
(ログインしていない場合)でsublocが/login
ならnullを返してそのまま、sublocがそれいがいなら/login
を返すことでLoginPageに遷移させています。
StateNotifierProviderのisLoggedInが更新された際に、自動的に遷移されます。
上記の設定のおかげで、LoginPageでuserController.logIn()
を呼び出すとinitialLocation: '/',に設定したとおり、MainPageにredirectされます
MainPageまたはSubPageでuserController.logOut()
を呼び出すと、isLoggedInがfalseになるので、redirect:に設定したとおりLoginPageにredirectされます
動作確認
initialLocation: '/',と設定してあるので、buildしたら最初にMainPageが表示されるはずですが、ログインしていなかったらLoginPageに遷移させるようにredirectを設定してあるため、MainPageではなくLoginPageが最初に表示されます。
Loginし終わったら、initialLocation: '/',に設定したとおり、MainPageにredirectされます。SubPageにもMainPageにも遷移できます。SubPageまたはMainPageでlogOutすると、LoginPageにredirectされます。
参考
Discussion