티스토리 뷰

지난 번에는 Navigator.push()를 통해 단순하게 화면을 넘기고, 데이터를 넘겨받는 방법을 알아봤다면,

오늘은 Navigator.pushNamed()로 화면을 넘기고, 그 때 데이터를 넘기고 넘겨받는 방법을 정리하려고 함!

 

 

공식 문서에 따르면 만약 앱의 다양한 부분에서 똑같은 화면으로 계속 네비게이트한다면, Navigator.push()를 통한 화면 전환은 코드 중복을 일으킬 수 있기 때문에 named route가 해결책이 될 수 있다고 함 

 

 

지난 번 글

https://day-of-soonjin.tistory.com/112

 

[Flutter] push, pop 화면 전환하며 데이터 넘겨 받는 방법

플러터에서 화면을 push, pop하면서 데이터를 넘겨받는 방법 1. push 할 때(1) 새로운 화면(SecondScreen)에 필수 파라미터를 설정하여 값을 넘기는 방법SecondScreen에 inputString이라는 필수 파라미터를 설

day-of-soonjin.tistory.com

 


 

Navigator.pushNamed()는 아래와 같은 파라미터를 받음!

말 그대로 라우트의 네임(String)으로 화면을 push하는 것 같음!

 

 

 

1. pushNamed를 통해 간단히 화면 전환부터 해보고

2. pushNamed를 통해 데이터를 넘기고

3. 넘어온 데이터를 받는 방법을 알아보겠음!

 

 


1.  pushNamed() 화면 전환

 

(1) push

pushNamed()를 쓰기 위해서는 route를 정의해줘야 함.

MaterialApp의 생성자에서 해주면 되는데, initialRouteroutes 속성을 이용할 예정

 

 

 

원래 코드 ⬇️에는 home 속성이 정의되어 있는데, 이 부분을 지우고, initialRoute와 routes 속성을 쓰면 됨

※ initialRoute 속성을 쓸 경우, home 속성은 정의하면 안됨!

더보기

원래 코드

※ initialRoute 속성을 쓸 경우, home 속성은 정의하면 안됨!

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      /// initialRoute 속성을 쓸 경우, home 속성은 정의하면 안됨!
      home: const MyHomePage(),
    );
  }
}

 

 

-> initialRoute는 말 그대로 첫번째 라우트라고 생각하면 됨.

home의 경우에는 앱이 실행되자마자 바로 표시되는 위젯인 반면, initialRoute는 문자열을 통한 네이게이션 라우트를 통해 화면을 표시하게 됨.

 

home은 네비게이션 스택이나 라우트와 직접적으로 연결되어 있지 않기 때문에 화면 간 이동을 제어하기가 어렵다고 함! 그에 비해 initialRoute는 유연성과 화면 간 이동 제어력이 뛰어나다고 함

 

 

-> routes 속성에는 각 String 값(routeName)에 맞는 스크린을 대응해주면 됨. 보통 이걸 route table이라고 부름

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      ///⭐️⭐️⭐️
      initialRoute: '/',
      ///⭐️⭐️⭐️ route table
      routes: {
        '/': (context) => const MyHomePage(),
        '/first': (context) => const FirstScreen(),
      },
    );
  }
}

 

 

이렇게 정의가 된 routes 들은 아래처럼 String 값으로 이동할 수 있음

OutlinedButton(
  onPressed: () {
    ///⭐️⭐️⭐️
    Navigator.pushNamed(context, '/first');
  },
  child: const Text('다음 화면으로')
)

 

 

 

※ 참고 : 아래처럼 String 값을 해당 화면 class 내부에 정의해서 사용하기도 함

//FirstScreen 내에 static 속성으로 선언
static const String routeName = '/first';

//routes 설정에 적용
routes: {
  '/': (context) => const MyHomePage(),
  FirstScreen.routeName: (context) => const FirstScreen(),
},

//push할 때도 사용
Navigator.pushNamed(context, FirstScreen.routeName);

 

 

 

2. pop

pop은 똑같이 해주면 됨!

Navigator.pop(context);

 

2~3. pushNamed를 통해 데이터를 넘기고 넘어온 데이터를 받는 방법을 알아보겠음!

  (1) Extract Arguments

  (2) Pass Arguments

 

 

다음 화면으로 데이터를 넘기는 데는 두 가지 방법이 있음

일단은 공식 문서에 따라 Extract Arguments 방법과 Pass Arguments라 부르겠음.

 

(1) Extract Arguments

 

pushNamed에도 push()와 동일하게 arguments 파라미터가 있어서 인자로 넘길 수 있음.

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('메인화면'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            OutlinedButton(
                onPressed: () {
                  Navigator.pushNamed(
                    context,
                    FirstScreen.routeName,
                    ///⭐️⭐️⭐️ 인자 보내기
                    arguments: {
                      'name': '윤순진',
                      'age': '23짤',
                    },
                  );
                },
                child: const Text('첫 번째 화면으로'))
          ],
        ),
      ),
    );
  }
}

 

 

받는 곳에서는 ModalRoute.of(context)!.settings.arguments로 받아서 사용함

