4 minute read

1. Navigator

Flutter에서 페이지는 routes라고 하는데, 이 routes를 이동할 때 Navigator의 push와 pop 기능을 이용해 이동한다. push와 pop을 가지고 페이지를 이동하는 방법과 각각의 유용한 옵션을 알아보자.

  • Navigator.push() : 다음 routes로 이동
  • Navigator.pop() : 이전 routes로 이동



2. 화면 이동 해보기

두 페이지간의 이동 예제를 간단하게 알아보자.
RouteOne, RouteTwo 화면을 만들고 각각 아래와 같이 StatelessWidget을 생성한다. 각 페이지 별로 버튼은 만들어서 RouteOne에서는 Navigator.push()를 사용해서 다음 routes로 넘어가도록, RouteTwo에서는 Navigator.pop()을 사용해서 이전 routes로 넘어가도록 코드를 작성해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// routes one
class RouteOne extends StatelessWidget {
  const RouteOne({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Route One'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
            ElevatedButton(
                onPressed: () {
                    Navigator.of(context).push();
                },
                child: Text('Push'),
            ),
        ],
      ),
    );
  }
}

// routes two
class RouteTwo extends StatelessWidget {
  const RouteTwo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Route Two'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
            ElevatedButton(
                onPressed: () {
                    Navigator.of(context).pop();
                },
                child: Text('Pop'),
            ),
        ],
      ),
    );
  }
}
  • Tip 공통코드 정리하기

위 코드를 보면 RouteOne과 RouteTwo의 구조가 똑같이 생긴 것을 알 수 있다. 지금은 2개의 페이지를 예로 들었지만 프로젝트를 진행하면서 동일한 코드가 여기저기 쓰일 수 있는데 이럴 때 코드를 좀 더 간결하고 보기 좋게 정리하는 방법을 알아보자.

위에서 예시로 든 코드를 보면 Scaffold 이하 영역을 공통 처리할 수 있다. 우선 코드를 정리할 .dart 파일을 하나 생성한다.

페이지명이 될 title과 처리부분인 children 부분을 변수로 받아주고 공통 부분 코드를 작성하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//common_layout.dart
import 'package:flutter/material.dart';

class CommonLayout extends StatelessWidget {
  final String title;
  final List<Widget> children;

  const CommonLayout({required this.title, required this.children, super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: children,
      ),
    );
  }
}

이렇게 공통 코드 정리가 끝나면 기존의 코드를 아래와 같이 수정이 가능하다. 공통 코드를 정리한 CommonLayout을 호출하고 변수로 설정한 title과 children에 넘겨줄 코드를 작성해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//route one
class RouteOne extends StatelessWidget {
  const RouteOne({super.key});

  @override
  Widget build(BuildContext context) {
    return CommonLayout(
      title: 'Route One', 
      children: [
        ElevatedButton(
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(
                builder: (_) => RouteTwo(),
              ),
            );
          },
          child: Text('Push'),
        ),
      ],
    );
  }
}

//route two
class RouteTwo extends StatelessWidget {
  const RouteTwo({super.key});

  @override
  Widget build(BuildContext context) {
    return CommonLayout(
      title: 'Route Two', 
      children: [
        ElevatedButton(
          onPressed: () {
            Navigator.of(context).pop();
          },
          child: Text('Pop'),
        ),
      ],
    );
  }
}



3. push(), pop()의 옵션

Flutter의 routes 구조는 스택(Stack) 구조로 이루어져있다. 스택 구조는 새로운 페이지가 기존 페이지 위로 불려오는 것으로 새로운 페이지가 불려올 때마다 차곡차곡 쌓여 있다는 뜻이다. 그렇기 때문에 pop()을 호출하면 현재 페이지 아래에 있는 스택의 페이지가 불려오는 것이다.

여기서 push()와 pop()의 옵션을 알아보고 routes의 스택 구조를 확인해보자.


3.1 push() 옵션

3.1.1 pushReplacement()

pushReplacement() 옵션은 새로 불려올 routes의 스택을 현재 routes의 스택으로 대신하는 옵션이다.

이게 무슨 말이냐면 [Home, RouteOne, RouteTwo] 이렇게 3개의 페이지가 있다고 가정하자. 여기서 RouteOne 페이지에서 pushReplacement()를 사용해서 RouteTwo 페이지를 호출하면 스택은 [Home, RouteTwo]만 남게 된다.

기존 페이지였던 RouteOne의 자리에 RouteTwo 들어오게 되는 것이다. 따라서 이 상태에서는 RouteTwo에서 pop()을 할 경우 Home으로 돌아가게 된다.

다른 코드는 위와 동일하니 버튼의 onPressed 영역만 코드를 통해 확인해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
//Home에서 RouteOne을 호출한 상태라고 가정한다.

//route one
 ElevatedButton(
    onPressed: () {
        Navigator.of(context).pushReplacement(
            MaterialPageRoute(
                builder: (_) => RouteTwo(),
            ),
        );
    },
    child: Text('Push ReplaceMent'),
)


3.1.2 pushAndRemoveUntil()

pushAndRemoveUntil() 옵션은 이전 routes 스택들을 전부 지우거나 원하는 스택을 찾아 그 지점까지 스택을 지울 수 있다.

듣기만 해선 잘 이해가 가지 않을 수 있으니 코드와 함께보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//route one
ElevatedButton(
    onPressed: () {
        // [Home(), RouteOne(), RouteTwo()]
        // true 인경우 모든 스택 유지, false인 경우 모든 스택 삭제
        Navigator.of(context).pushAndRemoveUntil(
            MaterialPageRoute(
                builder: (_) => RouteTwo(),
            ),
            (route) => true,        //true, false
        );
    },
    child: Text('Push And Remove Until'),
)

위 코드를 보면 pushAndRemoveUntil()를 사용해서 RouteTow 페이지를 호출하고 있다. 이 때 (route) => true 일 경우 RouteTwo 페이지가 호출되며 이전의 스택이 모두 유지 되지만 (route) => false 일 경우 RouteTwo 페이지가 호출되며 이전의 스택은 모두 삭제된다.

이 옵션은 Route를 적용하게 되면 원하는 페이지의 스택을 찾아 그 지점부터 호출한 페이지의 스택 사이의 모든 스택을 삭제할 수 있는데, Route를 적용하면 어떻게 달라지는지 다음에 확인해보도록 하자.


3.2 pop() 옵션

3.2.1 maybePop()

maybePop()은 뒤로 돌아갈 스택이 없는 경우 페이지 이동을 하지 않는 옵션이다.
예를 들어, Flutter에서는 첫 페이지에서 pop()을 실행하면 아무것도 없는 검은 화면이 보이게 된다. 돌아갈 스택이 없는데 pop()을 실행해서 나타나는 현상이다.

이 때 maybePop()을 사용하면 돌아갈 스택이 없어서 페이지가 이동되지 않고 현재 페이지에 머무르게 된다.

1
2
3
4
5
6
7
//home
ElevatedButton(
    onPressed: () {
        Navigator.of(context).maybePop();
    },
    child: Text('Maybe Pop'),
)


3.2.2 canPop()

canPop()은 pop()이 가능한지에 대한 여부를 반환하는 옵션이다. print로 canPop()을 감싸서 확인해보자.

1
2
3
4
5
6
7
8
//home
ElevatedButton(
    onPressed: () {
        //뒤로 돌아갈 수 있는 경우 true, 없는 경우 false 반환
        prtin(Navigator.of(context).canPop());
    },
    child: Text('Maybe Pop'),
)