Study/Dart,Flutter

2. 플러터 Navigator와 onGenerateRoute그리고 popUntil 의 고찰

코딩 잘 할거얌:) 2021. 5. 15. 02:06
반응형

앱 페이지를 넘기는 과정 중, PushName과 Pop만 사용하다가 popUntil을 사용해야 했다. 별생각 없이 PushNamed처럼 popUntil을 사용하는데 검은색 화면만 떠서 왜 안 될까 했는데 이번에 해결을 해서 포스팅하기로 했다.

 


바쁜 사람을 위한 결론!

  1. MaterialApp 혹은 CupertinoApp의 Parameter, onGenerator에 RouteGenerator을 선언했다고 popUntil이 되는 것이 아니다. 정확히는 ModalRoute.withName에 저장이 되지 않는다.
  2. ModalRoute.withName으로 Page이름을 적기 위해서는 Navigator push 할 때 MaterialPageRoute 또는 CupertinoPageRoute의 Parameter, setting에 정의를 해 주어야 한다.
  3. 아래의 코드를 참고하면 더욱 이해하기가 쉬울 것이다.

   //Instead of
   //Navigator.of(context).push(
   //         MaterialPageRoute(
   //          builder: (context) => Page1(),
   //         ),
   //       );
   //
   //Use this
   Navigator.of(context).push(
           MaterialPageRoute(
           settings: RouteSettings(name: "/Page1"),
           builder: (context) => Page1(),
		),
   );
   
   //Then
   Navigator.of(context)
              .popUntil(ModalRoute.withName("/Page1"));
              
//https://stackoverflow.com/questions/54525086/how-to-use-navigator-popuntil-flutter

 


어느 블로그에서 코드를 보고 내 방식대로 수정하고 사용했었는데 그게 어느 블로그였는지 모르겠다. 찾으면 바로 링크를 올리도록 하겠다.

 

Route Generator와 Navigator의 사전 배경

 

Flutter의 화면 전환은 Stack으로 구성되어있다. 따라서 화면 전환을 하기 위해서는 Navigator Push, Navigator Pop을 사용해서 전환을 한다.

class CakeOrderRouteGenerator {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    assert(settings.name.indexOf("/") == 0,
        "[ROUTER] routing MUST Begin with '/'");

    var _reDefine = settings.name.replaceFirst("/", "");
    var _pathParams = p.split(
        _reDefine.split("?").length > 1 ? _reDefine.split("?")[0] : _reDefine);

//QueryParameters example
// print(Uri.base.toString()); // http://localhost:8082/game.html?id=15&randomNumber=3.14
// print(Uri.base.query);  // id=15&randomNumber=3.14
// print(Uri.base.queryParameters['randomNumber']); // 3.14

// Navigator.of(context).pushNamed('/Page1?DATA=$data&PAGENAME="name"')
// ->arguments['DATA'] = data, arguments['PAGENAME'] = "name"

// Navigator.of(context).pushNamed('/MainPage?UID=$arg'); 또는
// Navigator.of(context).pushNamed('/Page1',arguments: {"PAGE1_DATA": data}); 으로 사용
    Map<String, dynamic> arguments = settings.arguments ??
        Uri.parse(settings.name.replaceFirst("/", "")).queryParameters ??
        {};
    var _pageName = _pathParams.isNotEmpty ? _pathParams.first : null;
    Widget _pageWidget;

    switch (_pageName) {
      case 'home':
        _pageWidget = CakeOrderApp();
        break;
      case 'AddOrder':
        _pageWidget = AddOrder();
        break;
      case 'DetailPage':
        _pageWidget = DetailPage(
          cakeData: arguments["DATA"],
        );
        break;
      case 'OrderAlterPage':
        _pageWidget = OrderAlterPage(
          cakeData: arguments["DATA"],
        );
        break;
      case 'AlterPage':
        _pageWidget = AlterPage();
        break;
      case 'SettingPartTimer':
        _pageWidget = SettingPartTimer();
        break;
      case 'ReportPage':
        _pageWidget = ReportPage();
        break;
      case 'CakeSetting':
        _pageWidget = CakeSetting();
        break;
      case 'CustomerList':
        _pageWidget =
            Consumer<CustomerProvider>(builder: (context, customerProvider, _) {
          return CustomerPhone(
            customerProvider: customerProvider,
          );
        });
        break;
      case 'PasswordPage':
        _pageWidget = PasswordPage();
        break;
      case 'BackUpPage':
        _pageWidget = BackUpPage();
        break;
    }
    return CurrentOSCheck.instance['Android']
        ? MaterialPageRoute(
            builder: (context) => _pageWidget)
        : CupertinoPageRoute(
            builder: (context) => _pageWidget,
          );
  }
}

RouteGenerator를 사용하고 싶은 분들은 위의 코드를 찬찬히 읽고 숙지해서 사용하면 되겠다.

class Example extends StatelessWidget {
  const Example({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: onGenerateRoute: CakeOrderRouteGenerator.generateRoute,
//      routes: ,
    );
  }
}


....
onPressed: () {
            Navigator.of(context).pushNamed('/AddOrder');
          },
...

 

현재 내가 사용하고 있는 Main에 RouteGenterator의 코드이다. 아래는 onGenerateRoute를 어떻게 main에 정의하는지 모르는 분들을 위해 올렸다. MaterialApp 혹은 CupertinoApp안에 Parameter로 지정해 줄 수 있다.

 

