pubspec.yaml에 추가하기
go_router: ^6.0.1
main.dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
final GoRouter _router = GoRouter(routes: []);
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp.router(
// 하단에 routeInformationParser , routerDelegate 여기부분은 이 이상 따로 건들지 않아도 된다고 한다.
// GoRouter에서 제공해주는 함수
// route 정보 전달
routeInformationProvider: _router.routeInformationProvider,
// URI String을 상태 및 Go Router에서 사용할 수 있는 형태로 변환해주는 함수
routeInformationParser: _router.routeInformationParser,
// 위에서 변경된 값으로 실제로 어떤 라우트를 보여줄지 정하는 함수
routerDelegate: _router.routerDelegate,
);
}
}
final GoRouter _router = GoRouter(
initialLocation: '/', // <- index 홈 경로?
routes: [
...
// 여기에 실제 screen들을 작성해주면 된다.
]
);
작성은 대괄호([]) 안에 이렇게 작성하면 된다.
GoRoute(
path: '/', // <- 경로
// (context , state)를 받는 builder임 그리고 widget반환
builder: (_, state) => HomeScreen()
)
우선 이동할 페이지 맹글기
import 'package:flutter/material.dart';
import 'package:go_router_study/layout/default_layout.dart';
class OneScreen extends StatelessWidget {
const OneScreen({super.key});
Widget build(BuildContext context) {
return DefaultLayout(
body: Column(children: [])
);
}
}
main.dart 파일에 GoRouter 안 routers 배열안에 추가로 적는다.
GoRoute(
path: '/one',
builder: (_, state) => OneScreen()
),
다른 방법이 또 있는데
routes: [
GoRoute(
path: '/',
builder: (_, state) => HomeScreen(),
routes: [
// http://..../one
GoRoute(
path: 'one',
builder: (_, state) => OneScreen(),
routes: [
// http://..../one/two
GoRoute(
path: 'two',
builder: (_, state) => TwoScreen()
),
]
),
]
),
// http://..../two
GoRoute(
path: '/two',
builder: (_, state) => TwoScreen()
),
]
같은 two라는 경로로 가기위해 밑에는 그냥 원래 주소뒤에 two가 붙는거고 위에는 /one/two이렇게 네스팅되서 붙는다. 위에같은 경우는 맨위에 two라는 페이지가 있고 그 뒤에 one 그 뒤에 메인 이렇게 깔려있다고 보면 된다고한다. 근데 밑에 two는 단독으로 있는거라 밑에 one이 깔리고 그런건 없다고 한다.
페이지 이동을 해보기 위해 코드를 마저 작성한다.
main.dart
final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (_, state) => HomeScreen(),
routes: [
GoRoute(
path: 'one',
builder: (_, state) => OneScreen()
),
]
),
]
);
제대로 이동했는지 경로를 appbar에 출력하기위해 default_layout.dart 파일도 추가로 작성한다.
...
final router = GoRouter.of(context);
return Scaffold(
appBar: AppBar(
title: Text(router.location),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: body,
),
);
...
이동을 위해 home_screen.dart 파일에 go 함수를 작성해본다.
...
ElevatedButton(
onPressed: () {
context.go('/one');
},
child: const Text('Screen One (Go)')
)
...
실행해보면
우선 시작 전에 페이지를 두개 더 만든다.
two_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router_study/layout/default_layout.dart';
class TwoScreen extends StatelessWidget {
const TwoScreen({super.key});
Widget build(BuildContext context) {
return DefaultLayout(
body: Column(
children: []
)
);
}
}
three_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router_study/layout/default_layout.dart';
class ThreeScreen extends StatelessWidget {
const ThreeScreen({super.key});
Widget build(BuildContext context) {
return DefaultLayout(
body: Column(
children: []
)
);
}
}
그다음 main.dart에서 경로를 설정해준다.
GoRoute(
path: '/',
builder: (_, state) => HomeScreen(),
routes: [
GoRoute(
path: 'one',
builder: (_, state) => OneScreen(),
routes: [
GoRoute(
path: 'two',
builder: (_, state) => TwoScreen(),
routes: [
// http://.../one/two/three
GoRoute(
path: 'three',
builder: (_, state) => ThreeScreen(),
)
]
)
]
),
]
),
이동을 위해 home_screen.dart에서 버튼을 만든다.
ElevatedButton(
onPressed: () {
context.go('/one/two/three');
},
child: const Text('Screen Three (Go)')
),
실행하면
이렇게 경로상의 각 페이지들이 스텍처럼 쌓이게 됐다.
하지만
context.go('/one/two/three');
이런식으로 계속 작성하다보면 나중에 경로가 길어질 경우 오타가 날 확률도 높고 번거롭기 그지없다. 그래서 사용하는게 goNamed 이다.
우선 작성법은
GoRoute(
path: 'three',
name: 'three',
builder: (_, state) => ThreeScreen(),
)
이렇게 GoRoute안에 name을 작성하면된다. 그 후 다시 버튼으로가서
ElevatedButton(
onPressed: () {
context.goNamed('three');
},
child: const Text('Screen Three (GoNamed)')
),
이렇게 작성하면 된다. 결과는 똑같다. 하지만 경로나 name을 이런식으로 작성하기보단 좀더 확실하게 적어주는게 좋다고한다.
해당 파일로 가서 three_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router_study/layout/default_layout.dart';
class ThreeScreen extends StatelessWidget {
static String get routeName => 'three';
const ThreeScreen({super.key});
Widget build(BuildContext context) {
return DefaultLayout(
body: Column(
children: []
)
);
}
}
static String get routeName => 'three';
이렇게 static 형태로 작성해주고 main.dart 파일로 와서
GoRoute(
path: 'three',
name: ThreeScreen.routeName,
builder: (_, state) => ThreeScreen(),
)
이렇게 수정해준다. 그 후 다시 버튼으로 가서
ElevatedButton(
onPressed: () {
context.goNamed(ThreeScreen.routeName);
},
child: const Text('Screen Three (GoNamed)')
),
위와같이 작성해주면 끝!
one_screen.dart
import 'package:go_router/go_router.dart';
...
ElevatedButton(
onPressed: () {
context.pop();
},
child: const Text('home (pop)')
),
뒤로가기 기능이다.
테스트 용으로 error_screen.dart를 만들어서 errorpage를 확인해보려고한다.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_study/layout/default_layout.dart';
class ErrorScreen extends StatelessWidget {
final String error;
const ErrorScreen({super.key, required this.error});
Widget build(BuildContext context) {
return DefaultLayout(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(error),
ElevatedButton(
onPressed: () {
context.go('/');
},
child: const Text('HOME')
)
]
),
);
}
}
그 다음 main.dart
당연히 모든 에러를 일로 보낸다기보다 navigation에서 일어난 에러일 때만 이다.
...
initialLocation: '/',
// error가 나면 작성한 error page로 이동
errorBuilder: (context, state) {
return ErrorScreen(error: state.error.toString());
},
routes: [
...
home_screen.dart에서 일부러 없는 url로 이동할 버튼을 만든다.
ElevatedButton(
onPressed: () {
context.go('error');
},
child: const Text('error page test')
),
실행해보면
특정 변화가 있을 때 변화를 감지한 후 자동으로 라우팅을 변경하는 기능
시작전 준비를 위해 model과 provider를 만든다. (상태관리도 해야되니까 riverpod을 추가함)
pubspec.yaml에 추가하기
flutter_riverpod: ^2.1.3
그 후 user_model.dart
class UserModel {
final String name;
UserModel({ required this.name });
}
auth_provider.dart (여기다가 main.dart에 작성했던 router를 일단 옮김)
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_study/screen/error_screen.dart';
import 'package:go_router_study/screen/home_screen.dart';
import 'package:go_router_study/screen/one_screen.dart';
import 'package:go_router_study/screen/three_screen.dart';
import 'package:go_router_study/screen/two_screen.dart';
final routerProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: '/',
errorBuilder: (context, state) {
return ErrorScreen(error: state.error.toString());
},
routes: [
GoRoute(
path: '/',
builder: (_, state) => HomeScreen(),
routes: [
GoRoute(
path: 'one',
builder: (_, state) => OneScreen(),
routes: [
GoRoute(
path: 'two',
builder: (_, state) => TwoScreen(),
routes: [
// http://.../one/two/three
GoRoute(
path: 'three',
name: ThreeScreen.routeName,
builder: (_, state) => ThreeScreen(),
)
]
)
]
),
]
),
// http://.../three
// GoRoute(
// path: 'three',
// builder: (_, state) => ThreeScreen(),
// )
]
);
});
그리고 main.dart를 consumerWidget으로 변경한다.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router_study/provider/auth_provider.dart';
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends ConsumerWidget {
MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context , WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
// route 정보 전달
routeInformationProvider: router.routeInformationProvider,
// URI String을 상태 및 Go Router에서 사용할 수 있는 형태로 변환해주는 함수
routeInformationParser: router.routeInformationParser,
// 위에서 변경된 값으로 실제로 어떤 라우트를 보여줄지 정하는 함수
routerDelegate: router.routerDelegate,
);
}
}
다시 실행해보면 문제없이 잘 동작한다.
그럼 이제 시작할건데 auth_provider.dart에 작업 시작한다.
refresh -> 얘를 사용할라면 changeNotifier를 사용해야된다. 또는 Stream
auth_provider.dart
...
// 여기다가 route정리
class AuthNotifier extends ChangeNotifier {
List<GoRoute> get _route => [
GoRoute(
path: '/',
builder: (_, state) => HomeScreen(),
routes: [
GoRoute(
path: 'one',
builder: (_, state) => OneScreen(),
routes: [
GoRoute(
path: 'two',
builder: (_, state) => TwoScreen(),
routes: [
// http://.../one/two/three
GoRoute(
path: 'three',
name: ThreeScreen.routeName,
builder: (_, state) => ThreeScreen(),
)
]
)
]
),
]
),
];
}
그럼 저 AuthNotifier얘를 어떻게 가져오냐면
...
final routerProvider = Provider<GoRouter>((ref) {
final authStateProvider = AuthNotifier();
return GoRouter(
initialLocation: '/',
errorBuilder: (context, state) {
return ErrorScreen(error: state.error.toString());
},
// redirect
// refresh -> 얘를 사용할라면 changeNotifier를 사용해야된다. 또는 Stream
// 1. redirect로직이랑 refresh로직이랑 route 정보들을 따로 changeNotifier에다가 집어넣는다.
routes: authStateProvider._route
);
});
...
이런식으로 가져오면 된다. 그 다음엔 user관련된거 만들건데
...
class UserStateNotifier extends StateNotifier<UserModel?> {
UserStateNotifier(): super(null);
login({ required String name }) {
state = UserModel(name: name); // <- 로그인하면 이름 넣어버리고
}
logout() {
state = null; // <- 로그아웃하면 null로 해버림
}
}
이렇게한다. 보면 ?를 붙여서 null값을 허용했는데
로그인한 상태면 UserModel 인스턴스 상태로 넣어주고, 로그아웃 상태면 null 상태로 넣어주기위함이다.
그리고 provider를 생성할건데 작성한 UserStateNotifier 위에
...
final userProvider = StateNotifierProvider<UserStateNotifier, UserModel?>(
(ref) => UserStateNotifier()
);
class UserStateNotifier extends StateNotifier<UserModel?> {
...
작성한다. 그리고 이 userProvider를 AuthNotifier 여기서 좀 사용을 해봐야되는데 그건 어떻게 하냐
class AuthNotifier extends ChangeNotifier {
final Ref ref;
AuthNotifier({ required this.ref });
...
여기서 ref를 받을 준비하고
final routerProvider = Provider<GoRouter>((ref) {
final authStateProvider = AuthNotifier(ref: ref);
...
여기서 보내준다. 이렇게되면 AuthNotifier에서 좀 할 수 있는게 생겼는데 , 생성되자마자 실행문을 좀 작성할 수 있다.
AuthNotifier({ required this.ref }) {
// listen했을 때 반환받을 거는 UserModel 또는 null
ref.listen<UserModel?>(userProvider, (previous, next) {
// 반환받으면 뭐할거임? : 상태가 변경됐다는거만 알려줄거임
// notifyListeners(); <- 요게 state가 바꼈다는걸 좀 감지할 수 있나봄
// ChangeNotifier 얘를 바라보고있는 위젯들이 리빌드하라고 해줌 notifyListeners 얘가
// 결국 state 변경하는거랑 같다.
// 그럼 언제하냐
// if (기존값이랑 다음값이 다르면) {}
if (previous != next) {
notifyListeners();
}
});
}
이렇게 작성했다. 주석으로 설명을 적어놓았다.
이제 redirect함수를 작성할건데 주석으로 설명도 다 작성해놓았다.
......
// 이제 Redirect 로직을 좀 써볼까
// redirect 함수는 페이지를 이동할 때마다 동작한다고한다.
// 반환값을 이동할 route 이기때문에 String임
String? _redirectLogic(GoRouterState state) {
// 이거 만들면 GoRouterState state 이거 주는데 여기에는 현재 라우팅에 정보 , 경로 등등 다 준다고한다.
// 1. 유저의 현재 상태를 알고있어야함
// UserModel의 인스턴스(로그인 상태) 또는 null(로그아웃 상태)
final user = ref.read(userProvider);
// 2. 유저가 로그인을 하려는 상태인지 (로그인 페이지라면 로그인을 하려는 상태인지 알 수 있음)
final loggingIn = state.location == '/login';
// 3. 로직 작성
// if (유저정보가 null) <- 로그인한 상태가 아님
// { 유저정보가 없고 로그인하려는 중이 아니라면 로그인페이지로 이동한다. }
if (user == null) {
// null이면 이동하려던 곳 그대로 이동한다.
return loggingIn ? null : '/login';
}
// 유저 정보가 있는데 로그인페이지라면? : 홈으로 이동
if (loggingIn) {
return '/';
}
// 위 두조건 둘다아니면 너 가고싶은데로 가
return null;
}
...
작성 후 위에 GoRouter에서
initialLocation: '/',
errorBuilder: (context, state) {
return ErrorScreen(error: state.error.toString());
},
// redirect
redirect: authStateProvider._redirectLogic,
이렇게 추가하면 에러가 나버린다. 그래서 에러를 읽어보니 context를 추가하라는거같은데 쓰질않으니 _redirectLogic이 함수에서
String? _redirectLogic(_, GoRouterState state) {
이렇게 파라미터를 추가해주니까 에러가 사라졌다. 아마 강의와 버전을 다르게 해서 나왔던 에러같다.
그리고 login페이지 추가해주고
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router_study/layout/default_layout.dart';
import 'package:go_router_study/provider/auth_provider.dart';
class LoginScreen extends ConsumerWidget {
const LoginScreen({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return DefaultLayout(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () {
ref.read(userProvider.notifier).login(name: 'Code Factory');
},
child: const Text('Login')
),
ElevatedButton(
onPressed: () {
context.go('/one');
},
child: const Text('one으로 이동해보기')
),
]
),
);
}
}
경로도 추가해주고
GoRoute(
path: '/login',
builder: (_, state) => LoginScreen(),
)
initialLocation이거도 경로 로그인으로 바꾼다.
initialLocation: '/login',
그리고 home_screen.dart에 버튼도 추가한다.
ElevatedButton(
onPressed: () {
context.go('/login');
},
child: const Text('login test')
),
ElevatedButton(
onPressed: () {
ref.read(userProvider.notifier).logout();
},
child: const Text('logout test')
),
실행해보면
로그인 후 로그인 페이지로 이동
로그아웃 후 로그인 안되어있는 상태에서 다른 페이지 이동