출처: https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade
flutter 개발자라면 아래의 클래스가 익숙할 것이다.
Navigator 1.0은 일반적으로 Navigator가 named,anonymous route들을 Route 스택에 push, pop 하는 형태였다.
route들에 대한 정의 없이 그때 그때 widget을 호출하는 방식으로 화면 이동.
//화면 이동
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return NextPage();
}),
);
//화면 나가기
Navigator.pop(context);
MaterialApp이나 CupertinoApp에 route들을 미리 정의하고 정의된 route 이름을 통해 화면 이동.
class App extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
routes: {
//여기서 route name에 따라 이동할 화면을 미리 정의.
'/': (context) => HomeScreen(),
'/details': (context) => DetailScreen(),
},
);
}
}
class HomeScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FlatButton(
child: Text('View Details'),
onPressed: () {
//화면 이동시 route name을 통해 호출
Navigator.pushNamed(
context,
'/details',
);
},
),
),
);
}
}
onGenerateRoute: (settings) {
//route name을 파싱해서 parameter에 따라 동적으로 화면을 호출.
//web의 path처럼 사용 가능.
var uri = Uri.parse(settings.name);
if (uri.pathSegments.length == 2 &&
uri.pathSegments.first == 'details') {
var id = uri.pathSegments[1];
return MaterialPageRoute(builder: (context) => DetailScreen(id: id));
}
return MaterialPageRoute(builder: (context) => UnknownScreen());
},
Navigator 2.0은 앱의 화면을 앱 상태로 기능하게 하고 기본플랫폼에서 route를 파싱하는 기능을 제공하기 위해 몇가지 클래스를 추가 했다.
Navigator 2.0의 상호작용 그래프
Navigator의 history stack을 구성하는 불변 오브젝트(immutable object)
build(BuildContext context) {
return MaterialApp(
title: 'Books App',
//navigator 안에다 page들을 넣는다.
home: Navigator(
pages: [
//이렇게 MaterialPage를 사용해도 되고
MaterialPage(
child: BooksListScreen(),
),
],
),
);
}
//이런 식으로 Page를 상속 받아서 구현해도 된다.
class BookDetailsPage extends Page {
final Book book;
BookDetailsPage({
this.book,
}) : super(key: ValueKey(book));
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
pageBuilder: (context, animation, animation2) {
final tween = Tween(begin: Offset(0.0, 1.0), end: Offset.zero);
final curveTween = CurveTween(curve: Curves.easeInOut);
return SlideTransition(
position: animation.drive(curveTween).drive(tween),
child: BookDetailsScreen(
key: ValueKey(book),
book: book,
),
);
},
);
}
}
Widget
Navigator에 의해 보여지는 Page들을 구성시킨다(configure). 일반적으로 Page 리스트는 기본플랫폼(android, ios, web) 혹은 앱의 state 변경에 의해 변경된다.
return MaterialApp.router(
title: 'Books App',
routeInformationParser: _routeInformationParser,
routerDelegate: _routerDelegate,
);
RouteInformationProvider로부터 RouteInformation을 받아 user-defined data type으로 파싱한다.
//RouteInformationParser를 상속 받아서 구현
//BookRoutePath가 user-defined data type
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
//RouteInformation을 파싱해서 user-defined data type으로 리턴
Future<BookRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
// Handle '/book/:id'
if (uri.pathSegments.length == 2) {
if (uri.pathSegments[0] != 'book') return BookRoutePath.unknown();
var remaining = uri.pathSegments[1];
var id = int.tryParse(remaining);
if (id == null) return BookRoutePath.unknown();
return BookRoutePath.details(id);
}
}
//user-defined data type이 각각 어떤 Route와 대응되는지를 정의
RouteInformation restoreRouteInformation(BookRoutePath path) {
if (path.isUnknown) {
return RouteInformation(location: '/404');
}
if (path.isHomePage) {
return RouteInformation(location: '/');
}
if (path.isDetailsPage) {
return RouteInformation(location: '/book/${path.id}');
}
return null;
}
}
Router가 app의 state 변경에 대해 어떻게 처리하고 응답을 리턴 할지에 대해 구체적인 동작을 정의한다.
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
//...
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
BookRoutePath get currentConfiguration {
return _selectedBook == null
? BookRoutePath.home()
: BookRoutePath.details(books.indexOf(_selectedBook));
}
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
if (_selectedBook != null)
BookDetailsPage(book: _selectedBook)
],
//Navigator.pop()이 호출 되었을 때 실행되는 부분.
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
_selectedBook = null;
show404 = false;
notifyListeners();
return true;
},
);
}
//user-defined data type을 전달받아 적절하게 state를 변경
//예제에서는 _selectedBook을 업데이트
Future<void> setNewRoutePath(BookRoutePath path) async {
if (path.isDetailsPage) {
if (path.id < 0 || path.id > books.length - 1) {
show404 = true;
return;
}
_selectedBook = books[path.id];
} else {
_selectedBook = null;
}
show404 = false;
}
void _handleBookTapped(Book book) {
_selectedBook = book;
notifyListeners();
}
}
back button action을 Router에게 전달한다.
platform단에서 발생하는 예기치 못한 화면 변경(backbutton(android, web), url 변경(web)에 대응하기 위한 기능, 좀더 향상된 page 컨트롤 지원(멀티 pop or push, stack 내의 특정 page 삭제 등) 등이 추가되었다. 업무 중에도 멀티 pop이나 현재 화면 이전의 히스토리만 삭제 같은 것들이 지원되지 않아서 골치였던 부분들이 있었는데 반가운 소식이다. 다만 전면적으로 개념이 바뀐 업데이트라 마이그레이션이 쉽진 않을 듯 하다.