HyunJun 기술 블로그

Navigation Pattern, Navigator, Named Routes, Parameters 본문

Flutter

Navigation Pattern, Navigator, Named Routes, Parameters

공부 좋아 2023. 7. 11. 14:50
728x90
반응형

1. Navigation

플러터의 Navigation은 사용자 인터페이스(UI) 간의 화면 전환과 화면을 관리하는 방법을 말한다. 플러터에서의 Navigation 방식은 아래와 같은 방식이 존재한다. 이 글에서는 아래의 빨간색 부분만 기술하며, 다음 글부터 차례차례 기술하려고 한다. 참고로 이외에도 여러 가지 Navigation이 추가로 존재한다.

 

  • Navigator
  • Named Routes
  • Parameters
  • Navigation Rail
  • Navigation Drawer
  • Tab Navigation,
    • TabBar, BottomNavigationBar
  • PageView:

 

1-1. Navigator

Navigator는 플러터에서 화면 전환을 관리하는 객체로. 새로운 화면을 스택에 추가(push) 하여 전환하거나, 스택에서 최상위 화면을 제거(pop) 하여 이전 화면으로 돌아가는 등의 기능을 제공한다. 이렇게 Navigator를 사용하여 화면을 전환하면서 스택에 화면이 쌓이게 되는데, 이것이 플러터의 기본적인 Navigation 방식인 스택 방식(또는 스택 기반 내비게이션)이다.

 

1) Navigator.of(context)

현재 위젯의 BuildContext를 통해 Navigator를 얻을 수 있다. 즉, Navigator.of(context)는 현재 위젯의 context(BuildContext)를 통해 Navigator를 객체를 참조할 수 있도록 가지고 오는 것이다.

 

2) push(MaterialPageRoute())

플러터에서 제공하는 MaterialPageRoute 클래스는 머티리얼 디자인 스타일의 화면 전환을 담당하는 클래스이다. 머티리얼 디자인은 구글이 개발한 디자인 시스템으로, 플러터에서 기본적으로 사용되는 디자인 스타일이다. builder 매개 변수는 BuildContext를 입력받으며, BuildContext는 위젯 트리에서 위젯의 위치를 나타내는 객체이다. 화살표 함수 뒤에는 새로운 화면으로 전환될 위젯을 작성하면 된다.

 

즉 MaterialPageRoute를 생성하면서 머티리얼 디자인 스타일의 화면 전환을 설정하고, builder 매개변수를 사용하여 전환할 위젯을 새로운 화면으로 전환하는 역할을 한다. RouteOneScreen은 전환되는 새로운 화면의 내용을 구성하는 위젯으로, 이를 생성하고 반환하는 함수가 builder에 전달되어 새로운 화면이 생성되고 표시된다. 마지막에 해당 MaterialPageRoute를 Navigator 객체에 Push 하는 것이다.

 

아래의 코드는 버튼을 눌렀을 때 RouteOneScreen() 위젯이 현재 context의 Navigator 객체 stack의 맨 뒤에 push 되어서 화면이 전환되는 코드이다.

    ElevatedButton(onPressed: (){
      Navigator.of(context).push(
        MaterialPageRoute(
            builder: (BuildContext context) => RouteOneScreen())
      );
    }, child: Text("push"))

 

3) pop()

pop 같은 경우 기본적으로 스택의 마지막에 입력된 것을 꺼내오기 때문에, pop()을 사용하면 바로 이전의 페이지로 이동(뒤로 가기) 하게 된다. 예를 들어 아래와 같은 코드의 경우 이동된 RouteOneScreen에서 작성된 pop() 코드이다.

  ElevatedButton(onPressed: (){
    Navigator.of(context).pop();
  }, child: Text("pop"))

 

기본적으로 Navigator에 stack이 쌓여있기 때문에, 안드로이드는 뒤로 가기 버튼, 아이폰의 경우 왼쪽에서 쓸어넘기기를 사용하면 뒤로 가기, 즉 pop()과 같은 역할을 수행할 수 있다.

 

4) Parameters.

  • 상위 부모 위젯에서 하위 자식 위젯에게 파라미터를 전달할 수 있고,
  • 반대로 하위 자식 위젯에서 상위 부모 위젯에게 파라미터를 전달할 수 있다.

 

부모 -> 자식

