Study Record

[Flutter] 화면 전환하기 (Navigator) 본문

Flutter

[Flutter] 화면 전환하기 (Navigator)

초코초코초코 2023. 2. 11. 20:23
728x90

🎁 Navigator Class

Navigator 위젯은 스택 규칙(Last In First Out)에 따라 위젯들을 관리한다. 모바일 앱은 '화면', 혹은 '페이지'라 불리는 전체 화면 요소가 있는데 이걸 Flutter에서는 Route라고 부른다. Navigator는 이 Route 객체를 스택으로 관리한다. 앱에서 우리가 보는 화면은 Route Stack의 최상단에 있는 Route이고 다른 화면으로 전환하는 것은 Route Stack에 전환할 화면의 정보가 담긴 Route를 추가(push) 한 것이다. Route Stack에서 최상단에 있는 Route를 제거하면(pop) 그다음 상위 Route로 전환되면서 이전 화면으로 되돌아간다.

 

HomeScreen → TestScreen → FinalScreen 순으로 추가됐으면 pop 으로 제거하면 FinalScreen 이 없어지고 TestScreen, HomeScreen 순으로 사라진다.(마지막에 추가된 요소가 제일 먼저 나간다.)

 

Route는 일반적인 화면뿐 아니라 다이얼로그나 대화상자 등을 포함한다. 따라서 화면에 다이얼로그가 띄워졌다면 Route Stack에 다이얼로그 정보가 담긴 Route 가 추가(push)된 것이다.

 

void main() {
  runApp(const MaterialApp(home: MyAppHome()));
}

MaterialApp()의 home 인자는 Route Stack에 MyAppHome() 정보가 포함된 Route 가 추가하는 역할을 한다. 따라서 앱을 실행했을 때 첫 페이지는 MyAppHome 화면이 된다.

 

 

😶 push

push 함수로 Route Stack 에 push 할 수 있다. 즉, 화면 전환을 할 수 있다.

Ex.) TestScreen 으로 전환

Navigator.of(context).push(
  MaterialPageRoute(builder: (BuildContext context){
    return TestScreen();
  })
);

가고 싶은 화면으로 전환할 때는 Navigator.of(context).push(...) 에서 원하는 화면 위젯(TestScreen)을 넣으면 된다.

 

※ Navigator.of(context) 의 의미는 context와 관련된 상위 위젯트리의 Navigator를 가져온다는 의미이다.

 

 

1. settings (값 넘겨주기)

Navigator.of(context).push<int>(MaterialPageRoute(
  builder: (BuildContext context) {
    return TestScreen();
  },
  settings: const RouteSettings(
    // arguments: true
    /*arguments: {
      "firstArgument": true,
      "twoArgument": 45,
      "threeArgument": "세번재 인자"
    }*/
    arguments: TestArgument(false, 200)
  )
));

// arguments 예시를 위한 class
class TestArgument{
  bool argumentBool = true;
  int argumentInt = 333;

  TestArgument(this.argumentBool, this.argumentInt);
}

MaterialPageRoute 의 settins에 RouteSettings의 arguments(Object?)에 값을 넣어주면 된다. arguements에 들어가는 값은 Object? 이므로 bool, int, String, dictionary, List 등 어떤 타입도 가능하다.

 

이렇게 전달한 값를 화면에서 ModalRoute.of(context)?. settings.argument로 받아 사용하면 된다.

class TestScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    final argument =  ModalRoute.of(context)?.settings.arguments as TestArgument?;

    print("argument : ${argument?.argumentInt} ${argument?.argumentBool}");
    
    return Scaffold(
      body: ...
    );
  }

 

 

2. pushNamed(named navigator routes)

경로의 이름을 미리 설정해서 화면 전환을 시도할 수 있다.

void main() {
  runApp(MaterialApp(
    home: const MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
        '/a': (BuildContext context) => TestScreen(textWidget: Text('page A')),
        '/b': (BuildContext context) => TestScreen(textWidget: Text('page B')),
        '/c': (BuildContext context) => TestScreen(textWidget: Text('page C')),
    },
  ));
}

