document

Navigation API

새로운 화면으로 이동하고, 되돌아오기

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}

class FirstRoute extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondRoute()),
            );
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

Route를 Navigator에 의해 관리되는 route 스택에 추가한다.

Navigator.push(BuildContext context, Route route)

Navigator에 의해 관리되는 route 스택에서 가장 위에 있는 Route를 제거한다. result(로 이전 화면에 데이터를 넘겨 줄수도 있다.)

Navigator.pop(BuildContext context, [T? result])

  // 창을 닫고 결과로 "Yep!"을 반환합니다.
  Navigator.pop(context, 'Yep!');

Named route로의 화면 전환

만약 앱의 다른 부분에서 동일한 화면으로 이동하고자 한다면, 중복된 코드가 생기게 된다. 이런경우 named route를 정의하여 화면 전환에 사용하는 방법이 해결책이 될 수 있다.

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Named routes Demo',
    // "/"을 앱이 시작하게 될 route로 지정한다. 본 예제에서는 FirstScreen 위젯이 첫 번째 페이지가
    // 될 것이다.
    initialRoute: '/',
    routes: {
      // When we navigate to the "/" route, build the FirstScreen Widget
      // "/" Route로 이동하면, FirstScreen 위젯을 생성한다.
      '/': (context) => FirstScreen(),
      // "/second" route로 이동하면, SecondScreen 위젯을 생성한다.
      '/second': (context) => SecondScreen(),
    },
  ));
}

class FirstScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Launch screen'),
          onPressed: () {
            // Named route를 사용하여 두 번째 화면으로 전환한다.
            Navigator.pushNamed(context, '/second');
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Screen"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // 현재 route를 스택에서 제거함으로써 첫 번째 스크린으로 되돌아 갑니다.
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

이름을 매개변수로 넘겨받아 Navigator에 의해 관리되는 route 스택에 추가한다.

Navigator.pushNamed(BuildContext context,String routeName)

initialRoute

만약에 navigator가 생성되어 있으면 가장 첫번째로 보여줄 route이름을 정의 한다. routes에서 정의된 namedRoute string을 사용한다.

routes

이용가능한 named route와 해당 route로 이동했을 때 빌드될 위젯을 정의한다.

namedRoute로 인수 전달

어떤 경우에는 namedRoute로 인수를 전달해야 할 수도 있다. 예를 들어 경로를 /user을 탐색하고 사용자에 대한 정보를 해당 경로로 전달할 수 있다.

전달 순서
1. 전달해야 하는 인수를 정의.
2. 인수를 추출하는 위젯을 만듬.
3. 위젯을 routes테이블에 등록.
4. 위젯으로 이동.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      /**
       * onGenerateRoute: 앱이 이름이 부여된 라우트를 네비게이팅할 때 호출됨. RouteSettings 가 전달됨
       * RouteSettings: 다음과 같은 구조를 가짐
       * const RouteSettings({
          String name,  // 라우터 이름
          bool isInitialRoute: false, // 초기 라우터인지 여부
          Object arguments  // 파라미터
          })

       * routes 테이블에 PassArgumentsScreen이 등록되지 않았지만 onGenerateRoute 함수에 의해 라우터 호출이 가능함
       */
      onGenerateRoute: (settings) {
        // Navigator.pushNamed() 가 호출된 때 실행됨

        // 라우트 이름이 PassArgementScreen의 routeName과 같은 라우트가 생성될 수 있도록 함
        if (settings.name == PassArgumentsScreen.routeName) {
          // Cast the arguments to the correct type: ScreenArguments.
          final ScreenArguments args = settings.arguments as ScreenArguments;

          return MaterialPageRoute(
            builder: (context) {
              // 추출한 파라미터를 PassArgumentScreen의 아규먼트로 직접 전달하면서 라우트 생성 후 반환
              return PassArgumentsScreen(
                title: args.title,
                message: args.message,
              );
            },
          );
        }
      },
      title: 'Navigation with Arguments',
      home: HomeScreen(),
    );
  }
}

