Go Router 이론

Baek Dong Hyun·2023년 1월 23일
3

프로젝트 세팅하기

pubspec.yaml에 추가하기

go_router: ^6.0.1

GoRouter 세팅

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()
)

Go 함수

우선 이동할 페이지 맹글기

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)')
)
...

실행해보면

GoNamed 함수

우선 시작 전에 페이지를 두개 더 만든다.

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)')
),

위와같이 작성해주면 끝!

Pop 함수

one_screen.dart

import 'package:go_router/go_router.dart';

...

ElevatedButton(
  onPressed: () {
    context.pop();
  }, 
  child: const Text('home (pop)')
),

뒤로가기 기능이다.

ErrorScreen

테스트 용으로 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')
),

실행해보면

Redirect와 Refresh

특정 변화가 있을 때 변화를 감지한 후 자동으로 라우팅을 변경하는 기능

시작전 준비를 위해 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

  1. redirect로직이랑 refresh로직이랑 route 정보들을 따로 changeNotifier에다가 집어넣는다.

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')
),

실행해보면

로그인 후 로그인 페이지로 이동

로그아웃 후 로그인 안되어있는 상태에서 다른 페이지 이동

profile
안녕하세요.

0개의 댓글