설치하기
- Go_Router를 사용하기 위해서 pubspec.yaml에 Go_Router를 추가해야한다.
Go Router
- 설치가 끝나면 main.dart 파일에서
MaterialApp
에 .router()
라는 메소드를 사용할 수 있다.
- router 안에는 3가지 값들이 들어간다.
class MyApp extends StatelessWidget {
const MyApp({super.key});
final GoRouter _router = GoRouter(routes: []);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
);
}
}
GoRouter _router
에는 여러 값들이 들어가는 데 몇가지만 우선 알아보겠다.
final GoRouter _router = GoRouter(routes: []);
initialLocation
에는 첫 스크린 경로가 들어가게된다.
routes
는 리스트로 값을 받고 이 곳에 screen 값들이 들어가게 된다.
routes: []
안에는 GoRoute가 들어간다. path에는 경로를 지정해주고 builder에는 스크린을 지정해준다.
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
Go 함수
- 또 다른 스크린을 추가할 때는 라우트를 새로 등록을 해줘야한다.
- 스크린을 추가할 때 2가지 방식이 있다.
- 첫번째는 GoRoute를 밑에 추가하는 방식이다
- 이 방식의 특징은 도매인이 독립적으로 만들어진다.`,
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/one',
builder: (context, state) => OneScreen(),
),
GoRoute(
path: '/two',
builder: (context, state) => TwoScreen(),
),
],
- 두번째 방식은 GoRoute 안에 routes를 추가 하는 것이다.
- 이 방식으로 코드를 작성하면 각각의 스크린들이 네스팅되어 주소가 쌓이는 구조가 된다.
http://~~~/one/two
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
routes: [
GoRoute(
path: 'one',
builder: (context, state) => OneScreen(),
routes: [
GoRoute(
path: 'two',
builder: (context, state) => TwoScreen(),
),
]
),
],
),
],
- 이번 예제에서는 네스팅 구조 쓰이는 방식을 사용한다.
- 라우트를 작성하고나면 스크린을 이동할 함수를 만들어야한다.
go_route
를 사용하기 전에는 Navigator
를 사용해서 스크린을 이동했지만 이번에는 다른 방식으로 스크린을 이동하는 방법을 사용해 볼 것이다.
HomeScreen
에 버튼을 추가한다. onPressed
함수 body
에 context.go()
를 작성한다. go
함수 안에는 이동할 페이지 주소를 넣어준다.
- 버튼을 눌러 확인해보면
Push
를 사용했던 것과 같은 효과를 볼 수 있다.
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return DefaultLayout(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () {
context.go('/one');
},
child: Text('Screen One'),
),
],
),
);
}
}
GoNamed 함수
- 스크린을 2개 더 추가해서 총 3개를 만들어 라우트 해준다.
- 도메인 주소는 다음과 같이 된다.
http://~~~/one/two/three
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
routes: [
GoRoute(
path: 'one',
builder: (context, state) => OneScreen(),
routes: [
GoRoute(
path: 'two',
builder: (context, state) => TwoScreen(),
routes: [
GoRoute(
path: 'Three',
builder: (context, state) => ThreeScreen(),
),
],
),
],
),
],
),
],
- HomeScreen()에서 ThreeScreen으로 이동하는 버튼을 만들고 실행해 본다.
ElevatedButton(
onPressed: () {
context.go('/one/two/three');
},
child: const Text('Screen Three'),
),
- 네스팅을 해서 스크린을 집어넣고 마지막 라우트 스크린을
go
하게되면 Push 하지 않았던 중간에 있는 스크린들도 보이게 된다.
context.go('/one/two/three');
에 있는 /one/two/three
를 간단하게 바꿔 줄 수 있다.
- GoRoute에 name을 사용할 수 있는데 이곳에 스크린이름을 작성해준다.
GoRoute(
path: 'three',
name: 'three',
builder: (context, state) => ThreeScreen(),
),
- HomeScreen에 있는 onPressed 함수에
go
가 아닌 goNamed
를 사용해서 간단히 바꿔줄 수 있다.
ElevatedButton(
onPressed: () {
context.goNamed('three');
},
child: const Text('Screen Three'),
),
- 경로를 String 값으로 작성하다는 것보다 안전한게 코드를 작성하는 방법이 있다.
- 해당하는 스크린에서
static String get routeName => 'three';
을 작성해준다. 그러면 라우트와 버튼 경로에 String 값이 아닌 자동완성으로 코드를 작성해서 실수를 줄일 수 있다.
pop
pop
은 뒤로가기를 할 때 사용한다. Navigator.of(context).pop()
과 같은 방식으로 작성하면 된다.
go_route
에서는 context.pop()
을 사용하면 된다.
errorScreen
GoRouter
에 errorBuilder
를 추가한다. 그리고 context와 state를 파라미터로 받는 함수를 작성하고 ErrorScreen을 반환한다.
- Navigotion 상에서 발생한 에러는 state에 들어오게 된다.
final GoRouter _router = GoRouter(
initialLocation: '/',
errorBuilder: (context, state) {
return ErrorScreen(
error: state.error.toString(),
);
},
Redirect & Refresh
- 특정 url로 이동하려고 했을 때 이동하려는 곳이 아닌 다른곳으로 이동하는 것을 감지하고 자동으로 라우팅을 변경하는 것을 말한다.
- 상태관리를 위해 먼저
Provider
를 설치해준다.
- 모델을 만들어 로그인 여부를 확인하는데 사용한다.
class UserModel {
final String name;
UserModel({
required this.name,
});
}
- provider 파일을 만들고 이 곳에 라우터를 넣어준다.
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: [
GoRoute(
path: 'three',
name: ThreeScreen.routeName,
builder: (_, state) => ThreeScreen(),
)
],
)
],
),
],
),
],
);
},
);
- main.dart 파일도 수정해준다.
- Provider를 사용해서 watch() 해주고 router를 넣어준다.
void main() {
runApp(
ProviderScope(
child: _App(),
),
);
}
class _App extends ConsumerWidget {
_App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
debugShowCheckedModeBanner: false,
routerDelegate: router.routerDelegate,
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
);
}
}
- redirect, refresh로직과 route 정보들을 따로 changeNotifier에다가 집어넣는다.
- refresh 기능을 사용하기 위해서는 무조건 changeNotifier를 사용해야 한다.
- ChangeNotifier를 상속하는
AuthNotifier
클래스를 만든다. 이 클래스에는 앞서 작성했던 GoRoute로직을 잘라넣어 생성한다.
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: [
GoRoute(
path: 'three',
name: ThreeScreen.routeName,
builder: (_, state) => ThreeScreen(),
)
],
)
],
),
],
),
];
}
- 이렇게 만든 AuthNotifier를 사용하기 위해
final authStateProvider = AuthNotifier();
를 선언한다.
그리고 routes
값에 authStateProvider
를 넣어준다.
- 여기까지는 일반 클래스를 불러오는 것과 같다.
final routerProvider = Provider<GoRouter>(
(ref) {
final authStateProvider = AuthNotifier();
return GoRouter(
initialLocation: '/',
errorBuilder: (context, state) {
return ErrorScreen(error: state.error.toString());
},
routes: authStateProvider._route,
);
},
);
- 유저 상태를 관리하기 위해 StateNotifier를 만들어준다.
- provider를 사용하기 위해 만들어준다.
final userProvider = StateNotifierProvider<UserStateNotifier, UserModel?>(
(ref) => UserStateNotifier(),
);
class UserStateNotifier extends StateNotifier<UserModel?> {
UserStateNotifier() : super(null);
login({
required String name,
}) {
state = UserModel(name: name);
}
logout() {
state = null;
}
}
- 생성한 userProvider를 AuthNotifier에 사용하기위해(listen) ref를 받아오고 보내준다.
class AuthNotifier extends ChangeNotifier {
final Ref ref;
AuthNotifier({
required this.ref,
})
final routerProvider = Provider<GoRouter>(
(ref) {
final authStateProvider = AuthNotifier(ref: ref);
- 생성자 함수를 이용해서 생성이 되자마자 listen을 한다.
- userProvider를 listen하고 반환 받을 값을 <UserModel?>로 정해준다.
- 반환을 받게 되면 상태가 변경됬다라는 것만 알려주면 된다. ChangeNotifier에서는
notifyListeners();
를 사용하면 된다.
notifyListeners
는 ChangeNotifier를 바라보고 있는 모든 위젯들을 리빌드하게 한다. 이 때 notifyListeners
는 기존값과 다음값이 다를때 실행하게 한다.
class AuthNotifier extends ChangeNotifier {
final Ref ref;
AuthNotifier({
required this.ref,
}) {
ref.listen<UserModel?>(
userProvider,
(previous, next) {
if (previous != next) {
notifyListeners();
}
},
);
}
- 이제 redirect 로직을 작성해 보겠다.
- 반환 값이 route여서 String? 타입을 작성한다.
GoRouterState state
에는 라우팅 정보, 경로들이 담겨있다.
String? _redirectLogic(GoRouterState state) {
final user = ref.read(userProvider);
final logginIn = state.location == '/login';
if (user == null) {
return logginIn ? null : '/login';
}
if (logginIn) {
return '/';
}
return null;
}
redirect
에 authStateProvider._redirectLogic
작성하면 이 함수 안에 GoRouterState state
를 자동으로 넣어준다. 그래서 네비게이션이 될 때 마다 state가 들어가게 된다.
refreshListenable
에 authStateProvider
를 넣어준다.refreshListenable
은 이곳에 들어가는 값의 상태가 바뀌었을 때마다 redirect
를 재실행 해준다.
final routerProvider = Provider<GoRouter>(
(ref) {
final authStateProvider = AuthNotifier(ref: ref);
return GoRouter(
initialLocation: '/',
errorBuilder: (context, state) {
return ErrorScreen(error: state.error.toString());
},
routes: authStateProvider._route,
redirect: authStateProvider._redirectLogic,
refreshListenable: authStateProvider,
);
},
);
- 로그인 스크린과 라우트를 생성해준다.
- initialLocation의 경로도
initialLocation: '/login'
으로 변경해 준다.
GoRoute(
path: '/login',
builder: (_, state) => LoginScreen(),
),
class LoginScreen extends ConsumerWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return DefaultLayout(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () {
ref.read(userProvider.notifier).login(name: 'apple');
},
child: Text('로그인'),
),
],
),
);
}
}
- HomeScreen에 로그인 버튼과 로그아웃 버튼을 생성한다.
ElevatedButton(
onPressed: () {
context.go('/login');
},
child: const Text('Login Screen'),
),
ElevatedButton(
onPressed: () {
ref.read(userProvider.notifier).logout();
},
child: const Text('Logout Screen'),
),
- 로그인 버튼을 누르면 홈으로 이동하게 되고, 로그아웃 버튼을 누르면 다시 로그인 스크린으로 이동하게 된다. 그리고 홈에 만들어 놓은 로그인 스크린버튼을 누르게 되면 위에서 작성한 로직에 의해 아무 반응도 일어나지 않는다