Flutter Navigator 2.0

다용도리모콘·2021년 8월 31일
1

Wiki - Flutter

목록 보기
3/7

출처: https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade

Flutter Navigator 1.0

flutter 개발자라면 아래의 클래스가 익숙할 것이다.

  • Navigator - Route 오브젝트들의 스택을 관리하는 위젯
  • Route - Navigator에 의해 관리되는 오브젝트로 화면을 그린다. 일반적으로 MaterialPageRoute와 같은 클래스로 implement 된다.

Navigator 1.0은 일반적으로 Navigator가 named,anonymous route들을 Route 스택에 push, pop 하는 형태였다.

Anonymous routes

route들에 대한 정의 없이 그때 그때 widget을 호출하는 방식으로 화면 이동.

//화면 이동
Navigator.push(
	context,
    MaterialPageRoute(builder: (context) {
    	return NextPage();
    }),
);

//화면 나가기
Navigator.pop(context);

Named routes

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

Named routes 심화 과정(w onGenerateRoute)

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());
},

Flutter Navigator 2.0

Navigator 2.0은 앱의 화면을 앱 상태로 기능하게 하고 기본플랫폼에서 route를 파싱하는 기능을 제공하기 위해 몇가지 클래스를 추가 했다.

Navigator 2.0의 상호작용 그래프

Page

Navigator의 history stack을 구성하는 불변 오브젝트(immutable object)


  Widget 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,
          ),
        );
      },
    );
  }
}
 

Router

Navigator에 의해 보여지는 Page들을 구성시킨다(configure). 일반적으로 Page 리스트는 기본플랫폼(android, ios, web) 혹은 앱의 state 변경에 의해 변경된다.

 return MaterialApp.router(
      title: 'Books App',
      routeInformationParser: _routeInformationParser,
      routerDelegate: _routerDelegate,
    );

RouteInformationParser

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;
  }
}

RouterDelegate

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

BackButtonDispatcher

back button action을 Router에게 전달한다.

마치며

platform단에서 발생하는 예기치 못한 화면 변경(backbutton(android, web), url 변경(web)에 대응하기 위한 기능, 좀더 향상된 page 컨트롤 지원(멀티 pop or push, stack 내의 특정 page 삭제 등) 등이 추가되었다. 업무 중에도 멀티 pop이나 현재 화면 이전의 히스토리만 삭제 같은 것들이 지원되지 않아서 골치였던 부분들이 있었는데 반가운 소식이다. 다만 전면적으로 개념이 바뀐 업데이트라 마이그레이션이 쉽진 않을 듯 하다.

0개의 댓글