// 앱 실행시 출력되는 라우트
class HomeScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 첫번째 자식. 파라미터를 명시적으로 전달하는 방식으로 라우트 호출
            RaisedButton(
              child: Text("Navigate to screen that extracts arguments"),
              onPressed: () {
                //유저가 버튼을 누르면 특정한 route로 전달됨. 아규먼트는 RouteSettings의 일부로 제공
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ExtractArgumentsScreen(),
                    // 아규먼트는 RouteSettings의 일부로 제공
                    settings: RouteSettings(
                      arguments: ScreenArguments(
                        'Extract Arguments Screen',
                        'This message is extracted in the build method.',
                      ),
                    ),
                  ),
                );
              },
            ),

            //이 버튼을 누르면 아규먼트는 onGenerateRoute에 의해 추출됨.
            RaisedButton(
              child: Text("Navigate to a named that accepts arguments"),
              onPressed: () {
                // When the user taps the button, navigate to a named route
                // and provide the arguments as an optional parameter.
                Navigator.pushNamed(
                  context,
                  PassArgumentsScreen.routeName,
                  arguments: ScreenArguments(
                    'Accept Arguments Screen',
                    'This message is extracted in the onGenerateRoute function.',
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

// 3-2 인수를 추출하는 위젯을 생성한다.
class ExtractArgumentsScreen extends StatelessWidget {
  static const routeName = '/extractArguments';

  const ExtractArgumentsScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    // Extract the arguments from the current ModalRoute settings and cast them as ScreenArguments.
    final ScreenArguments args =
        ModalRoute.of(context)?.settings.arguments! as ScreenArguments;

    return Scaffold(
      appBar: AppBar(
        title: Text(args.title),
      ),
      body: Center(
        child: Text(args.message),
      ),
    );
  }
}

// 전달받은 아규먼트를 이용하여 라우트를 동적 구성하는 위젯
class PassArgumentsScreen extends StatelessWidget {
  static const routeName = '/passArguments';

  final String title;
  final String message;

  // 이 위젯에서는 인수를 생성자 매개 변수로 허용한다. ModalRoute에서 인수를 추출하지 않는다.
  // 인수는 MaterialApp 위젯에 제공된 onGenerateRoute 함수에 의해 추출된다.
  const PassArgumentsScreen({
    Key? key,
    required this.title,
    required this.message,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    // 전달받은 아규먼트로 Scaffold를 구성하여 반환
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Text(message),
      ),
    );
  }
}

// 3-1 전달해야하는 인수를 정의한다.
class ScreenArguments {
  final String title;
  final String message;

  ScreenArguments(this.title, this.message);
}

onGenerateRoute

앱이 namedRoute로 이동할때 사용되는 Route 생성 콜백이다. 만약에 null을 리턴하면 initialRoute에 지정된 route로 핸들링한다. 그리고 모든 route는 버려지고 대신 defaultRouteName(/)으로 사용된다.

  MaterialApp(
      onGenerateRoute: (RouteSettings settings) {}

ModalRoute

  • build 메소드 내에서 ModalRoute 클래스를 이용하여 data를 얻어온다.
  • of.context에서 settings에 아규먼트가 있다. (아규먼트 인자는 object이다. 그럼 당연히 collection 형태가 유리할 것이다.)
  • as를 활용해 data type을 변환해주면 value를 읽어올때도 에러가 뜨지 않는다.
  • 제공된 아규먼트는 RouteSettings.arguments 를 통해 푸시된 route에 전달된다. (아규먼트 인자는 object이다. 그럼 당연히 collection 형태가 유리할 것이다.)

  • 맵은 키-값 쌍을 전달하는 데 자주 사용된다.

  • Navigator.onGenerateRoute 또는 Navigator.onUnknownRoute에서 인수를 사용하여 route를 구성할 수 있다.

MaterialPageRoute

전체 화면을 대체하는 모달(modal) route이다. 안드로이드의 경우 줌인-줌아웃으로 화면이 전환되며, ios의 경우 오른쪽에서 왼쪽으로 슬라이드 되는 형태로 화면이 전환된다.

  • settings:RouteSettings

RouteSettings

  • arguments → Object?
    이 라우트로 전달된 아규먼트.
  • name → String?
    라우트 이름
  • copyWith({String? name, Object? arguments}) → RouteSettings
    새로 대체된 필드값을 포함하여 settings object의 카피본을 생성

새로운 화면으로 데이터 보내기

종종 새로운 화면으로 단순히 이동하는 것 뿐만 아니라 데이터를 넘겨주어야 할 때도 있다. 예를 들어, 사용자가 선택한 아이템에 대한 정보를 같이 넘겨주고 싶은 경우가 있다.

이 예제에서 Todo 리스트를 만들 것이다. Todo를 선택하면 새로운 화면(위젯)으로 이동하면서 선택한 todo에 대한 정보를 보여줄 것이다. 여기서는 아래와 같은 단계로 진행한다.

순서
1. Todo 클래스를 정의.
2. Todo 리스트를 보여줌.
3. Todo에 대한 상세 정보를 보여줄 수 있는 화면을 생성.
4. 상세 화면으로 이동하면서 데이터를 전달.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class Todo {
  final String title;
  final String description;

  Todo(this.title, this.description);
}

void main() {
  runApp(MaterialApp(
    title: 'Passing Data',
    home: TodosScreen(
      todos: List.generate(
        20,
        (i) => Todo(
          'Todo $i',
          'A description of what needs to be done for Todo $i',
        ),
      ),
    ),
  ));
}

class TodosScreen extends StatelessWidget {
  final List<Todo> todos;

  TodosScreen({Key? key, required this.todos}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todos'),
      ),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(todos[index].title),
            // 사용자가 ListTile을 선택하면, DetailScreen으로 이동합니다.
            // DetailScreen을 생성할 뿐만 아니라, 현재 todo를 같이 전달해야
            // 한다는 것을 명심하세요.
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => DetailScreen(todo: todos[index]),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  // Todo를 들고 있을 필드를 선언합니다.
  final Todo todo;

  // 생성자는 Todo를 인자로 받습니다.
  DetailScreen({Key? key, required this.todo}) : super(key: key);

  
  Widget build(BuildContext context) {
    // UI를 그리기 위해 Todo를 사용합니다.
    return Scaffold(
      appBar: AppBar(
        title: Text(todo.title),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(todo.description),
      ),
    );
  }
}

화면을 넘나드는 위젯 애니메이션

한 화면에서 다른 화면으로 넘어갈 때, 사용자에게 가이드를 주는 것은 종종 도움이 된다. 앱에서 사용할 수 있는 방법은 화면을 전환할 때 위젯에 애니메이션 효과를 주는 것이다. 이러한 방법은 두 화면을 이어주는 시각적 연결 고리를 만들어 준다.

  1. 같은 이미지를 보여주는 2개의 화면을 생성.
  2. 첫 번째 화면에 Hero 위젯을 추가.
  3. 두 번째 화면에 Hero 위젯을 추가.
  import 'package:flutter/material.dart';

void main() => runApp(const HeroApp());

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

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Transition Demo',
      home: MainScreen(),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Main Screen'),
      ),
      body: GestureDetector(
        child: Hero(
          tag: 'imageHero',
          child: Image.network(
            'https://picsum.photos/250?image=9',
          ),
        ),
        onTap: () {
          Navigator.push(context, MaterialPageRoute(builder: (_) {
            return const DetailScreen();
          }));
        },
      ),
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        child: Center(
          child: Hero(
            tag: 'imageHero',
            child: Image.network(
              'https://picsum.photos/250?image=9',
            ),
          ),
        ),
        onTap: () {
          Navigator.pop(context);
        },
      ),
    );
  }
}

참고

Hero(
  tag: 'imageHero',
  child: Image.network(
    'https://picsum.photos/250?image=9',
  ),
);

양쪽 위젯에서 위의 동일한 코드를 사용한다. 재사용을 위해 위젯을 만들어 사용하는 것이 합리적일 것이다... 여기에서는 예제니까 간단하게 사용한다.

Hero

tag를 명시하는 것 외에 필요한 작업은 없다. 간단하게 앱을 풍부하게 만드는 방법 중 하나!

Hero Widget api

0개의 댓글