class FirstScreen extends StatefulWidget {
  const FirstScreen({super.key});

  static const String routeName = '/first';

  @override
  State<FirstScreen> createState() => _FirstScreenState();
}

class _FirstScreenState extends State<FirstScreen> {
  @override
  Widget build(BuildContext context) {
  
  	///⭐️⭐️⭐️ 받은 인자 처리하기
    final arguments =
        ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
    final String name = arguments['name'] as String;
    final String age = arguments['age'] as String;

    return Scaffold(
      backgroundColor: Colors.green[200],
      appBar: AppBar(
        title: const Text('1번'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('이름: $name'),
            Text('나이: $age'),
          ],
        ),
      ),
    );
  }
}

 

 

 

요약하면, routes 속성에 필요한 화면들은 정의해주고 -> arguments 인자를 전달해 -> ModalRoute.of(context) ~~로 받으면 됨

 

 

※ 참고로 route table이 길어질수록 코드 가독성이 떨어질 수 있으니, 별도의 class로 관리하는 방법도 있음

import 'package:flutter/material.dart';
import 'package:navigator_route/main.dart';

class Routes {
  Routes._(); //더 이상 인스턴스 생성되지 않도록 막기

  static String main = '/';
  static String first = '/first';

  //위에 property로 선언되어 있어야만 아래에서 생성이 가능
  static final routes = <String, WidgetBuilder>{
    main: (context) => const MyHomePage(),
    first: (context) => const FirstScreen(),
  };
}

 

이제 만든 class를 route 속성에 할당해주면 됨

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      initialRoute: '/',
      routes: Routes.routes,
      },
    );
  }

 

사용할 때는 아래처럼.

OutlinedButton(
  onPressed: () {
    Navigator.pushNamed(
      context,
      Routes.first,
      arguments: {
        'name': '윤순진',
        'age': '23짤',
      },
    );
  },
  child: const Text('1. routes 정의된 화면'),
),

 

 


 

그런데, 만약 필수 파라미터를 넘겨야 한다면?

예를들어 두 번째 화면에 필수 파라미터가 있다면? pushNamed()로는 필수 파라미터를 넘길 수 없잖슴?

그리고 pushNamed로 네비게이트되었는데, 만약 route table에 해당 라우트 네임으로 등록된 게 없다면? 어디로 가야함?

 

 

이럴 때는 onGererateRoute 속성에 등록해주면 됨!

 

이게 바로 두 번째 방법!

(2) Pass Arguments

 

 

예를들어 아래와 같은 title, content가 필요한 SecondScreen이 있고,

class SecondScreen extends StatefulWidget {
  const SecondScreen({super.key, required this.title, required this.content});

  static const String routeName = '/second';

  ///⭐️ 필수 파라미터
  final String title;
  final String content;
  
  @override
  State<SecondScreen> createState() => _SecondScreenState();
}

class _SecondScreenState extends State<SecondScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.yellow[200],
      appBar: AppBar(
        title: const Text('2. routes에 정의되지 않은 화면'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('제목: ${widget.title}'),
            Text('내용: ${widget.content}'),
          ],
        ),
      ),
    );
  }
}

 

 

pushNamed()로 SecondScreen으로 arguments를 전달하며 네비게이트했다면 ?

OutlinedButton(
  onPressed: () {
  Navigator.pushNamed(
    context,
    SecondScreen.routeName,
      //⭐️arguments 전달
      arguments: {
        'title': '백설공주',
        'content': '백설기 먹고 싶다.',
      },
    );
  },
  child: const Text('2. routes에 정의되지 않은 화면'),
)

 

 

ongenerateRoute에서 해당 arguments를 받아주면 됨.

arguments로 넘겼던 인자값들도 여기서 처리해서 SecondScreen의 필수 파라미터의 인자로 넣어주면 됨

      onGenerateRoute: (settings) {
        if (settings.name == SecondScreen.routeName) {
          final arguments = settings.arguments as Map<String, dynamic>;
          final String title = arguments['title'] as String;
          final String content = arguments['content'] as String;

          return MaterialPageRoute(
            builder: (context) {
              return SecondScreen(title: title, content: content);
            },
          );
        }
        assert(false, 'Need to implement ${settings.name}');
    	return null;
      },

 

 

넘어온 데이터는 그냥 필수 파라미터로 넘어온 정보처럼 쓰면 됨

class SecondScreen extends StatefulWidget {
  const SecondScreen({super.key, required this.title, required this.content});

  final String title;
  final String content;
  
  @override
  State<SecondScreen> createState() => _SecondScreenState();
}

class _SecondScreenState extends State<SecondScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.yellow[200],
      appBar: AppBar(
        title: const Text('2. routes에 정의되지 않은 화면'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('제목: ${widget.title}'),
            Text('내용: ${widget.content}'),
          ],
        ),
      ),
    );
  }
}

 

 

 

routes은 정적인(named) 경로에 대한 화면을 매핑하는 곳에 사용되는 용도라면,

onGenerateRoute는 알려지지 않은 경로에 대한 화면을 동적으로 생성하는데 사용된다고 함!

 

 