home 이라고 되어있는 MyAppHome()의 경로 이름은 '/'이다. 미리 MaterialApp()의 routes 인자에 k/ey, value 형식으로 key는 경로 이름 value는 경로 정보를 저장해 놓고,

Navigator.of(context).pushNamed(String routeName, {Object? arguments});

// ex)
Navigator.of(context).pushNamed("/a", arguments: true);

pushNamed에 미리 설정한 경로이름(key)으로 화면전환할 수 있다. agruments 인자로 똑같이 값을 전달할 수 있다. 화면에서 값을 받아 사용할 때 똑같이 ModalRoute.of(context)?. settings.arguments를 사용한다.

class TestScreen extends StatelessWidget {
  Widget textWidget;
  TestScreen({required this.textWidget, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final argument =  ModalRoute.of(context)?.settings.arguments as TestArgument?;

    print("argument : ${argument?.argumentInt} ${argument?.argumentBool}");
    
    return Scaffold(
      body: ...
    );
  }

 

 

😶 pop

Navigator를 이용해 Route Stack에서 최상위 Route를 제거하러면(이전 화면으로 돌아가기) Navigator.of(context). pop()을 사용하면 된다.

Navigator.of(context).pop();

 

※ 예시 (HomeScreen <--> TestScreen의 경우)

> main()

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

 

> HomeScreen()

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
        child: ElevatedButton(
          onPressed: (){
            onStartTestScreen(context);
          },
          child: const Text("TestScreen 전환"),
        ),
      )),
    );
  }
  
  void onStartTestScreen(BuildContext context) {
    Navigator.of(context).push(
      MaterialPageRoute(builder: (BuildContext context){
        return TestScreen();
      })
    );
  }
}

 

> TestScreen()

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
          child: Center(
            child: ElevatedButton(
              onPressed: (){
                onBackPressed(context);
              },
              child: const Text("뒤로 가기"),
            ),
          )),
    );
  }

  void onBackPressed(BuildContext context) {
    Navigator.of(context).pop();
  }
}

 

 

😶 pop으로 이전 화면으로 돌아갈 때 값 전달하기

현 화면에서 이전 화면으로 돌아갈 때, 현 화면에서 pop()에 값을 전달하면 Navigator.of(context). push(...)의 리턴값으로 돌려받을 수 있다. pop에 전달할 값은 int, bool, List 등 어떤 타입도 가능하다.

// HomeScreen -> TestScreen
// TestScreen()
class TestScreen extends StatelessWidget {
  int param;
  TestScreen({required this.param, Key? key}) : super(key: key);
  
  ......

  void onBackPressed(BuildContext context) {
    // 이전 화면에 인자 전달하기
    Navigator.of(context).pop(156);
    // Navigator.of(context).pop(TestArgument(true, 200));
  }
}

// HomeScreen()
class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  ......
    
  void onStartTestScreen(BuildContext context) async {
    // 이전 값 받기
    final resultValue = await Navigator.of(context).push<int>(
      MaterialPageRoute(builder: (BuildContext context){
        return TestScreen(param: 145);
      })
    );
    
    print("TestScreen 에서 받은 값 : $resultValue");
  }
}

 

여기서 onStartTestScreen()을 async 키워드로 비동기로 실행하고 await 키워드로 resultValue 값을 받지 않으면 TestScreen()에서 return 값으로 주는 156을 받을 수 없다. TestScreen()을 실행하고 다시 HomeScreen()으로 돌아오기 전에 resultValue 값이 결정되고 print() 함수가 실행되기 때문이다.

resultValue는 push <intt>에서 int로 값을 받을 것이라고 알 수 있지만 TestScreen()에서 return 값을 안 줄 수도 있다. 따라서 resultValue는 null 이 될 수도 있다.

 

🎁 여러 가지 push, pop

 

😶 pushReplacement() , pushReplacementNamed()

기본 push 연산은 Route Stack에 추가하는 것으로 HomeScreen에서 TestScreen으로 push 하면 경로가 HomeScreen → TestScreen 으로 쌓이고 현재 화면인 TestScreen에서 pop 하면 HomeScreen으로 돌아온다. 하지만 pushReplacement는 현재 화면을 대체한다. 따라서 HomeScreen → TestScreen이고 현재 화면이 TestScreen 상태에서 TestReplaceScreen으로 pushReplacement를 하면 HomeScreen → TestReplaeScreen 이 되는 셈이다.

