[Flutter] 프로젝트 초기 설정 - 라우팅 (feat. Riverpod)
Table of contents
앞서 말했든 이번 프로젝트에선 Riverpod
를 활용할 예정이기에, 라우팅 기능이 내장되어있는 GetX
와 달리 라우팅을 따로 설정해줘야 했다.
가장 인기있는 라우팅 라이브러리인 GoRouter
를 활용하기로 결정했다. [링크]
GoRouter를 사용하는 기본적 라우팅 예제는 다음과 같다:
import 'package:go_router/go_router.dart';
// GoRouter configuration
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/page2/:id',
builder: (context, state) => Page2Screen(id: state.pathParameters['id']),
),
],
);
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
주요 메소드 설명
go
:- 특정 경로로 이동한다.
goNamed
:- 지정된 이름을 가진 경로로 이동한다.
pop
:흔히 아는 pop. 현재 화면을 닫고 이전 화면으로 돌아간다.
canPop()
을 통해 내비게이션 스택에서 뒤로 이동 가능한지 확인 가능하다.
popUntil
:- 특정 조건을 만족할 때까지 내비게이션 스택에서 화면을 pop 한다
push
:- 새로운 경로를 내비게이션 스택에 추가하고 해당 경로로 이동한다.
pushNamed
:- 지정된 이름을 가진 새로운 경로를 스택에 추가하고, 해당 경로로 이동한다.
replace
:현재 경로를 새로운 경로로 교체한다.
replaceNamed
도 지원된다. 여타 *Named 들과 비슷한 기능을 한다.
위 메소드들의 예제 코드를 적어보자면, 다음과 같다:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/details',
builder: (context, state) => DetailsScreen(),
),
GoRoute(
path: '/namedRoute',
name: 'namedRoute',
builder: (context, state) => NamedRouteScreen(),
),
],
);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: _router.routerDelegate,
routeInformationParser: _router.routeInformationParser,
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home Screen')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
context.go('/details');
},
child: Text('Details로 이동(go) 한다'),
),
ElevatedButton(
onPressed: () {
context.goNamed('namedRoute');
},
child: Text('Named Route로 이동(go) 한다'),
),
ElevatedButton(
onPressed: () {
context.push('/anotherRoute');
},
child: Text('Another Route로 push 해서 이동한다'),
),
ElevatedButton(
onPressed: () {
if (context.canPop()) {
context.pop();
}
},
child: Text('canPop 조건을 만족하면 pop 한다'),
),
ElevatedButton(
onPressed: () {
context.replace('/details');
},
child: Text('Details 로 replace (스택 내 교체) 한다'),
),
ElevatedButton(
onPressed: () {
context.replaceNamed('namedRoute');
},
child: Text('Named Route 또한 replace가 사용 가능하다'),
),
],
),
),
);
}
}
프로젝트 사용 예시 (feat. 내비게이션 바 칠하기)
저번 계절학기 프로젝트에선 다음과 같이 사용했다.
(아직 배우는 단계에서 구현한 것이라 비합리적일 수 있습니다. 더 좋은 구현 방법이 있다면 언제든 의견 남겨주시면 감사하겠습니다!)
lib/app_router.dart
파일을 따로 두어 라우팅을 관리했다.공통 UI shell을 제공하기 위해 ShellRoute를 사용했다. 이를 통해 여러 페이지에서 일관된 레이아웃을 공유 가능하다.
- 나는 내비게이션 바를 위해 사용했지만, 그 외에도 헤더나 푸터 등에 적용 가능하다!
class AppRouter {
GoRouter router(String locationAddress) {
return GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) {
return Scaffold(
body: child,
bottomNavigationBar: CustomNavBar(
selectedIdx: calculateSelectedIdx(state.uri.path),
),
);
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/board',
builder: (context, state) => const BoardPage(),
),
GoRoute(
path: '/post/:id',
builder: (context, state) {
final postId = state.pathParameters['id'];
return PostPage(id: postId!);
},
),
GoRoute(
path: '/write',
builder: (context, state) => const WritePage(),
),
],
),
],
);
}
나는 아래 사진처럼 현재 페이지에 따라 내비게이션 바 아이콘을 색칠하고 싶었다.
그걸 위해 먼저 현재 페이지의 인덱스를 계산하는 함수를 만들었고,
(이때는 급해서 그냥 경로를 하드코딩해서 인덱스를 반환하게 했는데, Map으로 처리하면 관리가 더 쉬울 것 같다.)
static int calculateSelectedIdx(String location) {
switch (location) {
case '/':
return 0;
case '/write':
return 1;
case '/board':
case '/post':
return 2;
default:
return 0;
}
}
}
여기서 반환된 index를 CustomNavBar에 넣어준다.
onItemTap을 통해 적절한 경로로 내비게이션을 수행하고,
전달받은 index 값에 따라 아이콘의 색을 칠해준다 (지금 보니 이것도 중복 코드가 넘 많아서 더 깔끔하게 정리하면 좋겠다 싶다)
class CustomNavBar extends StatelessWidget {
final int selectedIdx;
const CustomNavBar({super.key, required this.selectedIdx});
void _onItemTap(BuildContext context, int idx) {
switch (idx) {
case 0:
context.go('/');
break;
case 1:
context.go('/write');
break;
case 2:
context.go('/board');
break;
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: Colors.white, boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
offset: const Offset(0, -1),
blurRadius: 6)
]),
child: NavigationBar(
selectedIndex: selectedIdx,
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
backgroundColor: Colors.white,
height: 60,
indicatorColor: Colors.transparent,
onDestinationSelected: (int idx) {
_onItemTap(context, idx);
},
destinations: <NavigationDestination>[
NavigationDestination(
icon: Icon(
Icons.location_on_outlined,
color: selectedIdx == 0 ? Colors.black54 : Colors.black12,
size: 32,
),
label: 'Home'),
NavigationDestination(
icon: Icon(
Icons.post_add_outlined,
color: selectedIdx == 1 ? Colors.black54 : Colors.black12,
size: 32,
),
label: 'Post'),
NavigationDestination(
icon: Icon(
Icons.grid_view_outlined,
color: selectedIdx == 2 ? Colors.black54 : Colors.black12,
size: 32,
),
label: 'Board'),
],
),
);
}
}
작성한지 한 달도 되지 않은 코드인데, 그 짧은 시간을 두고 다시 봐도 고칠 점이 이곳저곳 보인다. 코드는 한 번 작성한다고 끝이 아니라, 계속 회고하는 시간이 필요하단걸 다시 한 번 체감했다!
Subscribe to my newsletter
Read articles from Cherrie Kim directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by