모든 화면이 route table에 적혀있으면 좋겠지만, 사용자가 사용되지 않은 경로로 이동하려고 하는 경우나 딥 링크를 통해 앱으로 들어온 사용자가 알 수 없는 경로로 접근할 수도 있으니, 이런 경우 onGenerateRoute를 통해 처리하면 됨

 

 

추가적으로 onGenerateRoute 속성을 통해 만약 유저가 알 수 없는 경로로 들어올 경우를 대비할 수도 있음

OutlinedButton(
  onPressed: () {
    Navigator.pushNamed(
      context,
      '/unknown',
    );
  },
  child: const Text('3. 아무곳에도 정의되지 않은 화면'),
 )
      onGenerateRoute: (settings) {
        if (settings.name == SecondScreen.routeName) {
          debugPrint('settings name = ${settings.name}');
          final arguments = settings.arguments as Map<String, dynamic>;
          final String title = arguments['title'] as String;
          final String content = arguments['content'] as String;

          return MaterialPageRoute(
            builder: (context) {
              return SecondScreen(title: title, content: content);
            },
          );
        } else {
          ///⭐️⭐️ 정의된 어떤 케이스도 아닐 경우
          return MaterialPageRoute(
            builder: (context) => const UnknownScreen();,
          );
        }
      },

 

 


전체코드

더보기
import 'package:flutter/material.dart';
import 'package:navigator_route/routes.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      // home: const MyHomePage(),
      initialRoute: '/',
      routes: Routes.routes,
      onGenerateRoute: (settings) {
        if (settings.name == Routes.second) {
          debugPrint('settings name = ${settings.name}');
          final arguments = settings.arguments as Map<String, dynamic>;
          final String title = arguments['title'] as String;
          final String content = arguments['content'] as String;

          return MaterialPageRoute(
            builder: (context) {
              return SecondScreen(title: title, content: content);
            },
          );
        } else {
          return MaterialPageRoute(
            builder: (context) {
              return const UnknownScreen();
            },
          );
        }
      },
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('메인화면'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            OutlinedButton(
              onPressed: () {
                Navigator.pushNamed(
                  context,
                  Routes.first,
                  arguments: {
                    'name': '윤순진',
                    'age': '23짤',
                  },
                );
              },
              child: const Text('1. routes 정의된 화면'),
            ),
            OutlinedButton(
              onPressed: () {
                Navigator.pushNamed(
                  context,
                  Routes.second,
                  arguments: {
                    'title': '백설공주',
                    'content': '백설기 먹고 싶다.',
                  },
                );
              },
              child: const Text('2. routes에 정의되지 않은 화면'),
            ),
            OutlinedButton(
              onPressed: () {
                Navigator.pushNamed(
                  context,
                  '/unknown',
                );
              },
              child: const Text('3. 아무곳에도 정의되지 않은 화면'),
            )
          ],
        ),
      ),
    );
  }
}

class FirstScreen extends StatefulWidget {
  const FirstScreen({super.key});

  @override
  State<FirstScreen> createState() => _FirstScreenState();
}

class _FirstScreenState extends State<FirstScreen> {
  @override
  Widget build(BuildContext context) {
    final arguments =
        ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
    final String name = arguments['name'] as String;
    final String age = arguments['age'] as String;

    return Scaffold(
      backgroundColor: Colors.green[200],
      appBar: AppBar(
        title: const Text('1. routes에 정의된 화면'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('이름: $name'),
            Text('나이: $age'),
          ],
        ),
      ),
    );
  }
}

class SecondScreen extends StatefulWidget {
  const SecondScreen({super.key, required this.title, required this.content});

  final String title;
  final String content;
  @override
  State<SecondScreen> createState() => _SecondScreenState();
}

class _SecondScreenState extends State<SecondScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.yellow[200],
      appBar: AppBar(
        title: const Text('2. routes에 정의되지 않은 화면'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('제목: ${widget.title}'),
            Text('내용: ${widget.content}'),
          ],
        ),
      ),
    );
  }
}

class UnknownScreen extends StatefulWidget {
  const UnknownScreen({super.key});

  @override
  State<UnknownScreen> createState() => _UnknownScreenState();
}

class _UnknownScreenState extends State<UnknownScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.pink[200],
      appBar: AppBar(
        title: const Text('3. 아무곳에도 정의되지 않은 화면'),
      ),
      body: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('route 테이블에도, ongenerateRoute에도 등록되지 않았습니다.'),
          ],
        ),
      ),
    );
  }
}

 

import 'package:flutter/material.dart';
import 'package:navigator_route/main.dart';

class Routes {
  Routes._(); //더 이상 인스턴스 생성되지 않도록 막기

  static String main = '/';
  static String first = '/first';
  static String second = '/second';

  //위에 property로 선언되어 있어야만 아래에서 생성이 가능
  static final routes = <String, WidgetBuilder>{
    main: (context) => const MyHomePage(),
    first: (context) => const FirstScreen(),
  };
}

https://docs.flutter.dev/cookbook/navigation/named-routes

 

Navigate with named routes

How to implement named routes for navigating between screens.

docs.flutter.dev

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크