정보

    • 업무명     : [Flutter] 쉽게 배우는 플러터 앱 개발 실무 04강
    • 작성자     : 이상호
    • 작성일     : 2025.01.04
    • 설   명      :
    • 수정이력 :

     

     화면 이동

    학습목표

    • 앱 화면을 추가하며(Push) 부모 화면에서 자식 화면으로 이동하는 방법을 이해합니다.
    • 사용중인 앱 화면을 빠져나와(Pop) 자식 화면에서 부모 화면으로 이동하는 방법을 이해합니다.
    • 사용중인 자손 화면을 빠져나와 Home 화면으로 바로 이동하는 방법을 이해합니다.
    • 소스코드를 분리하여 모듈화하는 장점과 방법을 이해합니다.
    • Widget간 인자를 넘기고 받는 방법을 이해합니다.

     

    Navigator 객체를 이용한 화면 이동

    • 우선 아래와 같이 2개의 화면을 만들어 간단히 화면 이동을 해 보겠습니다. 첫번째 페이지는 Flutter 프레임워크가 제공하는 MyHomePage Widget 객체를 사용하고 두번째 페이지는 MySecondPage Widget 객체를 만들어 사용하겠습니다.

     

    • 먼저 이전에 만든 프로젝트를 사용할 수 없으니 navigation 프로젝트를 만듭시다.
    • 먼저 첫번째 페이지를 만들기 위하여 MyApp 클래스와 _MyHomePageState 클래스의 코드를 아래와 같이 수정합시다. 이번에는 Scaffold 객체의 body 속성을 모두 변경하게 됩니다.
    // main.dart 파일에 구현한 첫번째 페이지
    ... 생략 ...
    class MyApp extends StatelessWidget {
    ... 중략 ...
          home: const MyHomePage(title: '첫번째 페이지'),   // 페이지 이름 변경
    ... 중략 ...
    class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Container(                 // 여기부터 코딩 시작
            width: double.infinity,            // 화면의 넓이 전체를 사용
            height: double.infinity,           // 화면의 높이 전체를 사용
            color: Colors.pinkAccent,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  "첫번째 페이지입니다.",
                  style: TextStyle(fontSize: 40),
                ),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.blue,
                    ),
                  onPressed: () {},            // 페이지 이동 로직은 이후 구현
                  child: Text(
                      "두번째 페이지로 이동",
                      style: TextStyle(fontSize: 20),
                  ),
                ),
              ],
            ),
          ),                               // 여기까지 코딩
        );
      }
    }

     

    • 실행해 보면 첫번째 페이지가 잘 나타납니다.

     

    • 이제 두번째 페이지를 만들어 봅시다. 먼저 아래 화면과 같이 main.dart 파일의 끝에서 st를 친후 안드로이드 스튜디오가 제시해 주는 몇가지 선택지 중에서 stless을 선택합니다.

     

    • 그러면 안드로드 스튜디오가 아래 화면과 같이 클래스를 생성해 주는데 이는 Spring Boot를 개발할 때 IntelliJ에서 psvm을 입력하여 코드를 자동으로 생성받는 것과 유사한 기능입니다.

     

    • 이때 class와 extends 사이에 클래스의 이름인 MySocondPage를 입력합니다. 그러면 아래 화면과 같이 안드로이드 스튜디오가 클래스 이름이 필요한 곳에 MySocondPage를 자동으로 추가해 주면서 클래스를 수정해 줍니다.

     

    그후 return const Placeholder(); 문장을 _MyHomePageState 클래스의 return Scaffold(…중략…); 문장 전체로 대체합니다. 그리고 배경색과 화면 중앙의 텍스트와 버튼의 색상의 두번째 페이지에 맞게 수정해 줍니다.

    // MySecondPage 클래스에 구현한 두번째 페이지
    class MySecondPage extends StatelessWidget {
      const MySecondPage({super.key});
      @override
      Widget build(BuildContext context) {
    
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text("두번째 페이지"),      // 변경된 코드
          ),
          body: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.blue,               // 변경된 코드
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  "두번째 페이지입니다.",        // 변경된 코드
                  style: TextStyle(fontSize: 40),
                ),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.pinkAccent,  // 변경된 코드
                  ),
                  onPressed: () {},
                  child: Text(
                    "첫번째 페이지로 이동",      // 변경된 코드
                    style: TextStyle(fontSize: 20),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

     

    • 위의 코드에서 MyHomePage와 MySecondPage의 큰 차이점을 느꼈나요? MyHomePage는 2개의 클래스로 만들어지는 반면에 MySecondPage는 1개의 클래스로 만들어지고 있습니다. 나중에 설명하겠지만 여기서는 Widget를 하나의 클래스로 만들 수도 있고 두개의 클래스로 만들수도 있구나 하는 정도만 이해하고 가면 됩니다.
    • 이제는 버튼을 눌렀을 때 두번째 페이지로 이동하게 해주는 코드를 작성할 차례입니다. 이벤트 처리기로 만들면 되겠지요. 자식 페이지로의 이동은 Navigator 객체의 push 메소드에 의하여 이루어집니다. 이때 또 다시 Navigator.of(context) 팩토리 메소드 혹은 생성자 메소드를 사용하여 현재 화면인 context 화면 이동을 제어하기 위한 Navigator 객체를 만듭니다. 즉 화면의 이동도 현재 화면에 보이는 맥락으로 움직인다는 의미로 받아 들이면 됩니다. 제공되는 인자는 새로 생성되는 MySecondPage 화면의 경로를 의미하는데 MaterialPageRoute 객체를 사용하여 MySecondPage 화면으로 이동하는 경로 객체를 만듭니다. Route는 화면 전환과 네비게이션을 관리하는 개념으로, 모바일 앱뿐만 아니라 웹에서도 사용됩니다. Flutter는 이러한 Route 개념을 활용하여 앱 내에서 페이지 전환을 쉽게 구현할 수 있도록 합니다.
    • 위의 설명이 어렵다면 그냥 아래의 코드 조각을 하위 화면으로 이동하는 코드의 전형적인 모습으로 받아 들이면 됩니다.
    // 이벤트 처리기를 onPressed 이벤트에 구현하는 코드 (첫번째 페이지)
    ElevatedButton(
      style: ElevatedButton.styleFrom(
          backgroundColor: Colors.blue,
        ),
      onPressed: () {         // 이벤트 처리기의 시작
        Navigator.of(context).push(    // 자식 화면으로 이동하기 위한 전형적인 코드
          MaterialPageRoute(builder: (_) => MySecondPage()),
        );                             // (_)는 문법적으로 인자의 지정이 강제화되어
                                       //       있으나 사용하지 않을 때 사용
      },                      // 이벤트 처리기의 끝
      child: Text(
          "두번째 페이지로 이동",
          style: TextStyle(fontSize: 20),
      ),
    ),

     

    • 먼저 My Home Page에서 My Second Page로 이동이 잘 되는지 확인하고 갑시다. 코드의 모순이 누적되면 감당하기 어렵기 때문에 코드가 변경될 때마다 테스트하고 가는 것이 테스트 주도 개발(TDD, Test Driven Development)의 기본 사상입니다. 매우 중요한 원칙이니 우리도 따라서 합시다.

     

     

    • My Home Page에서 My Second Page로 화면 이동하는 로직이 잘 동작하니 이제 자신감을 가지고 부모 페이지인 My Home Page로 이동하는 로직을 추가합시다. 코딩하는 방법은 부모 화면에서 자식 화면으로 이동하는 것과 유사하나 훨씬 코딩이 단순합니다. Navigator.of(context).push(MaterialPageRoute(builder: (_) => MySecondPage()));와 같은 코드를 Navigator.pop(context);와 같이 수정해 주면 되는데 두 코드를 비교해 보면 인자가 빠져 있습니다. 당연할 것입니다. 자식은 여럿이니 어디로 가야할지 알아야 하지만 부모는 하나이니 어디로 가는지를 이미 알고 있으니 인자는 필요없는 것입니다. 그리고 push 메소드가 pop 메소드로 바뀌어 있는데 이는 화면 이동을 관리하는 Navigator 객체가 스택(Stack)이라는 자료 구조를 사용하기 때문에 붙은 이름입니다.
    // 이벤트 처리기를 onPressed 이벤트에 구현하는 코드 (두번째 페이지)
    ElevatedButton(
      style: ElevatedButton.styleFrom(
          backgroundColor: Colors.blue,
        ),
      onPressed: () {         // 이벤트 처리기의 시작               
        Navigator.pop(context);      // 부모 화면으로 이동하기 위한 전형적인 코드
      },                      // 이벤트 처리기의 끝
      child: Text(
          "첫번째 페이지로 이동",
          style: TextStyle(fontSize: 20),
      ),
    ),

     

    • 이제 실행해 보면 첫번째 페이지와 두번째 페이지간에 화면 이동이 자연스럽게 이루어지는 것을 확인할 수 있습니다.

     

     

    • 이제 아래 그림을 봅시다. 아래 그림에서 Data를 화면으로 바꾸어 이해하면 Navigator 객체가 하는 일을 이해할 수 있습니다. 부모화면에서 자식 화면으로 이동할 때 화면을 하나 밀어 넣는(Push) 것이 되고, 다시 자식화면에서 부모 화면으로 돌아갈 때 화면 하나를 빼내는(Pop) 것이 되니 메소드의 이름의 의미를 알 수 있습니다.

     

    • build() 메소드는 화면을 만드는(build) 역할을 하는 메소드입니다. 이때 동일한 화면에서 그리는 작업을 하려면 동일한 context를 가지고 다녀야 합니다. 그래서 Navigator.of(context)와 같이 context에 기반하여 Navigator 객체를 생성하게 하고 있고, Navigator.pop(context) 메소드의 첫번째 인자로 context를 지정해 주었습니다. 이 말을 이해하려고 하다가 이해가 안되면 그냥 넘어가도 됩니다. 전공자는 알고 가야 하겠지만 비전공자는 코딩할 위치와 코딩하는 방법만 알고 있어도 됩니다. 그것도 인터넷 검색이나 ChatGPT 등의 생성형 AI의 도움을 받을 정도만 알고 있으면 됩니다.

     

     

     

    • 간단한 프로그램에서 화면간의 이동은 위에서 설명하는 정도의 개념을 이해하고 있으면 됩니다. Flutter 앱의 경우도 웹처럼 Route를 복잡하게 가져갈 수 있으나 이도 또한 전문 프로그래머의 영역이라고 생각합니다.

     

    모듈 분리 (소스 코드 분리)

    • 앞에서 예를 든 것과 같이, Dart 언어는 하나의 파일에서 여러 개의 위젯 클래스를 함께 관리하는 것을 허용합니다. 그러나 프로젝트를 진행할 때는 각 위젯을 별도의 파일로 분리해 사용하면 소스 파일의 크기를 줄이고, 여러 명이 각자 담당하는 위젯을 구분하여 동시에 작업할 수 있는 등의 이점이 있습니다. 이러한 작업을 모듈 분리라고 합니다. 쉽게 말해, 하나의 소스 파일이 하나의 위젯을 포함하도록 분리하는 것으로 이해할 수 있습니다.
    • 먼저 lib 폴더 아래 my_second_page.dart 파일을 만듭시다.

     

    • 그리고 main.dart 파일에 개발해 두었던 MySecondPage 클래스의 코드들을 잘라내서 my_second_page.dart 파일로 이동시킵니다. 그러면 아래와 같이 문법 오류가 잔뜩 나타나는데 필요한 import 작업이 누락되어 있기 때문입니다.

     

    • 이때 import 'package:flutter/material.dart'; 문장을 파일의 맨앞에 추가하면 오류가 사라집니다.

     

    • 그런데 main.dart 파일로 이동해 보면 import 'package:flutter/material.dart'; 문장이 존재하는데도 1개의 오류가 발생하는 것을 볼 수 있습니다.

     

    • 그 이유는 아래 화면에서 빨간색 밑줄로 오류를 표시해 보여주는 것과 같이 같은 파일에 있던 MySecondPage Widget이 다른 파일로 옮겨 가서 찾을 수 없기 때문인데요 이렇게 모듈 분리된 Widget은 import해야 사용이 가능합니다.

     

    • import 'my_second_page.dart'; 문장과 같이 상대경로를 사용하여 Widget을 import하거나 import 'package:navigation/my_second_page.dart'; 문장과 같이 절대경로를 사용하여 Widget을 import합니다. 외부 패키지와 달리 내부 패키지의 경우에는 대부분 상대경로가 개발환경과 운영환경에 종속성이 없어 더 나은 선택이 됩니다.

     

    • 이렇게 모듈 분리 후에도 정상적으로 실행되는지 테스트를 완료하기 바랍니다.

     

     

    • 모듈 분리의 이점을 누려 봅시다. 먼저 my_second_page.dart 파일을 복사하여 붙여 넣는 방법으로 my_third_page.dart 파일을 만듭니다.

     

    • 클래스의 이름을 MySecondPage에서 MyThirdPage로 변경한 후 변경이 필요한 곳만 수정해 주는 것으로 쉽게 하나의 화면을 더 만들 수 있습니다.
    • 변경한 전체 코드는 아래와 같습니다.
    // 모듈을 복사하여 수정이 완료된 my_third_page.dart 파일
    import 'package:flutter/material.dart';
    
    class MyThirdPage extends StatelessWidget {    // 변경된 코드
      const MyThirdPage({super.key});              // 변경된 코드
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text("세번째 페이지"),     // 변경된 코드
          ),
          body: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.green,             // 변경된 코드
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  "세번째 페이지입니다.",      // 변경된 코드
                  style: TextStyle(fontSize: 40),
                ),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.blue,  // 변경된 코드
                    ),
                  onPressed: () {
                    Navigator.pop(context);
                  },
                  child: Text(
                    "두번째 페이지로 이동",             // 변경된 코드
                    style: TextStyle(fontSize: 20),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

     

    • 예제. MySecondPage에서 MyThirdPage로 이동하는 기능을 구현해 보세요. 구현하는 방식은 MyHomePage에서 MySecondPage로 이동하는 로직과 동일한데 세번째 페이지로 이동 Elevated Button을 만들어 세번째 페이지로 이동하는 코드를 추가하면 됩니다. my_second_page.dart 파일에 import 'my_third_page.dart'; 문장을 추가하는 것도 잊지 말고 추가해야 오류가 발생하는 것을 방지할 수 있습니다.
    • Page들이 부모 Widget에서 자식 Widget으로 순차적으로 Push되고 Pop되는 경우를 생각해 봅시다. Navigator Stack의 상태는 아래 화면과 같이 변화할 것입니다.

     

    • 이번에는 MyThirdPage에서 MyHomePage로 바로 이동해야 하는 경우를 생각해 봅시다.
    • 아래 화면과 같은 형태의 이동이 기술적으로 가능하지도 않지만 가능해도 문제입니다.

     

    • MyHomePage를 계속 Push하는 것으로 구현할 수도 있는데 동작은 할텐데 정신을 차리고 생각을 좀 해 봅시다. 이렇게 코딩을 하면 정상동작을 하는 것처럼 보이는데 화면이 계속 쌓여가다가 중간에 남은 화면이 계속 메모리에 남아 쌓여 가다가 어느순간 스마트폰이 느려지다가 결국 메모리를 모두 소모하고 멈출 것입니다.

     

    • 이것이 프로그래머들이 흔히 말하는 메모리 누수의 의미입니다. 이뮬레이터도 이런 현상을 모두 해결하지 못했는지 가끔 동작하지 않고 죽습니다. 메모리 누수를 포함하여 가끔 꼬여버린 컴퓨터의 문제를 해결하는 방법은??? 컴퓨터를 껐다가 켜서 초기화하는 것입니다. → 이것이 전문가들이??? 가장 많이 사용하는 컴퓨터의 문제해결방법인 이유입니다. 저도 이 교안을 만드는 동안에 이뮬레이터의 현상태를 몇번 지웠다(Wipe Data)가 다시 설정했는지 컴퓨터를 몇번을 껐다 켰는지 모릅니다. ㅠㅠㅠ 프로그래밍은 동작할 때부터 더 어렵습니다. 문제들이 숨겨져 있는데 이런 문제를 테스트를 통하여 모두 잡아 내야 하기 때문입니다. 물론 이런 영역은 프로그래밍 전문가들이 할 일입니다만 기획을 하는 사람들도 알고는 있어야 합니다. 그래야 멋진 기획을 할 수 있을테니까요.
    • 자손 화면에서 조상 화면으로 이동하려면 이런 용도에 맞는 popUntil()이라는 별도의 메소드를 사용하고 인자로 (route) => route.isFirst 람다 함수를 넘겨 주어야 합니다.

     

    • 먼저 MyThirdPage에 홈으로 이동하는 버튼을 만들어 보겠습니다. 홈버튼은 스마트폰 상단의 앱바에 만드는 것이 보기 좋을 것 같습니다.
    • 아래 코드와 같이 AppBar에 actions라는 속성으로 IconButton Widget을 추가하였습니다. 이벤트를 처리하여야 하니 Icon이 아니라 IconButton을 추가하여야 하겠지요?
    // 홈버튼 추가후 코드
    ... 생략 ...
    class MyThirdPage extends StatelessWidget {
      const MyThirdPage({super.key});
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text("세번째 페이지"),
            actions: [       // actions 속성 추가 시작
              IconButton(
                icon: Icon(Icons.home),
                onPressed: () {}
              ),
            ]                // actions 속성 추가의 끝
          ),
    ... 생략 ...

     

    • 실행한 후 MyHomePage를 거쳐, MySecondPage를 거쳐 MyThirdPage로 이동해 보니 스마트폰의 우측 상단에 즉 AppBar의 우측에 홈 버튼이 나타납니다.

     

    • 이번에는 홈버튼을 눌렀을 때 MyHomePage로 이동하는 코드를 추가하겠습니다. IconButton Widget의 onPressed 이벤트 처리기에 Navigator.of(context).popUntil까지만 입력하면 아래 화면과 같이 전체 문법을 자동으로 완성해 줍니다.

     

    • 여기서 predicate는 true 혹은 false를 반환하는 람다함수를 말합니다. Navigator.of(context).popUntil 함수는 predicate가 true를 반환하면 pop을 멈추고 false를 반환하면 계속 pop하며 화면을 Stack에서 지워게 됩니다.
    // 변경후 MyThirdPage 클래스 코드
    ... 생략 ...
    class MyThirdPage extends StatelessWidget {
    ... 중략 ...
              IconButton(
                icon: Icon(Icons.home),
                onPressed: () {
                  //Navigator.of(context).popUntil(predicate)
                  Navigator.of(context).popUntil( // route.isFirst가 false인 경우 pop 지속
                      (route) => route.isFirst    // route.isFirst가 true인 경우 pop 중단
                  );
                },
              ),
    ... 생략 ...

     

    • 코드를 완성하여 화면간 이동을 실행해 봅시다. 첫번째 페이지 → 두번째 페이지 → 세번째 페이지 → 첫번째 페이지의 순으로 화면이 이동하는 것을 확인할 수 있습니다.

     

     

     

     

    Widget간 인자 넘기고 받기

    • 아래의 두 코드를 읽으며 인자를 넘겨 주고 넘겨 받는 방법에 대하여 직관적으로 이해해 봅시다.
    // Widget으로 인자를 넘겨 주는 코드
    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,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),  // 인자를 넘겨 주는 코드
        );
      }
    }
    
    // Widget에서 인자를 받아 오는 코드
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key, required this.title});   // 인자를 받아 속성을 초기화하며
                                                            // 객체를 생성하는 생성자
      final String title;                                   // 인자를 받아 저장하는 속성
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }

     

    • 참고. 위의 코드 중 MyHomePage({super.key, required this.title}); 가 객체의 생성자(Constructor)입니다. **생성자(Constructor)**는 MyHomePage(title: 'Flutter Demo Home Page')와 같이 객체를 변수화 혹은 인스탄스화하는 문장을 만나면 넘겨받은 인자를 객체의 속성으로 초기화하는 방법을 통하여 객체를 생성합니다.

     

    • 위의 코드를 보고 Widget간에 인자를 넘겨 주고 넘겨 받는 방법을 요약하면 아래와 같습니다.
      • 호출받는 Widget의 생성자에 받아올 인자를 정의 & 그에 맞는 속성을 정의
      • 호출하는 Widget에서 호출받는 Widget의 정의에 맞게 호출받는 Widget을 호출

     

    • 그러면 이번에는 위의 코드를 참조하여 MyHomePage에서 MySecondPage로 인자를 넘기는 코드를 구현하면서 Widget간에 인자를 주고 받는 방법을 살펴 봅시다.
    // 인자를 넘겨 받는 코드를 하기 전의 MySecondPage 클래스의 헤더 코드
    class MySecondPage extends StatelessWidget {
      const MySecondPage({super.key});

     

    • MyHomePage 클래스의 코드 형식에 맞게 id와 password를 받아올 수 있도록 MySecondPage 클래스의 코드를 작성해 보았습니다.
    // 인자를 넘겨 받는 코드를 한 후의 MySecondPage 클래스의 헤더 코드
    class MySecondPage extends StatelessWidget {
      const MySecondPage({super.key, required this.id, required this.password});// 변경된 코드
      final int id;            // 추가된 속성 코드
      final String password;   // 추가된 속성 코드

     

    • MyHomePage 클래스에서 MySecondPage 클래스를 호출하는 코드를 수정하기 위하여 main.dart 편집기로 이동했더니 MySecondPage 클래스의 정의가 변하며 오류가 발생하고 있습니다. 그러면 앞에서 정의된 인자의 형식에 맞게 MySecondPage 클래스를 호출하는 문장을 수정해 보겠습니다.

     

    • 키워드 인자 형식이어서 인자이름: 값의 형식으로 인자를 넘겨 줍니다. 2개의 인자가 모두 필수(required)이어서 id와 password 값을 임의로 지정하여 2개의 인자를 모두 넘겨 주었습니다.
    // MySecondPage를 호출하는 코드
    MaterialPageRoute(builder: (_) => MySecondPage(id: 320811, password: "mypassword")),

     

    • 호출할 때의 인자가 속성으로 잘 넘어 오는지 MySecondPage 클래스에서 확인하는 코드를 작성해 보겠습니다.
    // 변경전 MySecondPage 클래스의 코드
    class MySecondPage extends StatelessWidget {
      const MySecondPage({super.key, required this.id, required this.password});
      final int id;
      final String password;
    ... 중략 ...
          body: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.blue,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  "두번째 페이지입니다.",
                  style: TextStyle(fontSize: 40),
                ),
                ... 생략 ...
                
    // 변경후 MySecondPage 클래스의 코드
    class MySecondPage extends StatelessWidget {
      const MySecondPage({super.key, required this.id, required this.password});
      final int id;
      final String password;
    ... 중략 ...
          body: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.blue,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(       // 추가된 Text Widget 시작
                  "ID - $id / Password - $password",
                  style: TextStyle(fontSize: 30),
                ),          // 추가된 Text Widget의 끝
                Text(
                  "두번째 페이지입니다.",
                  style: TextStyle(fontSize: 40),
                ),
                ... 생략 ...

     

     

    • 실행 후 두번째 페이지로 이동해 보니 넘어온 인자의 값이 잘 보입니다.

     

    • 이번에는 첫번째 페이지에서 책들의 이미지를 보여준 후 이미지를 클릭하면 이미지의 상세화면을 만들어 보여주는 기능을 구현해 보겠습니다.
    // 변경후 _MyHomePageState 클래스의 코드
    ... 생략 ...
                Text(
                  "첫번째 페이지입니다.",
                  style: TextStyle(fontSize: 40),
                ),
                Row(                  // 코드 추가의 시작
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    GestureDetector(
                      onTap: () {
                        print("파이썬 + AI 책이 클릭되었습니다.");
                      },
                      child: Image.network("https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg"),
                    ),
                    GestureDetector(
                      onTap: () {
                        print("점프 투 파이썬 책이 클릭되었습니다.");
                      },
                      child: Image.network("https://image.aladin.co.kr/product/31794/10/cover200/k362833219_1.jpg"),
                    ),
                  ],
                ),                    // 코드 추가의 끝 
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                  )
    ... 생략 ...

     

    • 위의 코드를 실행해서 책의 이미지가 나오고 Image Widget을 GestureDetector Widget으로 감싸서 이미지를 클릭할 때 이벤트 처리기가 동작합니다.

     

     

    • my_second_page.dart 파일을 복사하여 붙여 넣는 방법으로 book_detail_page.dart 파일을 만듭니다.

     

    • 그리고 book_detail_page.dart 파일의 코드를 아래와 같이 수정합니다.
    // 변경후 book_detail_page.dart 파일의 코드
    import 'package:flutter/material.dart';
    
    //import 'my_third_page.dart';          세번째 페이지로 이동하지 않아 삭제
    
    //class MySecondPage extends StatelessWidget {
    //  const MySecondPage({super.key, required this.id, required this.password});
    //  final int id;
    //  final String password;
    
    class BookDetailPage extends StatelessWidget {             // 클래스의 이름을 바꾸고
      const BookDetailPage({super.key, required this.name});   // 책의 이름을 인자로 가져 오도록 수정
      final String name;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(name),                // title을 책의 이름으로 변경
          ),
          body: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.brown,             // 배경색을 갈색으로 변경
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                /*
                Text(
                  "ID - $id / Password - $password",
                  style: TextStyle(fontSize: 30),
                ),
                Text(
                  "두번째 페이지입니다.",
                  style: TextStyle(fontSize: 40),
                ),
                */
                // 책의 이미지를 임시로 보여줌 - 나중에 인자로 넘어온 책의 이름으로 조회하여 보여줄 것임
                Image.network("https://image.aladin.co.kr/product/3422/90/cover200/8966260993_1.jpg"),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.pinkAccent,
                  ),
                  onPressed: () {
                    Navigator.pop(context);
                  },
                  child: Text(
                    "첫번째 페이지로 이동",
                    style: TextStyle(fontSize: 20),
                  ),
                ),
                /* 세번째 페이지로 이동 기능을 제거
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.green,
                  ),
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(builder: (_) => MyThirdPage()),
                    );
                  },
                  child: Text(
                    "세번째 페이지로 이동",
                    style: TextStyle(fontSize: 20),
                  ),
                ),
                */
              ],
            ),
          ),
        );
      }
    }

     

    • 그리고 위에서 개발한 BookDetailPage로 이동하도록 main.dart 파일의 코드를 아래와 같이 수정합니다.
    // 변경후 main.dart 파일의 _MyHomePageState 클래스의 코드
    ... 생략 ...
        Text(
          "첫번째 페이지입니다.",
          style: TextStyle(fontSize: 40),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            GestureDetector(
              onTap: () {
                //print("파이썬 + AI 책이 클릭되었습니다.");
                Navigator.of(context).push(   // 화면 이동 기능 추가
                  MaterialPageRoute(builder: (_) => BookDetailPage(name: "파이썬 + AI")),
                );
              },
              child: Image.network("https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg"),
            ),
            GestureDetector(
              onTap: () {                     
                //print("점프 투 파이썬 책이 클릭되었습니다.");
                Navigator.of(context).push(   // 화면 이동 기능 추가
                  MaterialPageRoute(builder: (_) => BookDetailPage(name: "점프 투 파이썬")),
                );
              },
              child: Image.network("https://image.aladin.co.kr/product/31794/10/cover200/k362833219_1.jpg"),
            ),
          ],
        ),
        ElevatedButton(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.blue,
          ),
    ... 생략 ...

     

    • 실행해 보면 BookDetailPage의 제목으로 책의 이름이 나타나고 임의의 책 이미지가 화면에 나타나는 것을 확인할 수 있습니다.

     

     

    • 이제는 인자로 넘어온 책의 이름으로 이미지의 URL을 찾아 책의 이름에 적합한 책의 이미지를 보여 주는 기능을 개발하겠습니다.
    • 아래 화면과 같이 bookUrls 맵(딕셔너리) 변수를 만들면 const BookDetailPage({super.key, required this.name}); 문장에서 오류가 발생합니다. 이유는 변수 정의전까지 변하지 않는 상수(Constant) 상태이었던 객체가 변할 수 있는 변수(Variable) 상태로 변하기 때문인데

     

    • BookDetailPage({super.key, required this.name}); 문장과 같이 const 키워드를 제거하는 것으로 해결이 됩니다.

     

    // 변경후 book_detail_page.dart 파일의 코드
    import 'package:flutter/material.dart';
    
    class BookDetailPage extends StatelessWidget {
      // bookUrls 변수의 정의로 BookDetailPage이 const가 아니게 되어 const 삭제
      //const BookDetailPage({super.key, required this.name});
      BookDetailPage({super.key, required this.name});
      final String name;
    
      // 책의 이름을 키(Key)로 책의 이미지 URL을 값(Value)으로 맵(딕셔너리) 변수를 정의
      var bookUrls = { "파이썬 + AI": "https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg",
                       "점프 투 파이썬": "https://image.aladin.co.kr/product/31794/10/cover200/k362833219_1.jpg",
                     };
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(name),
          ),
          body: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.brown, 
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                //Image.network("https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg"),
                // 책 이미지의 URL을 bookUrls 맵(딕셔너리)에서 찾아서 보여 줌            
                Image.network("${bookUrls[name]}"),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.pinkAccent,
                  ),
                  onPressed: () {
                    Navigator.pop(context);
                  },
                  child: Text(
                    "첫번째 페이지로 이동",
                    style: TextStyle(fontSize: 20),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

     

    • 그러면 선택한 책과 일치하는 이미지가 나타나는 것을 확인할 수 있습니다.

     

     

    • 화면을 이동할 때 애니메이션(전환 애니메이션,Transaction Animation)해 주는 Hero Widget을 사용하면 화면의 이동이 매우 역동적으로 변하는데 아래의 코드들과 같이 애니메이션이 필요한 동일한 시각적 요소들을 Hero Widget으로 감싸주면 됩니다. 이때 tag로 제공되는 문자열은 서로 연결되어 애니메이션이 필요한 동일한 시각적 요소들에게 동일한 값이 지정되어야 합니다.
    // 화면 전환을 시작하는 Hero Widget 코드 - main.dart 파일의 _MyHomePageState 클래스의 코드 
    Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        GestureDetector(
          onTap: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => BookDetailPage(name: "파이썬 + AI")),
            );
          },
          child: Hero(              // 책의 이름을 tag로 지정하여 Hero Widget 추가
            tag: "파이썬 + AI",
            child: Image.network("https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg")
          ),
        ),
        GestureDetector(
          onTap: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => BookDetailPage(name: "점프 투 파이썬")),
            );
          },
          child: Hero(
              tag: "점프 투 파이썬", // 책의 이름을 tag로 지정하여 Hero Widget 추가
              child: Image.network("https://image.aladin.co.kr/product/31794/10/cover200/k362833219_1.jpg")
          ),
        ),
      ],
    ),
    
    // 화면 전환이 끝나는 부분의 Hero Widget 코드 - book_detail_page.dart 파일의 코드
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        //Image.network("https://image.aladin.co.kr/product/3422/90/cover200/8966260993_1.jpg"),
        Hero(
          tag: name,      // 인자로 받은 책의 이름을 tag로 지정하여 Hero Widget 추가
          child: Image.network("${bookUrls[name]}")
        ),
        ElevatedButton(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.pinkAccent,
          ),

     

    • Hero Widget 애니메이션을 실행해 보면 tag로 연결된 두 이미지가 서로 연결되어 동작하는 듯한 애니메이션이 발생하는 것을 확인할 수 있습니다.
    • 현재까지 작성된 코드를 공유합니다.
    // main.dart 파일의 _MyHomePageState 클래스의 코드 
    class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.pinkAccent,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  "첫번째 페이지입니다.",
                  style: TextStyle(fontSize: 40),
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    GestureDetector(
                      onTap: () {
                        Navigator.of(context).push(
                          MaterialPageRoute(builder: (_) => BookDetailPage(name: "파이썬 + AI")),
                        );
                      },
                      child: Hero(
                        tag: "파이썬 + AI",
                        child: Image.network("https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg")
                      ),
                    ),
                    GestureDetector(
                      onTap: () {
                        Navigator.of(context).push(
                          MaterialPageRoute(builder: (_) => BookDetailPage(name: "점프 투 파이썬")),
                        );
                      },
                      child: Hero(
                          tag: "점프 투 파이썬",
                          child: Image.network("https://image.aladin.co.kr/product/31794/10/cover200/k362833219_1.jpg")
                      ),
                    ),
                  ],
                ),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blue,
                  ),
                  onPressed: () {
                    Navigator.of(context).push(
                      MaterialPageRoute(builder: (_) => MySecondPage(id: 320811, password: "mypassword",)),
                    );
                  },
                  child: Text(
                    "두번째 페이지로 이동",
                    style: TextStyle(fontSize: 20),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

     

    // BookDetailPage 클래스의 코드 
    import 'package:flutter/material.dart';
    
    class BookDetailPage extends StatelessWidget {
      BookDetailPage({super.key, required this.name});
      final String name;
    
      var bookUrls = { "파이썬 + AI": "https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg",
                       "점프 투 파이썬": "https://image.aladin.co.kr/product/31794/10/cover200/k362833219_1.jpg",
                     };
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(name),
          ),
          body: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.brown,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Hero(
                  tag: name,
                  child: Image.network("${bookUrls[name]}")
                ),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.pinkAccent,
                  ),
                  onPressed: () {
                    Navigator.pop(context);
                  },
                  child: Text(
                    "첫번째 페이지로 이동",
                    style: TextStyle(fontSize: 20),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

     

    • Instructor Note : 노션의 “Flutter FAQ와 Tips”에서 “안드로이드 스튜디오와 삼성폰을 연결하는 방법”, “앱의 이름을 지정하는 방법”, “앱의 아이콘을 지정하는 방법”을 설명할 것 혹은 보여줄 것

     

    [실습] 자신의 가족을 소개하는 family_intro 앱을 개선해 보세요.

    • 지금까지 배운 것들을 기반으로 팀을 소개하는 하나의 화면으로 구성된 앱을 확장하여 아래 그림과 같이 구성원의 이미지를 클릭하는 경우 구성원의 상세 화면으로 이동하는 기능을 가진 앱으로 개선해 보기 바랍니다.

     

    • 홈화면의 이미지에서 구성원 상세 화면의 이미지로 이동할 때 Hero 애니메이션을 구현해 보고, 앱을 구현한 후 앱의 이름과 앱의 아이콘을 지정하여 안드로이드 스마트 폰에 추억으로 간직하기 바랍니다. 안드로이드 스튜디오에서 이뮬레이터 대신 스마트폰을 연결하여 실행하면 스마트폰에 앱이 설치됩니다.

     

     참고 문헌

    [논문]

    • 없음

    [보고서]

    • 없음

    [URL]

    • 없음

     

     문의사항

    [기상학/프로그래밍 언어]

    • sangho.lee.1990@gmail.com

    [해양학/천문학/빅데이터]

    • saimang0804@gmail.com
    • 네이버 블러그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기