부모 위젯에서 해당 자식 위젯의 매개 변수로 데이터 전달.

Navigator.of(context).push(
            MaterialPageRoute(
                builder: (BuildContext context) => RouteOneScreen(
                  number: 123,
                ))
        );

 

자식 위젯에서는 필드에 number를 추가하고, 생성자로 받아서 사용하면 된다.

class RouteOneScreen extends StatelessWidget {
  final int number;
  const RouteOneScreen({required this.number, super.key});

  @override
  Widget build(BuildContext context) {
    return MainLayout(title: "Route One", children: [
      Text(number.toString()),
      ElevatedButton(
          onPressed: () {
            Navigator.of(context).pop();
          },
          child: Text("pop"))
    ]);
  }
}

 

 

자식 -> 부모

하위 자식 위젯에서 상위 부모 위젯에게 파라미터 전달하는 방법은 스택을 제거할 때 즉, pop()을 호출하는 곳에서 매개 변수로 데이터를 넣어준다.

Navigator.of(context).pop(456);

 

스택이 빠지고, 이동된 위젯에서 asyn await를 써서 변수에 값을 받아 활용한다. 아래의 예는 단순 StatelessWidget이고, 이것으로 위젯의 상태를 변경하고 싶다면 StatefulWidget의 setState를 활용하면 된다.

  ElevatedButton(onPressed: () async{
    final result = await Navigator.of(context).push(
        MaterialPageRoute(
            builder: (BuildContext context) => RouteOneScreen(
              number: 123,
            ))
    );
    print(result);
  }, child: Text("push"))

 

 

 

5) arguments로 전달하기.

push의 매개 변수인 settings의 값에 RouteSettings(arguments: value,)를 사용하면 push 할 때 데이터를 넘길 수 있다.

Navigator.of(context).push(
  MaterialPageRoute(builder: (_) => RouteTwoScreen(),
  settings: RouteSettings(
    arguments: 789
  ))
);

이때 arguments를 받는 위젯에서는 ModalRoute.of(context)!.settings.argumetns를 통해 받는다.

  @override
  Widget build(BuildContext context) {
    final arguments = ModalRoute.of(context)!.settings.arguments;

    return MainLayout(title: "Route Two", children: [
      Text('arguments: ${arguments}'),
      ElevatedButton(onPressed: (){
        Navigator.of(context).pop();
      }, child: Text("Pop"))
    ]);
  }

 

 

2. Named Route

아래의 코드처럼 기존 main.dart 파일의 home에 위젯(클래스)을 생성하는 게 아닌,

void main() {
  runApp(MaterialApp(
    home: HomeScreen(),
  ));
}

 

아래의 코드처럼 routes (Map 구조)를 사용하여 라우트 이름과, 그에 해당하는 위젯 클래스를 설정해 주면 앱 전역에서 사용할 수 있다.

const HOME_ROUTE = "/";

void main() {
  runApp(MaterialApp(

    // 초기 Route 설정(처음 실행 시 라우트)
    initialRoute: HOME_ROUTE,

    routes: {
      HOME_ROUTE: (context) => HomeScreen(),
      "/one": (context) => RouteOneScreen(),
      "/one/two": (context) => RouteTwoScreen(),
      "/one/two/three": (context) => RouteThreeScreen(),
    },
  ));
}

 

1) pushNamed

routes 방식에서는 pushNamed()를 사용하여 라우트 네임으로 push를 해야 한다. 이때 라우트 네임 다음의 매개변수로 argumetns를 사용하여 값을 전달할 수 있다.

  ElevatedButton(
      onPressed: () {
        Navigator.of(context).pushNamed("/one/two/three", arguments: 999);
      },
      child: Text("Push Named"))

데이터 받는 법은 위에서 사용했던 것처럼 ModalRoute.of(context).settings.argumetns를 통해 받을 수 있다. pop()의 경우에는 기존과 똑같이 사용하면 된다.

 

 

3. push

1) pushReplacement

push 할 때, 현재 페이지를 push 할 페이지로 교체한다. 즉, 기존에는 [screen1, screen2, screen3]의 스택 구조에서 screen3을 pop 하면 screen2가 보여젔지만, screen2 -> screen3에서 pushReplacement로 push를 하면 screen3을 pop 할 때 [screen1, screen2, screen3]의 형태가 된다. 즉, screen3에서 pop() 하면 screen1로 돌아간다.

  ElevatedButton(onPressed: (){
    Navigator.of(context).pushReplacement(
      MaterialPageRoute(builder: (_) => RouteThreeScreen())
    );
  }, child: Text("Push Replacement"))

 

