🐄

【Flutter】go_router+RiverpodでLogin前とLogin後で遷移先を変える

2023/05/05に公開

はじめに

go_routerとRiverpodを使ったアプリで、Login前とLogin後で遷移先を変えたい(Loginされていなければ、Loginしてから遷移したい)時があって、その時につまずいたことがあったので、備忘録として残しておきます。
ログインしているかどうかはStateNotifierProviderで検知する場合とします。

pubspec.yaml
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

シンプルな例

main.dart

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を更新させています。


main_page.dart
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が更新されます。


user_controller.dart
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を呼び出せるようにしてあります。


user_controller.freezed.dart
//省略

$flutter packages pub run build_runner build --delete-conflicting-outputsをしてください


page_router.dart
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されます。

参考

https://zenn.dev/mkikuchi/articles/cc87c84e1404c4

Discussion