플러터를 처음 시작하거나 크게 신경 쓰지 않는 사람들은 다음 화면을 넘길 때, 다음과 같이 사용할 것이다.

onPressed: () {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SecondRoute()),
  );
}

Flutter의 공식문서에도 나와있는 Navigator push의 사용방법이고 매번 이렇게 사용해도 상관은 없다. 하지만 하나의 클래스에서 어떤 것을 띄워주고 각 페이지마다 이름을 어떻게 정하는지 한눈에 볼 수 있으면 코드의 가독성을 올라가고 유지보수는 좀 더 좋아질 것이다.

 


 

문제점

 

두 번째 코드처럼 pushNamed(-----) 같이 사용하면 정상적으로 화면 전환이 된다. 하지만 이와 비슷하지만 이전 페이지를 가져오는 기능이 있다. 정확히는 현재 페이지를 pop 하여 이전 페이지를 띄워주는 것이다.

onTap(){
    Navigator.popUntil(
    context, ModalRoute.withName('/Page2'));
    }

이렇게 사용을 할 수가 있다. 하지만 나의 RouteGenerator로 했을 때는 검은 화면만 뜬다(Black Screen).

분명 pushNamed 할 때는 정상적으로  화면 전환이 되었는데, 왜 popuntil은 되지 않는 것일까? 당연하게도 정의를 해 준 page이름으로만 popUntil을 해줬다.

popUntil, 내가 원하는 위치까지만 pop을 한다는 뜻이다. 그렇다면 이름이 동일하지 않다면 계속 pop이 된다는 뜻인데, 내가 RouteGenerator에서 정의한 이름이 무언가 잘못되어서 Navigator Stack에서 끝까지 pop이 되어 검정화면이 뜬다는 이야기가 된다. 분명 등록을 했는데 안 되는 이유가 뭘까?

 

해결

 

그럼 반대로 popUntil을 사용하려면 어떻게 해야 할까? Flutter 공식문서를 찾아봤다.

https://api.f lutter.dev/flutter/widgets/Navigator/popUntil.html

 

popUntil method - Navigator class - widgets library - Dart API

void popUntil (BuildContext context, RoutePredicate predicate ) Calls pop repeatedly on the navigator that most tightly encloses the given context until the predicate returns true. The predicate may be applied to the same route more than once if Route.will

api.flutter.dev

void _logout() {
  Navigator.popUntil(context, ModalRoute.withName('/login'));
}

popUntil은 단순히 Page이름만 적는 것이 아니라 ModalRoute.withName 이 포함되어있다. 확실히 pushNamed에서 Page이름만 parameter로 보내주는 것과 다르다. 그렇다면 Page이름을 어떻게 ModalRoute에 저장을 할까? 저장할 수 있다면 popUntil을 사용할 수 있을 것이다.

 

https://stackoverflow.com/questions/54525086/how-to-use-navigator-popuntil-flutter

 

How to use Navigator.popUntil Flutter

I m doing a Flutter app and I would like to go back from page 4 to page 1. I have an error really strange : Bad state : Future already completed I created a simple project to reproduce this bu...

stackoverflow.com

popUntil을 사용하는 방법을 StackOverflow에서 찾아보았다. 위의 링크에서 두 번째 답변을 보면 알 수 있다.

   Navigator.of(context).push(
            MaterialPageRoute(
              settings: RouteSettings(name: "/Page1"),
              builder: (context) => Page1(),
            ),
          );

Navigator push를 할 때 parameter setting에 RouteSettings(name: ----)를 선언해서 사용한다고 한다. 확실히 RouteGenerator와 다르게 사용을 한다. 따라서 맨 위의 코드에 마지막 부분을 수정해 주면 된다.

 return CurrentOSCheck.instance['Android']
        ? MaterialPageRoute(
            settings: RouteSettings(name: settings.name.toString()),
          //settings: RouteSettings(name: '/'+_pageName) 와 동일
            builder: (context) => _pageWidget)
        : CupertinoPageRoute(
            settings: RouteSettings(name: settings.name.toString()),
            builder: (context) => _pageWidget,
          );
  }

실행하니 정상적으로 popUntil이 된다.


결론

  1. MaterialApp 혹은 CupertinoApp의 Parameter, onGenerator에 RouteGenerator을 선언했다고 popUntil이 되는 것이 아니다. 정확히는 ModalRoute.withName에 저장이 되지 않는다.
  2. ModalRoute.withName으로 Page이름을 적기 위해서는 Navigator push 할 때 MaterialPageRoute 또는 CupertinoPageRoute의 Parameter, setting에 정의를 해 주어야 한다.
  3. 아래의 코드를 참고하면 더욱 이해하기가 쉬울 것이다.
   //Instead of
   //Navigator.of(context).push(
   //         MaterialPageRoute(
   //          builder: (context) => Page1(),
   //         ),
   //       );
   //
   //Use this
   Navigator.of(context).push(
           MaterialPageRoute(
           settings: RouteSettings(name: "/Page1"),
           builder: (context) => Page1(),
		),
   );
   
   //Then
   Navigator.of(context)
              .popUntil(ModalRoute.withName("/Page1"));
              
//https://stackoverflow.com/questions/54525086/how-to-use-navigator-popuntil-flutter

오류, 지적사항 그리고 궁금한 것은 댓글 부탁드립니다.

 

728x90