[Flutter] 프로젝트 초기 설정 - 라우팅 (feat. Riverpod)

Cherrie KimCherrie Kim
4 min read

앞서 말했든 이번 프로젝트에선 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'),
        ],
      ),
    );
  }
}

작성한지 한 달도 되지 않은 코드인데, 그 짧은 시간을 두고 다시 봐도 고칠 점이 이곳저곳 보인다. 코드는 한 번 작성한다고 끝이 아니라, 계속 회고하는 시간이 필요하단걸 다시 한 번 체감했다!

0
Subscribe to my newsletter

Read articles from Cherrie Kim directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Cherrie Kim
Cherrie Kim