https://docs.flutter.dev/ui/navigation
MaterialApp을 쓴다고 가정한다.
import 'package:flutter/material.dart';
Router, Navigator 둘 다 페이지 이동할 때 사용할 수 있다.
복잡한 요구사항이 들어가면 Router를 써야 된다.
둘은 배타적이지 않으며 함께 사용하도록 설계되었다.
그냥 위젯 인스턴스를 통째로 넘겨줄 수 있다.
// Profile위젯으로 화면을 바꾸는 코드조각
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const Profile(),
));
// 만약 뒤로 가고 싶다면 push 말고 pop
Navigator.of(context).pop();
https://docs.flutter.dev/cookbook/navigation/named-routes
아니면 위젯에 문자열 이름 붙인 다음, 문자열을 인스턴스 대신 넘겨줄 수 있다. NamedRoute라고 하는데, 별로 권장하지 않는단다. 이유는
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Nav',
initialRoute: '/',
routes: {
'/': (context) => Home(),
'/profile': (context) => Profile(),
},
);
}
}
class Home extends StatelessWidget {
const Home({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: GestureDetector(
onTap: () {
Navigator.of(context).pushNamed('/profile');
},
child: Text('go Profile'),
),
),
);
}
}
Navigator로 하면 딥링크 처리에 골머리를 앓을 수 있다고 한다.
그래서 Router를 쓰고, 날로먹기 위해 공식 라이브러리 go_router를 쓴다. (아니면 Get을 쓴다고 함)
https://pub.dev/packages/go_router
https://jh-industry.tistory.com/123
그렇다고 한다.
TabBar.tabs[n]을 누르면 TabBarView.children[n]을 보여준다.
DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab(text: 'first'),
Tab(icon: Icon(Icons.directions_transit)),
Tab(text: 'thrid', icon: Icon(Icons.table_bar)),
],
),
title: const Text('Tabs Demo'),
),
body: TabBarView(
children: [
Text('hi 1st'),
Icon(Icons.directions_boat),
Container(color: Colors.amber),
],
),
),
),
그냥 그렇다.
class _MyHomePageState extends State<MyHomePage> {
int _selectedIndex = 0;
// _selectedIndex에 따라 scaffold body에 보여줄 위젯
static const List<Widget> _widgetOptions = <Widget>[
Text('selected Index 0'),
Text('selected Index 1'),
Text('selected Index 2'),
];
void _onItemTapped(int index, BuildContext context) {
setState(() {
_selectedIndex = index;
});
Navigator.pop(context); // drawer닫기
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: _widgetOptions[_selectedIndex],
),
drawer: Drawer(
child: ListView(
children: [
const DrawerHeader(
decoration: BoxDecoration(
color: Colors.blue,
),
child: Text('Drawer Header'),
),
// 선택가능한 drawer내 버튼 3개
ListTile(
title: const Text('index 0'),
selected: _selectedIndex == 0,
onTap: () {
_onItemTapped(0, context);
},
),
ListTile(
title: const Text('index 1'),
selected: _selectedIndex == 1,
onTap: () {
_onItemTapped(1, context);
},
),
ListTile(
title: const Text('index 2'),
selected: _selectedIndex == 2,
onTap: () {
_onItemTapped(2, context);
},
),
],
),
),
);
}
}
생성자에 주는 것 말고, 네비게이팅하는 행위 자체에 데이터(arguments)를 넘겨줄 수도 있다.
출발지에서 대상지로 데이터를 보내려면 아래처럼 하면 된다.
// 주는쪽에서
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DetailScreen(),
settings: RouteSettings(
arguments: 42, // 넘길 데이터
),
),
);
// 받는 쪽에서
final n = ModalRoute.of(context)!.settings.arguments as int;
print(n);
아래는 동작하는 예제
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
title: 'Passing Data',
home: ListScreen(),
),
);
}
class ListScreen extends StatelessWidget {
ListScreen({super.key});
final List<int> nums = List.generate(
5,
(i) => i,
);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todos'),
),
body: ListView.builder(
itemCount: nums.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('${nums[index]}'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailScreen(),
settings: RouteSettings(
arguments: nums[index],
),
),
);
},
);
},
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key});
Widget build(BuildContext context) {
final num = ModalRoute.of(context)!.settings.arguments as int;
// Use the Todo to create the UI.
return Scaffold(
appBar: AppBar(
title: Text('$num'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Text('You tapped on number $num'),
),
);
}
}
Navigator.push로 다른 화면을 보여 준 뒤, 액션에 의해 pop될 것이 예상 되는 경우
--> pop에 파라미터를 실어 보내고, push로 리턴받을 수 있다.
// push하는 쪽 (데이터 돌려 받을 곳)
final result = Navigator.of(context).push(
MaterialPageRoute(builder: (context) => Somewhere()),
);
if (!context.mounted) return;
print(result); // 'I_am_result'
// pop 하는 쪽 (데이터 돌려 보낼 곳)
Navigator.pop(context, 'I_am_result');
https://pub.dev/documentation/go_router/latest/topics/Configuration-topic.html
GoRoute와 GoRouter가 다르다는 (당연한)사실에 주의
context.go
를 쓰자.
_router.go(PATH)
와 context.go(PATH)
는 같은 동작을 한다.
다만 context.go()
는 빌드 컨텍스트에서 알아서 라우터를 찾는다.
특별한 제어가 필요한게 아니면 컨텍스트를 통해서 이동하는게 바람직하다고 한다.
routerConfig를 리턴한다.
MaterialApp.router
생성자의 routerConfig
값으로 주면 된다.
final _router = GoRouter(
routes : [
GoRoute(
path : '/',
builder : (context, state) => HomeScreen(),
),
GoRoute(
path : '/profile',
builder : (context, state) => ProfileScreen(),
),
]
);
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
routerConfig: _router,
);
}
}
참고로
initialLocation
debugLogDiagnostics
errorBuilder
final _router = GoRouter(
initialLocation : '/어딘가',
debugLogDiagnostics: true,
errorBuilder : (context, state) => ErrorScreen(state.error),
routes : [ ... ],
);
path와 builder 두 개가 필수 파라미터
builder에는 builder 또는 pageBuilder가 들어갈 수 있음
GoRouter 생성자가 호출된 뒤에도 라우트의 목록과 내용을 바꿀 수 있다.
RoutingConfig
를 만든다. (Route r
Config가 아니다)ValueNotifier<RoutingConfig>
로 1을 감싼다GoRouter.routingConfig
생성자로 만든다. 파라미터 routingConfig의 값으로 2를 준다2번_객체.value
에 새로운 RoutingConfig
를 만들어 대입해주면 된다.코드로 정리하면 아래와 같다.
// 1~2
final ValueNotifier<RoutingConfig> rcf = ValueNotifier<RoutingConfig>(
RoutingConfig(
routes : <RouteBase>[
GoRoute(path: 경로, builder: 빌더),
GoRoute(path: 경로, builder: 빌더),
]
)
)
// 3
final GoRouter _router = GoRouter.routingConfig(
routingConfig: rcf,
);
// 4
rcf.value = RoutingConfig(
routes: <RouteBase>[ // 맘대로 바꾸면 됨
GoRoute(path: 새_경로, builder: 원래_빌더),
GoRoute(path: 원래_경로, builder: 새_빌더),
]
)
4번 과정(새 설정 대입)까지 끝나면 GoRouter는 곧바로!
변경된 RoutingConfig로 현재 라우트를 재분석한다.
path template에 경로 파라미터를 넘길 수 있음(e.g., userId).
이름은 고유해야 함.
참고 :
state.pathParameters[something]
은String?
타입이다.
GoRoute(
path : '/profile/:userId',
builder : (context, state) => ProfileScreen(
userId : state.pathParameters['userId'],
),
)
// 이동하는 메소드는
_router.go('/profile/123'); // 123은 userId
query string도 받을 수 있음. (path template에 미리 정의되지는 않았지만)
GoRoute(
path : '/profile',
builder : (context, state) => ProfileScreen(
userName : state.uri.queryParameters['userName'],
),
)
// 쿼리스트링을 넣어서 이동하는 메소드는
_router.go('/profile?userName=wjlee'); // wjlee는 userName
아래 두 개 path에 대해 각각의 GoRoute가 설정되었다고 하자.
/profile
/profile/detail
아래 코드는 두 GoRoute가 같은 level에 각각 있다.
이 상태에서 _router.go('/profile/detail')
을 하면,
/profile/detail
로 대체한다. final _router = GoRouter(routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(
userName: state.uri.queryParameters['userName'],
),
),
GoRoute(
path: '/profile/detail',
builder: (context, state) => ProfileDetailScreen(),
),
]);
아래 코드는 /profile/detail
을 profile
라우트의 자식으로 넣어버렸다.
이 상태에서 _router.go('profile/detail')
을 한 경우,
/profile
에서 /detail
을 Navigator.push()
하는 것과 같다. (Navigator.of(context).widget.pages
를 찍어보면 알 수 있다.)/profile
, /detail
두 개가 스택에 들어가는 것./profile/detail
에서 pop하면 /profile
로 간다.조금 다르게 _router.push('/profile/detail')
을 하면 스택에 찍히는게 /profile/detail
하나밖에 없는 재미있는 상황을 볼 수 있다. 마치 child routes가 아닌 것 처럼 행동한다.
이렇듯 어떤 GoRoute의 하위 라우트로 GoRoute를 넣은걸 Child routes라고 한다.
final _router = GoRouter(routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(
userName: state.uri.queryParameters['userName'],
),
routes: [
GoRoute(
path: 'detail', // '/profile/detail'이 여기로 왔다.
builder: (context, state) => ProfileDetailScreen(),
),
],
),
]);
https://pub.dev/documentation/go_router/latest/go_router/ShellRoute-class.html
지금껏 살펴본 것은 path에 따라 스크린을 통째로 갈아치워버렸다.
그러지 않고 화면의 일부만 바꿀 수도 있다.
(마치 머티리얼 bottom navigation bar가 항상 그 자리에 있는 것 처럼)
그걸 하고싶다면 ShellRoute
를 쓴다.
ShellRoute의 builder함수 인수에 child가 있다.
이걸 빌더가 리턴하는 위젯에 넘겨서 잘 써먹으면 된다.
ShellRoute.routes의 path가 바뀌면 child만 다시 빌드한다.
final _router = GoRouter(routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
ShellRoute(
builder: (context, state, child) => ProfileScreen(
child: child,
),
routes: [
GoRoute(
path: '/profile/summary',
builder: (context, state) => Text('Profile Summary'),
),
GoRoute(
path: '/profile/detail',
builder: (context, state) => Text('Profile Detail'),
),
]),
]);
가장 간단한 사용법은 .go()
메소드의 파라미터로 path를 넣어주는 것.
path parameter도(:id
), query parameter도 path string에 다 때려박을 수 있다.
e.g., /somewhere/123?name=wjlee
GoRouter.of(context).go(경로);
context.go(경로);
_router.go(경로); // 고라우터 인스턴스가 _router에 저장됐다면 이것도 된다.
스트링 하나에 모든걸 때려박는게 정 싫다면 Uri().toString()
을 넣어도 된다.
context.go(
Uri(
path : '/somewhere/123',
queryParameters : {
'name' : 'wjlee',
},
).toString()
);
추가 데이터를 extra
에 실어 보낼 수도 있다.
// 보내기
context.go(경로, extra: 'I_AM_EXTRA_DATA');
// 받기
final String str = GoRouterState.of(context).extra! as String;
context.push(), context.pop()이 가능하긴 한데 별로 권장되지 않는다고 한다.
(이름에서 예상되는대로, Navigator 스택에 쌓고 빼는 메소드다.)
브라우저 히스토리에 이슈가 좀 있다는게 권장되지 않는 이유.
해보니까 push, pop은 Router API가 의도한바를 명확하게 반영하지 못한다.
child routes가 있는 경우 자식 라우트에서 pop했을 때 부모 라우트로 돌아갈 것이 예상된다.
그러나 push를 하면 path전체를 하나로 인식해서 스택에 쌓기 때문에 push()
가 호출되었던 그 라우트로 돌아간다.
/a
의 자식 라우트로 /a/b
가 있다면go('/a/b')
를 하면 현재 스택 요소를 /a
로 대체한 뒤, /b
가 쌓인다. (pop하면 a로 간다)push('/a/b')
를 하면 스택에 /a/b
하나가 쌓인다. (pop하면 원래있던 그곳으로 돌아감)쓰면 머리 아프니까, 꼭 써야겠으면 미리미리 예상 동작을 잘 생각해서 쓰도록 하자.
참고로 push()의 좋은점은, push호출하고 기다렸다가 pop할때 값을 받을 수 있다.
// push해주는 쪽
final int? result = await context.push<int>(경로);
print(result); // 123
// pop해주는 쪽
context.pop(123);
포스트의 초반부에 밝혔듯 go_router와 함께 Navigator를 사용할 수 있다.
주의해야 하는 점은
context.go(다른경로)
동작은 GoRoute냐 ShellRoute냐에 따라 좀 다르다.
https://pub.dev/documentation/url_launcher/latest/link/Link-class.html
웹상의 실제 링크를 렌더링할 수 있다.
네이티브에서 링크를 열어 보여주려면 WebViews를 쓰면 된다.
현재 앱 상태에 기반해 incoming location을 변경한다.
e.g., 유저 인증정보 없는데 마이페이지에 접근했다? 넌 로그인 페이지 행이다.
GoRouterRedirect
타입 콜백을 GoRouter나 GoRoute의 redirect값에 넣어주면 된다.
// Top-level redirection
final _router = GoRouter(
redirect: (BuildContext context, GoRouterState state) {
return '/profile/summary';
},
routes : [ ... ],
...
);
// Route-level redirection
final _router = GoRouter(routes: [
GoRoute(
path: '/somewhere',
builder: (context, state) => HomeScreen(),
redirect: (BuildContext context, GoRouterState state) {
if (대충 무슨 조건) {
return '/profile/summary';
}
},
),
);
GoRouter - redirectLimit
로 최대 리디렉션 횟수를 지정해야 한다.
기본값은 5.
만약 이 숫자 넘어서 리디렉션 하려고 하면 에러화면이 표시된다.
플랫폼에서 딥링크를 수신하면 알아서 path처리해서 알맞은 화면 보여준다.
따로 go_router에서 처리할 거리는 없다.
GoRoute - pageBuilder
를 사용하면 트랜지션 애니메이션을 줄 수 있다.
기존 builder함수의 리턴값을 pageBuilder - CustomTransitionPage - child
로 주면 된다.
그냥 원래 알던 그 플러터 애니메이션 쓰는 것 맞다.
// 기존 builder
GoRoute(
path: '/profile/summary',
builder: (context, state) => ProfileScreen(child: Text('Profile Summary')),
),
// pageBuilder로 변경
GoRoute(
path: '/profile/summary',
pageBuilder: (context, state) {
return CustomTransitionPage(
child: ProfileScreen(child: Text('Profile Summary')),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child,
);
},
);
},
),
https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html
URL 스트링 때려넣어서 네비게이팅 할 수도 있는데, 그건 너무 무식하니까 아래 세 개 패키지를 사용한다.
또 코드 자동생성이다.
flutter pub add dev:go_router_builder
flutter pub add dev:build_runner
flutter pub add dev:build_verify
GoRouteData를 상속한 Route를 만들고, build메소드에서 실제로 보여줄 위젯을 리턴하도록 한다.
이후 아래 커멘드로 자동생성 코드를 빌드한다.
flutter pub global activate build_runner
flutter pub run build_runner build
도 있다.
// GoRoute 정의
GoRoute(
name: 'summary',
path: '/profile/summary',
builder: (context, state) => ProfileScreen(child: Text('Profile Summary')),
),
// ---------------------------------
// 네비게이팅 방법 1
context.goNamed('summary');
// 네비게이팅 방법 2
final String location = context.namedLocation('summary');
context.go(location);
// 리디렉션도 가능
redirect: (_, __) {
if (로그인 안됐다) {
return context.namedLocation('mypage');
} else {
return null; // 로그인 된 경우 리디렉션 안함.
}
}
Android, iOS