Navigator.of(context).pushReplacement(
  MaterialPageRoute(
    builder: (_) => RouteThreeScreen(),
  ),
);

 

pushReplacementNamed() 도 pushReplacement()와 같은 기능을 하지만 Named 방식을 사용하는 것만 다르다.

Navigator.of(context).pushReplacementNamed('/three');

 

 

😶 pushAndRemoveUntil(), pushNamedAndRemoveUntil()

pushAndRemoveUntil(Route  newRoute, bool Function(Route ) predicate)

 

새로운 화면으로 전환할 때 predicate에 의해 현재 Route Stack의 최상위 Route을 검사하여 false 이면 삭제하고 그다음 Route를 검사하여 true를 리턴하거나 Route Stack에 요소가 없을 때까지 반복한다.

 

예를 들어, 

Navigator.of(context).pushAndRemoveUntil(
  MaterialPageRoute(builder: (_) => TestScreen2()),
  (route) => false
);

이런 식으로 모든 route를 false로 return 하게 되면 TestScreen2()만 Route Stack에 남게 된다. 따라서 TestScreen2에서 뒤로 가기 버튼을 누르면 바로 종료된다.

 

최초 화면이자 home 이 MyAppHomeScreen이고 Route Stack 이 MyAppHomeScreen → T1 Screen → T2 Screen 일 때, 현재 화면인 T2 Screen에서 다음과 같이 pushAndRemoveUntil()을 하면,

Navigator.of(context).pushAndRemoveUntil(
  MaterialPageRoute(builder: (_) => T3Screen()),
  (route) => route.settings.name == '/',
);

Route Stack 은 MyAppHomeScreen →T3 Screen 이 된다. home의 경로는 '/' 이기 때문에 route.settings.name 이 home 일 때만 true이고 나머지는 false 이어서 MyAppHomeScreen 만 남고 다 삭제된다.

 

pushNamedAndRemoveUntil의 경우 새로운 화면에 대한 정보를 미리 설정해 둔 경로이름을 사용하는 것만 다르다.

void main() {
  runApp(MaterialApp(
    home: const MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
        '/a': (BuildContext context) => TestScreen(),
        '/b': (BuildContext context) => TestScreen2(),
        '/c': (BuildContext context) => TestScreen3(),
        '/d': (BuildContext context) => TestScreen4(),
    },
  ));
}

경로 이름이 위와 같고, MyAppHome() → TestScreen() → TestScreen2() → TestScreen3() → TestScreen4()로 화면전환한다고 할 때, 

// TestScreen3()
Navigator.of(context).pushNamedAndRemoveUntil("/d", (route) {
  if(route.settings.name == "/a") return true;
  return false;
});

TestScreen3 이 위와 같다면, TestScreen4 화면으로 진입했을 때 Route Stack 은 MyAppHome → TestScreen() →  TestScreen4()가 될 것이다. (TestScreen3 = false , TestScreen2 = false, TestScreen = true)

 

 

😶 maybePop()

Navigator.of(context).maybePop();

maybePop() 은 pop()을 할 수 있을 때는 하고 하지 못하는 경우에는 하지 않는다. 하지 못하는 경우에는 Route Stack 에 현재 화면밖에 없을 때가 될 수 있다. 첫 화면인데 pop() 을 한다면 Route Stack 에는 아무것도 없어 검은 화면이 보일 수 있다.

 

 

😶 canPop()

Navigator.of(context).canPop();

pop() 이 가능하면 true 불가능하면 false을 리턴한다. canPop() 은 pop()을 하지 않고 pop() 이 가능한지만 알려준다.

 

 

+참고 사이트

 

Navigator class - widgets library - Dart API

A widget that manages a set of child widgets with a stack discipline. Many apps have a navigator near the top of their widget hierarchy in order to display their logical history using an Overlay with the most recently visited pages visually on top of the o

api.flutter.dev

 

728x90