Named Route를 활용하는 pushReplacementNamed도 존재한다.

  ElevatedButton(onPressed: (){
    Navigator.of(context).pushReplacementNamed(
      '/one/two/three'
    );
  }, child: Text("Push Replacement Named"))

 

2) pushAndRemoveUntil

스택에 push 후 이전 스택에 남아있던 라우트들을 제거할지 그대로 둘지 하나하나 설정할 수 있다. (route)에는 모든 라우트(스택)들이 담겨있다. 이 라우트들을 일일이 확인하여 true를 리턴하면 스택에 살려두고 false를 리턴하면 스택에서 제거한다. 즉 현재 아래의 코드는 RouteThreeScreen()으로 이동한 뒤, 이전의 route들이 전부 삭제되어 pop을 하게 되면 검은 화면이 나오게 된다.

  ElevatedButton(
      onPressed: () {
        Navigator.of(context).pushAndRemoveUntil(
            MaterialPageRoute(builder: (_) => RouteThreeScreen()),
            (route) => false);
      },
      child: Text("Push And Remove Until"))

 

간단한 예시로 아래의 코드는 해당하는 라우트 네임이 있을 경우 해당 라우트만 true로 적용된다. 즉 RouteThreeScreen()으로 이동한 뒤 pop을 하면 "/"의 이름을 가지고 있는 Home 라우트로 이동한다.

  ElevatedButton(
      onPressed: () {
        Navigator.of(context).pushAndRemoveUntil(
            MaterialPageRoute(builder: (_) => RouteThreeScreen()),
            (route) => route.settings.name == '/');
      },
      child: Text("Push And Remove Until"))

 

이 역시, pushNameAndRemoveUntil을 사용할 수 있다.

  ElevatedButton(
      onPressed: () {
        Navigator.of(context).pushNamedAndRemoveUntil(
            '/one/two/three',
            (route) => route.settings.name == '/');
      },
      child: Text("Push Named And Remove Until"))

 

 

4. pop

1) maybePop

현재, 스택의 가장 처음에 위치해 있을 때 pop 메서드를 호출하면 스택이 전부 사라지며, 검은 화면이 뜨게 된다. 하지만 이 maybePop은 현재 위젯이 스택의 처음일 경우에는 maybePop을 호출하면 뒤로 가지 않고 화면전환을 하지 않는다. 만약 스택의 처음이 아닐 때 maybePop을 동작시키면 기존의 pop처럼 잘 동작한다. 즉, 검은 화면(스택의 완전한 제거)을 방지한다.

  ElevatedButton(onPressed: (){
    Navigator.of(context).maybePop();
  }, child: Text("Maybe Pop")),

  ElevatedButton(onPressed: (){
    Navigator.of(context).pop();
  }, child: Text("Pop")),

 

2) canPop

실제로 pop의 동작은 하지 않는다. 하지만 maybePop에서처럼 0번 스택인지(뒤로 갈 스택이 있냐 없냐)에 따라 true & false를 리턴해준다.

  ElevatedButton(onPressed: (){
    print(Navigator.of(context).canPop());
  }, child: Text("Can Pop")),

3) WillPopScope

스택의 젤 처음, 즉 메인 페이지에서 아이폰의 경우 Pop 버튼을 만들지 않는 이상 뒤로 가기를 할 수 없다. 하지만 안드로이드의 경우 기기에 있는 뒤로 가기 버튼을 누르게 되면 강제로 pop 했을 때처럼 검은 화면이 뜨는 것이 아니라 앱이 종료하게 된다. 이 WillPopScope를 사용하면 이렇게 앱이 종료되는 것을 막을 수 있다.

  return WillPopScope(
    onWillPop: () async {
      // true = pop 가능
      // false = pop 불가능
      return false;
    },
    child: Scaffold(
     body: Container(
       child: Text("WillPopScope"),
     ),
    ),
  );

하지만 이것조차도 pop을 명시적으로 만들었을 때 해당 pop을 호출하여 강제로 뒤로 가는 경우 검은 화면이 뜬다.

728x90
반응형
Comments