정보

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

     

     기본 Widget I

    [학습목표]

    • Flutter가 객체 지향 프레임워크(Object Oriented Framework)라는 것을 이해합니다.
    • Widget의 기본적인 개념과 원리와 의미를 이해합니다.
    • Flutter가 제공하는 Widget을 추가, 수정 삭제하며 Widget 객체를 체험으로 익힙시다.
    • 기본적인 Widget들의 사용법을 익히고 코딩해 본다.

     

    [객체]

    • 앞에서 Dart의 자료형(Data Type)이 객체(Object)이고, Flutter의 레고블럭에 해당하는 위젯(Widget)도 객체(Object)라고 설명했습니다.
    • 하나의 객체는 객체의 상태(State)나 속성(Property)을 나타내는 멤버 변수(Variable)와 객체의 행동(Behavior)을 나타내는 멤버 함수(Function) 혹은 메소드(Method)로 구성됩니다.
    • 객체를 만드는 것은 어려우나 만들어진 객체를 사용하는 것은 쉬운데, 먼저 앞에서 설명한 코드들 중에서 객체를 만들어 사용하는 예를 들어 객체를 만들어 사용하는 법을 설명하겠습니다.
    • 객체는 클래스(Class)를 사용하여 객체를 만들어 사용합니다. 클래스의 이름은 변수나 속성과 달리 대문자로 시작합니다.

     

    [객체의 생성 - 속성이 없는 경우]

    • 아래의 코드에서 Placeholder는 int나 String과 같은 클래스(Class)입니다. Placeholder()과 같이 클래스 이름에 괄호를 붙이면 객체가 생성됩니다.
    Placeholder()

     

    [객체의 생성 - 속성을 특정 위치에 부여하는 경우]

    • 아래의 코드를 보면 'App Bar의 타이틀' 문자열을 Text 클래스의 첫번째 속성으로 넘겨주고 있습니다.

     

    [객체의 생성 - 속성을 이름으로 부여하는 경우]

    • 아래의 코드를 보면 home:과 같이 속성의 이름을 소문자로 주고 그뒤에 Placeholder() 값을 주어 MaterialApp() 객체를 생성하고 있습니다. Placeholder()와 같은 객체도 정수나 문자열 등의 다른 자료형과 같이 값으로 사용될 수 있습니다.
    MaterialApp(
    	home: Placeholder(),
    )

     

    [객체의 생성 - 속성을 여러개 지정하는 경우]

    • 위치로 지정하는 속성을 앞에 오게 하고, 이름으로 지정하는 속성을 뒤에 배치하는데 이름으로 지정하는 속성간의 순서는 상관없습니다. 속성들은 콤마(,)로 구분합니다.
    Text(
      'Scaffold의 몸통(body)',
            style: TextStyle(
        color: Colors.white,
        fontSize: 26,
      ),
    ),

     

    [객체의 메소드]

    • 아래의 코드를 보면 정수형 객체인 ageInt에 toString() 메소드가 존재하여 정수형 자료형을 문자열 자료형으로 변환해 주고 있습니다.
    int ageInt = 42;
    String ageString = ageInt.toString();

     

    [Flutter 프레임워크 코드 분석 I]

    • Instructor Note : 안드로이드 스튜디오의 Font 크기를 20으로 키우고, 브라우저의 화면도 글자가 충분히 크게 보이도록 키울 것.
    • Instructor Note : 코드를 작성하며 설명을 할 때에는 중간에 멈추고 수강생들에게 생각할 시간과 따라칠 시간을 주는 주기가 한화면을 넘지 않아야 함. → 중간에 따라 오다가 포기하는 경우가 많음.
    • 안드로이드 스튜디오의 왼쪽 창을 보면 Flutter의 소스 파일을 가지고 있는 lib 폴더가 보입니다. lib 폴더 안에 있는 main.dart가 Flutter 프레임워크에 의하여 기본적으로 제공되는 소스 파일로 Flutter 프로젝트가 실행될 때 최초로 수행되는 파일입니다.

     

    • main.dart 소스 코드의 최상위 계층만 보이도록 우측 화살표(>) 모양의 아이콘을 눌러 코드들을 축소시켜 봅시다. 그러면 Flutter 프레임워크가 import 문장 하나와 main() 함수와 3개의 클래스로 구성되는 것을 이해할 수 있습니다.

     

    • 이 상태에서 위의 코드를 보면 import 문장과 main() 함수는 Flutter App 만들기 기초에서 설명하였고, main() 함수가 최초로 실행되며 runApp(const MyApp()); 문장으로 MyApp 객체를 생성하여 실행시킵니다. 그래서 MyApp 객체안에서 MaterialApp 위젯 객체가 호출되어 Flutter 프레임워크가 실행됩니다.
    • 그 뒤에 나오는 MyHomePage _MyHomePageState 2개의 클래스가 first_flutter 프로젝트 실행시 나타나는 화면을 제공해 줍니다.
    • Flutter 프레임워크에서 우리가 가장 관심을 가질 코드의 목적지는 _MyHomePageState 클래스입니다. Flutter 프레임워크는 main() 함수 → MyApp 객체 → MyHomePage 객체를 거쳐 최종적으로 _MyHomePageState 객체가 실행되게 만들어 놓았으며 _MyHomePageState 객체에 우리가 이뮬레이터를 통해서 보았던 화면을 구성하는 코드들이 들어 있기 때문입니다.
    • 이번에는 _MyHomePageState 클래스의 코드를 확장하여 주석을 제외한 코드만 확인해 보겠습니다. _MyHomePageState 클래스는 아래 코드에서 확인할 수 있는 것과 같이 _counter 속성 변수 하나에 2개의 메소드 함수로 구성된 클래스입니다.
    • 코드의 큰그림을 이해했으면 조금 더 세부적으로 이해해 봅시다.
    • 숫자를 증가시키며 화면에 보여주는 기능을 가지고 있으니 숫자를 저장하기 위하여 int _counter = 0;와 같이 속성 변수가 하나가 정의되어 초기값을 0으로 설정하고 있습니다.
    • _incrementCounter() 메소드가 _counter 변수의 값을 증가시키는 기능을 합니다.
    • build() 메소드가 화면에 보여주는 역할을 하는데 화면에 보여지는 것은 Scaffold 객체입니다.
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      void _incrementCounter() {
        setState(() {
          _counter++;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }

     

    • 위의 코드는 직관적으로는 이해가 될지 모르나 이론적으로는 정확하게 모르는 것이 많을텐데 앞에서 여러번 강조했습니다. 프레임워크는 처음부터 모두 이해하려고 하지 않고 큰 그림 위주로 이해하기 바랍니다.
    • 아무튼 코드의 이해도를 더 높이기 위하여 프로그램을 Pixel 8 Pro 가상 장치에서 실행한 화면과 코드 안의 객체들과 속성을 연관지어 이해해 보겠습니다. 코드의 객체와 실행화면의 객체을 연결하여 이해해 보고 가겠습니다.

     

    • Instructor Note : 객체의 최하위까지 코드를 풀어 가면서 코드의 객체와 실행화면의 객체를 연결해 볼 것. appBar 속성을 backgroundColor와 title을 모두 바꾸어 가면서 실행화면의 변화와 비교하여 설명하고, body 속성은 Text의 내용을 바꾸는 정도로 설명한 후 원복해 두고, floatingActionButton 속성은 플러스 버튼(+) 나타나게 해준다는 정도만 설명하고 넘어갈 것.
    • 지금까지 설명한 코드와 화면들을 보면서 Flutter 프레임워크가 객체로 구성되어 있고 객체를 변경하는 것으로 화면이 모양과 동작이 달라지는 것을 이해하여야 합니다. 화면을 그려주는 HTML과 같은 보조 언어가가 없이 모두 객체의 속성으로 처리되며, CSS와 같이 화면에 보여지는 모습을 조절해 주는 보조 언어도없이 이 또한 모두 객체의 속성으로 처리됩니다. 우리는 Dart 언어 하나만 알면 앱프로그램을 만들 수 있습니다.
    • 위에서 설명한 것을 그림으로 그려 보면 아래와 같습니다.

     

    • 위의 그림으로는 객체의 계층이 보이지 않으니 속성은 제외하고 객체의 계층만 다시 그려 보겠습니다.

     

    • 위의 그림을 보면 Flutter 앱은 객체들의 계층으로 구성된 객체지향프로그램이라는 것을 알 수 있습니다. 웹의 경우 Java 언어는 객체지향이었지만 HTML와 CSS는 객체지향이 아니었는데 Flutter는 화면의 구성요소와 Dart 언어를 포함하여 모든 것이 객체입니다.
    • 예제. 앱바의 타이틀을 “나의 첫번째 Flutter 앱”이라고 수정해 보세요. Flutter 프레임워크의 규칙에 따르면 앱바의 타이틀을 MyApp 클래스의 home: const MyHomePage(title: 'Flutter Demo Home Page') 문장에서 수정하게 되어 있습니다.
    • 예제. 화면의 중앙에 자신의 팀 이름과 수강생 여러분의 이름이 추가되도록 수정해 보세요.
    • 예제. 화면 하단에서 + 버튼을 눌렀을 때 숫자가 2씩 증가하도록 수정해 보세요. 코드를 자세히 설명하지 않았지만 직관적으로 코드를 수정하여 구현할 수 있을 것입니다.
    import 'package:flutter/material.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(title: '나의 첫번째 Flutter 앱'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key, required this.title});
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      void _incrementCounter() {
        setState(() {
          // _counter = _counter + 2;
          _counter += 2;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  '팀 이름: [Flutter] 쉽게 배우는 플러터 앱 개발 실무',
                  style: TextStyle(
                      fontSize: 18,
                      color: Colors.grey,
                      fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 10),
                const Text(
                  '수강생: 이상호',
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.w500,
                    color: Colors.grey,
                  ),
                ),
                const SizedBox(height: 20),
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          ),
    
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }

     

     

    [Widget이란?]

    • Widget 대하여 Flutter App 만들기 기초에서도 언급하였는데 어렵게 생각할 것이 없습니다. 화면을 구성하고 조작하기 위한 **객체(Object)**입니다. 대부분 Flutter 프레임워크가 제공하는 Widget을 사용하게 될 것이며 아주 특별한 경우에만 기존에 있던 Widget 객체를 상속받아 수정하여 사용하게 될 것입니다.
    • 1. 화면에 보여지는 UI Widget
    텍스트 이미지
    아이콘들 버튼
    앱 바  
     

     

    • 2. 화면의 배열 / 정렬 Widget
    가운데 정렬 세로 배치
    가로 배치 바둑판 배열

     

    • 3. 화면의 뼈대

    • 이 외에도 눈에 보이지 않는 수많은 것들이 위젯 객체로 이뤄져 있습니다!
    • Flutter Widget 카탈로그
     

    Flutter Widget of the Week

    Get your weekly Flutter widget tips here! Get started with Flutter → https://goo.gle/3IoQovn

    www.youtube.com

     

     

    Flutter Gems - A Curated List of Top Dart and Flutter packages

    Flutter Gems is a curated list of top Dart and Flutter packages that are categorized based on functionality. Flutter Gems is also a visual alternative to pub.dev

    fluttergems.dev

     

    • 백문이 불여일견인데 first_flutter 앱코드를 수정하여 실행해 보며 Widget에 대한 이해를 높여 갑시다. 그동안 객체지향프로그램을 배운다고 고생이 많았는데 이번 경험을 통하여 객체지향프로그램이 얼마나 쉽고 강력한지 알게 될 것입니다.
    • Center Widget을 지우고 실행해 봅시다. Center Widget 위에 마우스 커서를 두고 Alter+Enter 키를 쳐 보세요. 그리고 화면에 나타나는 팝업 메뉴에서 Remove this widget을 선택합니다.

     

    • 이제 main.dart 프로그램을 실행해 보세요. 실행된 화면을 보니 화면 중앙에 나타나던 글자(Text Widget)들이 왼쪽으로 치우쳐 나타나지요? 실행화면을 보니 Center Widget이 저절로 이해가 되지요?
    • Center Widget을 다시 추가하여야 하겠습니다. 이번에는 Column Widget 위에 마우스 커서를 두고 다시 Alter+Enter 키를 쳐 보세요. 그리고 화면에 나타나는 팝업 메뉴에서 Wrap with Center을 선택합니다.

     

    • Column Widget을 Row Widget으로 변경한 후 실행해 봅시다. 두 Widget의 특성이 비슷하니 Column을 Row로 변경하기만 하면 됩니다. 이제 main.dart 프로그램을 다시 실행해 보세요.
    • 실행된 화면을 보니 상하로 배열되던 Text Widget들이 좌우로 배열되지요? 다시 Row Widget을 Column Widget으로 복원해 둡시다.
    • Text Widget의 색깔과 크기를 변경한 후 실행해 봅시다. 아래의 변경전 코드를 변경후 코드와 같이 변경합시다. 변경 후 코드에서 style 인자에 TextStyle 객체를 넘겨서 글자의 크기와 색깔을 바꾸는 것을 눈여겨 보기 바랍니다.
    // 변경전 Text Widget
    const Text(
      'You have pushed the button this many times:',
    ),
    
    // 변경후 Text Widget
    const Text(
      'You have pushed the button this many times:',
      style: TextStyle(color: Colors.pink, fontSize: 40),  // 추가되는 코드
    ),

     

    • 실행해 보니 간단히 글자의 크기와 색상이 바뀌어 나타납니다. 웹을 만드는 기술에서는 CSS, HTML, JavaScript, Java 및 Spring으로 분리되어 구성되던 로직과 화면 구성이, Flutter에서는 하나의 객체로 아니 하나의 객체들의 계층으로 처리되어 코드가 매우 단순하고 직관적으로 변했습니다.
    • 이것이 객체지향프로그램의 힘이고 언젠가는 웹 프로그램도 이런 기술들이 적용되며 진화해 갈 것으로 믿어 의심하지 않습니다. 아직은 미미한 움직임이지만 Flutter가 제공하는 플랫폼에 Android와 iOS 외에 웹이 추가되어 있는 것도 우연은 아니라고 생각합니다.
    • Icon Widget을 숫자 뒤에 추가한 후 실행해 봅시다. 이번에는 매번 실행 버튼을 누르지 말고 아래에 나오는 지침을 참조하여 실행해 보기 바랍니다.
        Text(
          '$_counter',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
        Icon(Icons.access_alarm),   // 추가되는 코드
      ],

     

    • 실행 아이콘을 누르면 앱이 재시작되어 오래 걸리지만

     

    • Flutter Hot Reload 아이콘을 누르면 단순한 화면 변경은 앱을 재시작하지 않고 빠르게 이뮬레이터에 Update해 줍니다.

     

    • 이로써 Flutter 프레임워크가 처음으로 생성하여 넣어준 Widget들을 알아 보았습니다. 이와 같이 Scaffold, AppBar, Center, Text, Theme, Column, Icon 및 FloatingActionButton 등 아래 객체의 계층에 나오는 것들이 모두 Widget입니다.

    • 그러면 이제 각각의 Widget들에 대하여 하나씩 알아 가도록 합시다.

     

    [Scaffold Widget]

    • 대부분의 스마트폰 앱들의 화면은 아래 그림과 같은 전형적인 구조를 가지는데 Scaffold Widget은 이런 화면의 구조를 지원하는 Widget입니다. Flutter가 기본적으로 제공하는 코드에서는 아래 화면에 나오는 bottomNavigationBar 속성 대신 floatingActionButton 속성이 사용되었습니다.

     

    • Scaffold Widget에는 appBar, body, floatingActionButton 및 bottomNavigationBar 속성 외에도 아래 Constructors 화면에서 보는 것과 같은 많은 속성들이 있습니다. 한번에 모두 배울 필요는 없고 필요한 것만 필요할 때 찾아서 쓰면 됩니다.

     

    • Widget에 대한 기본적인 개념과 원리와 의미를 설명한 후 상세한 설명에 대한 Link를 달아 놓겠습니다. 필요시 참조하기 바랍니다. docs.flutter.dev에서 개발 흐름과 기본 개념을 배우고, 필요한 경우 구체적인 클래스와 메서드의 사용법을 알아보려면 api.flutter.dev를 참고합니다.
     

    Scaffold class - material library - Dart API

    Implements the basic Material Design visual layout structure. This class provides APIs for showing drawers and bottom sheets. To display a persistent bottom sheet, obtain the ScaffoldState for the current BuildContext via Scaffold.of and use the ScaffoldSt

    api.flutter.dev

     

    • Scaffold Widget에 대한 이해를 돕기 위하여 Flutter가 기본적으로 제공한 소스코드에서 Scaffold에 관련된 코드만 발췌해 보면 다음과 같습니다. 설명하지 않은 FloatingActionButton Widget을 제외하고는 이제 모두 이해가 될 것으로 믿습니다.
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
            ... 중략 ...
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
            ),
    );

     

    [텍스트(Text) 관련 Widget]

    • Flutter의 기본 Widget들은 크게 다음과 같이 4개의 분류로 나눌 수 있습니다.

     

    • Text Widget에서 글자의 배치와 글씨 스타일과 배경색을 바꾸는 방법을 추가로 알아 보겠습니다. 글자의 배치는 Text Widget의 textAlign 속성으로 지정하는데 지정할 수 있는 종류는 TextAlign.center와 같이 지정할 수 있습니다. 글자의 배경색은 Text Widget의 style 속성으로 넘겨지는 TextStyle 객체의 backgroundColor 속성으로 지정합니다. 객체의 속성 지정이 많아지는 경우 한줄에 하나의 속성을 나열하는 것이 Flutter 프로그래머들의 관행입니다. 코드의 가독성이 매우 높은 습관입니다.
    • Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → 여기서는 추가되는 코드를 한줄씩 모두 분리하여 보여줄 수 있음
    // 변경전 Text Widget
    const Text(
      'You have pushed the button this many times:',
      style: TextStyle(color: Colors.pink, fontSize: 40),
    ),
    
    // 변경후 Text Widget
    const Text(
      'You have pushed the button this many times:',
      textAlign: TextAlign.center,        // 추가되는 코드
      style: TextStyle(
          color: Colors.pink,
          fontSize: 40,
          backgroundColor: Colors.yellow, // 추가되는 코드
          fontStyle: FontStyle.italic,    // 추가되는 코드
      ),
    ),

     

    • 실행해 보면 글자의 배경색과 글씨 스타일과 배치가 원하는대로 되는 것을 알 수 있습니다.

     

    • 위의 코드에서 TextAlign과 Colors는 앞에서 설명했던 Icons와 같은 열거형(enum)인데 이름의 뒤에 점(.)을 찍으면 선택할 수 있는 모든 종류를 알 수 있습니다. 외우지 말고 찾아서 선택합시다. 열거형은 마치 속성만으로 구성된 객체처럼 보이고 코딩하는 방법도 그와 유사합니다.
    • Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → TextAlign 열거형을 하나씩 바꾸면서 실행화면을 보여 줄 것 (start와 end는 아랍이나 예전의 한자와 같이 오른쪽에서 왼쪽으로 쓰기를 고려한 정렬이고 left와 right는 단순히 왼쪽과 오른쪽을 의미합니다.)

     

     

     

    • TextStyle 객체에 대한 전체적인 이해도 Scaffold Widget 객체와 같이 생성자(Constructor)를 보면 알 수 있습니다. 쉽게 표현해서 웹을 개발할 때에 지원되는 대부분의 기능들이 구현되어 있다고 보면 됩니다. 필요할 때 필요한 만큼 찾아서 사용하기 바랍니다.

     

     

    TextStyle class - painting library - Dart API

    An immutable style describing how to format and paint text. Bold Here, a single line of text in a Text widget is given a specific style override. The style is mixed with the ambient DefaultTextStyle by the Text widget. link content_copy const Text( 'No, we

    api.flutter.dev

     

     

    Text class - widgets library - Dart API

    A run of text with a single style. The Text widget displays a string of text with single style. The string might break across multiple lines or might all be displayed on the same line depending on the layout constraints. The style argument is optional. Whe

    api.flutter.dev

     

    [디자인(Design) 관련 Widget]

    • Icon Widget
    • 이번에는 Icon Widget을 살짝 개선해 보는 것으로 Icon Widget에 대한 설명을 대신합니다. 기술적인 사항들은 Text Widget을 설명할 때 했기 때문에 반복하지 않습니다.
    // 변경전 Icon Widget
    const Icon(Icons.access_alarm),
    
    // 변경후 Icon Widget
    const Icon(
        Icons.access_alarm,
        color: Colors.green,    // 추가되는 코드
        size: 100,              // 추가되는 코드
    ),

     

    • 실행해 보면 아이콘의 색깔과 크기가 원하는대로 되는 것을 알 수 있습니다.

     

     

    Icon class - widgets library - Dart API

    A graphical icon widget drawn with a glyph from a font described in an IconData such as material's predefined IconDatas in Icons. Icons are not interactive. For an interactive icon, consider material's IconButton. There must be an ambient Directionality wi

    api.flutter.dev

     

    [Image Widget]

    • Image Widget을 사용할 때 Image.network() 메소드를 사용하면 Network 상의 Image를 가져다 보여줄 수 있습니다. Image.network()과 같이 객체를 생성해 주는 메소드를 팩토리 메소드(Factory Method) 혹은 팩토리 생성자(Factory Constructor)라고 부릅니다. 객체를 공장에서 만들어 주듯이 만들어 준다고 기억하면 될 것 같습니다. 실제로 객체를 공장에서 만들어 주는 것처럼, 객체 생성의 복잡한 로직을 캡슐화하여 간편하게 인스턴스를 생성할 수 있게 해줍니다.
    • Instructor Note : 구글링으로 “알라딘 서점”에 가면 도서 이미지의 URL을 복사해 오기 용이합니다.
    // 변경전 코드
    const Icon(
        Icons.access_alarm,
        color: Colors.green,
        size: 100,
    ),
    
    // 변경후 코드
    const Icon(
        Icons.access_alarm,
        color: Colors.green,
        size: 100,
    ),      // 추가된 코드 (아래 한줄)
    Image.network("https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg"),

     

    • Image.network() 대신 Image.asset() 메소드를 사용하면 PC에 있는 Image를 가져다 보여줄 수 있습니다.
    • 그런데 개발 중인 프로젝트에서 Image File을 가져다 사용하려면 소정의 절차를 거쳐야 합니다. 자기가 사용 중인 PC에 위치한 Image File을 모두 가져다 사용할 수 있다면 언젠가 관리가 불가능한 상태 빠지겠지요. 그리고 사용할 이미지를 포함한 자원들을 한곳에 모아 놓으면 개발 환경에서 운영 환경으로 이관할 때에도 편리할 것입니다.
    • 먼저 test와 lib 폴더와 같은 레벨로 images 폴더를 생성합니다.

     

    • 그리고 pubspec.yaml 파일을 더블 클릭하여 편집기에서 열고 assets 설정 부분으로 이동합니다. pubspec.yaml은 Flutter를 사용하기 위한 설정을 하는 파일이고, assets은 개발 중인 프로젝트가 사용할 이미자나 폰트 등의 자원을 등록하는 설정입니다.

     

    • 설정을 아래와 같이 변경합니다. yaml 파일은 시스템 설정을 위하여 고안된 언어인데실수를 하면 안되기 때문에 주의하여 수정하여야 합니다. 이미지 파일을 사용하는 절차를 거칠 때 파일별로 승인을 해 주는 것이 보안을 위해 좋은 방법인데 작업의 편의를 위하여 폴더 전체를 허용하겠습니다.

     

    • pubspec.yaml 파일을 수정하면 편집기의 상단에 Pub get 메뉴가 나타납니다. 설정이 변경되었으니 프로젝트에 반영하라는 의미입니다. Pub get 메뉴를 클릭합니다.

     

    • Pub get 메뉴를 누른 후에는 안드로이드 스튜디오의 하단에 Process finished with exit code 0 메세지가 나타나며 설정이 정상적으로 반영되는 것을 확인하여야 합니다. 안타깝게도 이런 설정 작업을 새로운 프로젝트를 만들때마다 반복하여야 합니다.

     

    • 이제 만들어진 images 폴더에 사용할 이미지 파일을 복사해 넣읍시다. 저는 점프 투 파이썬.jpg 파일을 복사해 넣었습니다.

     

    // 변경전 코드
    const Icon(
        Icons.access_alarm,
        color: Colors.green,
        size: 100,
    ),
    Image.network("https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg"),
    
    // 변경후 코드
    const Icon(
        Icons.access_alarm,
        color: Colors.green,
        size: 100,
    ),
    Image.network("https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg"),
    Image.asset("images/점프 투 파이썬.jpg"),   // 추가된 코드

     

    • 지금까지 만들어진 화면의 형태는 아래와 같습니다. 책은 동일한 주제의 이미지이니 상하로 배치하지 말고 좌우로 배치해볼까요?

     

    • 이번에는 아래 화면과 같이 image와 관련된 코드를 마우스로 선택하여 묶은 후 Alt+Enter 키를 친 후 Wrap with Row 메뉴를 선택합니다.

     

    • 그러면 아래 화면과 같이 선택된 문장들을 모두 Row Widget이 감싸게 됩니다. 이때 Row Widget은 여러개의 하위 Widget을 가지게 되니 속성의 이름이 children으로 주어지고 Widget이 여러개이니 리스 기호 [ ]로 묶여지게 됩니다. 논리적으로 조금만 생각하면 당연합니다.

     

    • 또 당연하겠지만 여러개의 하부 객체를 가지지 않고 하나의 객체를 가지는 경우 아래의 화면과 같이 속성의 이름이 child가 되고 또 당연하게 리스트 기호 [ ]로 묶어지지도 않습니다.

     

    • 내친김에 이미지를 하나 더 추가해 보겠습니다. 이미지를 images 폴더에 복사해 넣고, 코드를 아래와 같이 추가합니다.
    // 변경후 코드
    Row(
      children: [
        Image.network("https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg"),
        Image.asset("images/점프 투 파이썬.jpg"),
        Image.asset("images/생각하는 프로그래밍.jpg"),   // 추가된 코드
      ],
    ),

     

    • 그리고 프로그램을 수행해 보면 아래 화면의 우측과 같이 공간이 부족한 곳에 빗금이 나타납니다. 웹 프로그램이었다면 화면의 공간이 넓어지며 자동으로 좌우 스크롤바가 나타났을텐데 말입니다.

     

    • Image Widget에 width 속성과 height 속성을 조정하여 해결해 봅시다. 코드를 두군데에서 수정하여야 합니다. 첫번째로 가상장치의 넓이를 알아내어 3등분하는 로직을 만들어 보겠습니다.
    // 변경전 코드
      @override
      Widget build(BuildContext context) {
        // This method is rerun every time setState is called, for instance as done
        // by the _incrementCounter method above.
        //
        // The Flutter framework has been optimized to make rerunning build methods
        // fast, so that you can just rebuild anything that needs updating rather
        // than having to individually change instances of widgets.
        return Scaffold(
          appBar: AppBar(

     

    • MediaQuery는 장치의 화면 및 환경 정보를 제공하는 객체입니다. MediaQuery.of() 메소드를 사용하여 현재 사용 중인 장치의 화면을 의미하는 context의 장치 정보를 가지게 됩니다. 그중에서 size 속성 하부의 width 속성으로 화면의 넓이를 알 수 있습니다. 화면의 넓이를 3으로 나누어 세장의 이미지가 화면에 들어갈 수 있도록 이미지의 넓이를 구하여 imageWidth 변수에 저장하였습니다.
    // 변경후 코드
      @override
      Widget build(BuildContext context) {
        double imageWidth = MediaQuery.of(context).size.width / 3;  // 추가된 코드
    
        return Scaffold(
          appBar: AppBar(

     

    • 두번째로 화면의 넓이를 계산한 변수를 Image Widget의 width 속성에 추가해 보겠습니다. 높이에는 아직 별 문제가 없어서 height 속성은 지정하지 않았습니다.
    // 변경후 코드
    Row(
      children: [
        Image.network(
          "https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg",
          width: imageWidth,  // 추가된 코드
        ),
        Image.asset(
          "images/점프 투 파이썬.jpg",
          width: imageWidth,  // 추가된 코드
        ),
        Image.asset(
          "images/생각하는 프로그래밍.jpg",
          width: imageWidth,  // 추가된 코드
        ),
      ],
    ),

     

    • 이렇게 해서 오류가 해결되었고 우리의 Flutter에 대한 이해도도 함께 향상 되었습니다.

     

     

    Image class - widgets library - Dart API

    A widget that displays an image. Several constructors are provided for the various ways that an image can be specified: The following image formats are supported: JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP. Additional formats may be s

    api.flutter.dev

     

    [Container Widget]

    • Container Widget은 자식(child) Widget의 크기를 지정하고 다양하게 꾸밀 수 있게 해 줍니다.
    • 화면의 중앙에 작고 외롭게 고립되어 있는 카운트를 보여주는 Text Widget을 Container Widget으로 감싸서 꾸며 봅시다. 이제부터는 Alt+Enter 키를 쳐서 Widget을 추가하는 등의 설명은 반복하지 않습니다. Flutter 코드는 객체가 계층적으로 복잡하게 상호 연결되어 타이핑으로 Widget을 추가하거나 삭제하면 실수가 발생하기 쉬우니 Widget을 추가하거나 삭제할 때에는 꼭 Alt-Enter 키를 사용하기 바랍니다.
    • 아래의 코드에서 Theme.of(context).textTheme.headlineMedium 코드는 앞에서 설명한 MediaQuery.of(context).size.width 코드에 비추어 이해할 수 있는데 현재 사용 중인 화면(context)의 테마(Theme) 중 텍스트 테마(textTheme)에서 headlineMedium 스타일을 가져다 사용하는 것 정도로 이해할 수 있습니다.
    // 변경전 코드
    Text(
      '$_counter',
      style: Theme.of(context).textTheme.headlineMedium,
    ),

     

    • 아래 코드를 보면 Container의 배경색이나 테두리 등을 decoration 속성과 BoxDecoration 객체로 처리하고 있습니다. Container를 다양하게 꾸미기 위하여 객체의 계층을 하나 더 추가한 것으로 이해하면 됩니다. Border.all(), BorderRadius.all() 및 BorderRadius.only() 등이 팩토리 메소드 혹은 생성자 메소드라는 것을 주의깊게 이해하면 코드가 조금 길지만 속성 단위로 객체 단위로 떼어서 이해하면 큰 무리없이 이해할 수 있을 것입니다. 객체를 만드는 것은 어렵지만 객체를 사용하는 것은 어려운 일이 아닙니다.
    • Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → 여기서는 추가되는 코드를 한줄씩 모두 분리하여 보여줄 수 있음
    // 변경후 코드
    Container(        // 추가된 Widget
      width: 100,                   // 추가된 코드
      height: 100,                  // 추가된 코드
      alignment: Alignment.center,  // 추가된 코드
      decoration: BoxDecoration(    // decoration 추가 시작
        color: Colors.blueAccent,
        border: Border.all(color: Colors.black,width: 8),
    
    
        borderRadius: BorderRadius.all(Radius.circular(10)),   // 추가된 코드
        //borderRadius: BorderRadius.all(Radius.circular(30)),
        //borderRadius: BorderRadius.all(Radius.circular(50)),
    
        //borderRadius: BorderRadius.horizontal(left: Radius.zero, right: Radius.circular(50)),
        //borderRadius: BorderRadius.vertical(top: Radius.circular(50), bottom: Radius.zero),
    
        //borderRadius: BorderRadius.only(topLeft: Radius.circular(50)),
        //borderRadius: BorderRadius.only(bottomRight: Radius.circular(50)),
      ),                            // decoration 추가의 끝
      child: Text(
        '$_counter',
        style: Theme.of(context).textTheme.headlineMedium,
      ),
    ),

     

    • Flutter에서 width: 100과 같은 숫자는 논리적 픽셀(Logical Pixel) 단위로 사용됩니다. 논리적 픽셀을 사용한다는 의미는 다양한 해상도와 화면 크기의 디바이스에 상관없이 화면 밀도에 맞추어 자동으로 크기가 조정되기 때문에 프로그래머가 디바이스를 신경쓰지 않고 코딩을 할 수 있다는 의미입니다. 그래도 장치마다 특성이 달라서 조금씩 다르게 화면에 나타나기 때문에 앞에서 MediaQuery 객체를 사용하는 예를 든 것과 같이 미세하게 조정하여야 합니다.
    • 실행해 보면 화면의 중앙에 Container Widget이 나타납니다.

     

    • Instructor Note : 소스 코드에서 각각이 사용되는 위치를 보여줄 것
    • 혹시 위의 코드에서 속성의 이름 alignment와 속성을 지정하기 위하여 사용된 Alignment 열거형의 이름이 첫번째 글자만 다른 것을 눈치챘나요? 이런 형태는 fontStyle과 FontStyle, textAlign과 TextAlign 등에서도 동일하게 나타나는 현상으로 이름을 부여하는 부담을 크게 줄여 줍니다.

     

    • 앞에서 코딩해 보았던 것들 중에 오른쪽 화면이 마음에 들지 않습니다. 글자의 배경색이 글자 뒤에 만있고 좌우에 하얀색 공백이 영 눈에 거슬립니다. Container Widget을 사용해서 개선해 봅시다.
    // 변경전 코드
    const Text(
      'You have pushed the button this many times:',
      textAlign: TextAlign.center,
      style: TextStyle(
          color: Colors.pink,
          fontSize: 40,
          backgroundColor: Colors.yellow,
          fontStyle: FontStyle.italic,
      ),
    ),

     

    • Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → 여기서는 Container Widget의 속성을 먼저 보여 주어 하늘색(cyan)과 노란색(yellow) 색상이 겹치는 것을 보여준 후, TextStyle 객체의 backgroundColor 속성을 주석처리한 후 color: Colors.cyan을 color: Colors.yellow으로 변경
    // 변경후 코드
    Container(
      width: double.infinity,       // 추가된 코드 (장치의 전체 넓이 사용)
                                                  // height 속성 미사용시 child의 높이 사용
      alignment: Alignment.center,  // 추가된 코드
      //color: Colors.cyan,         // 추가된 코드
      color: Colors.yellow,         // decoration 속성 사용시 삭제(오류 발생)
      child: const Text(
        'You have pushed the button this many times:',
        textAlign: TextAlign.center,
        style: TextStyle(
            color: Colors.pink,
            fontSize: 40,
            //backgroundColor: Colors.yellow, // Container의 color 속성을 대신 사용
            fontStyle: FontStyle.italic,
        ),
      ),
    ),

     

    • 실행화면에서 노란색 배경이 스마트폰의 좌우로 꽉 차니 훨씬 보기가 좋습니다.

     

    • Container Widget의 margin과 padding 속성을 차례로 적용해 보면 공란이 노란색 바탕의 바깥쪽에 생기면 margin이고, 노란색 바탕의 안쪽에 생기면 padding인 것의 차이를 가시적으로 확인할 수 있습니다.
    // 변경후 코드
    Container(
      width: double.infinity,
      alignment: Alignment.center,
      color: Colors.yellow,
      margin: EdgeInsets.all(50),   // 추가된 코드
      padding: EdgeInsets.all(50),  // 추가된 코드
      child: const Text(
        'You have pushed the button this many times:',
        textAlign: TextAlign.center,
        style: TextStyle(
            color: Colors.pink,
            fontSize: 40,
            fontStyle: FontStyle.italic,
        ),
      ),
    ),

     

    • 오른쪽 실행화면의 노란색의 바깥 공간이 margin이고

     

    • 오른쪽 실행화면의 노란색의 안쪽 공간이 padding입니다.

     

    • 예제. Container Widget에 margin 속성을 부여하여 어떻게 변하는지 관찰해 보기 바랍니다. 아래에 몇가지 코드의 예를 들어 보았습니다.
    // 상하좌우의 Margin이 모두 동일할 경우
    margin: EdgeInsets.all(50), 
    
    // 상하의 Margin이 동일하고, 좌우의 Margin이 각각 동일한 경우
    margin: const EdgeInsets.symmetric(
      vertical: 10.0,    // 상하 margin
      horizontal: 20.0,  // 좌우 margin
    ),
    
    // 상하좌우의 Margin이 모두 다른 경우 (좌 10, 상 20, 우 30, 하 40)
    margin: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0),
    
    // 필요한 곳에만 Margin을 추가하는 경우
    margin: EdgeInsets.only(
      right: 10,    // 오른쪽 여백
      bottom: 40,   // 하단 여백
    ),

     

    • 예제. Container Widget에 padding 속성을 부여하여 어떻게 변하는지 관찰해 보기 바랍니다. 아래에 몇가지 코드의 예를 들어 보았습니다. Margin을 부여하는 방식과 Padding을 부여하는 방식이 동일하다는 점에 주목합시다.
    // 상하좌우의 Padding이 모두 동일할 경우
    padding: EdgeInsets.all(50), 
    
    // 상하의 Padding이 동일하고, 좌우의 Padding이 각각 동일한 경우
    padding: EdgeInsets.symmetric(
      vertical: 10.0,    // 상하 margin
      horizontal: 20.0,  // 좌우 margin
    ),
    
    // 상하좌우의 Padding이 모두 다른 경우 (좌 10, 상 20, 우 30, 하 40)
    padding : EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0),
    
    // 필요한 곳에만 Padding을 추가하는 경우
    padding: EdgeInsets.only(
      right: 10,    // 오른쪽 여백
      bottom: 40,   // 하단 여백
    ),

     

    • Flutter는 위의 BorderRadius나 EdgeInsets의 예와 같이 경우에 따라 팩토리 메소드(Factory Method) 혹은 생성자 메소드(Constructor Method)가 달라지는 코딩 패턴을 자주 사용합니다. 앞으로 유사한 사례가 나올 때 하나 하나 나열하여 설명하지 않을테니 위의 코딩 패턴을 파악하여 다른 곳에도 활용하기 바랍니다.
    • 화면의 공간 확보를 위하여 Container Widget의 margin과 padding 속성을 주석처리합시다.
     

    Container class - widgets library - Dart API

    A convenience widget that combines common painting, positioning, and sizing widgets. A container first surrounds the child with padding (inflated by any borders present in the decoration) and then applies additional constraints to the padded extent (incorp

    api.flutter.dev

     

    SizedBox Widget

    • SizedBox Widget은 child Widget의 크기를 결정하거나 Widget들간의 여백을 조정하기 위하여 사용합니다. child Widget의 크기 조정은 Container Widget을 대신 사용할 수 있어서 주로 여백을 조정하기 위한 목적으로 사용합니다.
    • 오른쪽의 화면에서 Column으로 나열된 Widget들과 Row로 나열된 Widget들 사이가 너무 좁아서 화면이 답답해 보입니다. 여백을 추가해 보겠습니다.

     

    • SizedBox Widget에서 여백을 주기 위하여 상하 여백에서는 height 속성만 부여하고, 좌우 여백을 주기 위해서는 width 속성만 부여합니다.
    • Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → 여기서는 SizedBox Widget를 하나싹 추가하며 실행화면이 변하는 것을 보여 주면서 추가하는 방식으로 빗금 화면이 나오는 것을 확인한 후 double imageWidth = (MediaQuery.of(context).size.width - 30) / 3; 문장을 추가하여 조치하는 형태로 보여줄 것
    • 그리고 이미지의 넓이를 계산할 때 여백을 고려하여 스마트폰의 넓이에서 여백의 총합을 빼고 계산해야 합니다. 여백을 부여하는 방식이 조금 촌스러워 보이지만 화면에 많은 Widget들이 들어가지 않는 앱의 특성을 고려할 때 크게 문제가 있어 보이지는 않습니다.
    // 변경후
      @override
      Widget build(BuildContext context) {
        double imageWidth = (MediaQuery.of(context).size.width - 20) / 3;  // 수정된 코드
                                       // 좌우 여백의 총합인 20을 빼고 이미지의 넓이를 계산
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Container(
                  width: double.infinity,
                  alignment: Alignment.center,
                  color: Colors.yellow,
                  child: const Text(
                    'You have pushed the button this many times:',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                        color: Colors.pink,
                        fontSize: 40,
                        fontStyle: FontStyle.italic,
                    ),
                  ),
                ),
                SizedBox(height: 30),        // 추가된 코드
                Container(
                  width: 100,
                  height: 100,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: Colors.blueAccent,
                    border: Border.all(color: Colors.black,width: 8),
                    borderRadius: BorderRadius.only(topLeft: Radius.circular(50)),
                  ),
                  child: Text(
                    '$_counter',
                    style: Theme.of(context).textTheme.headlineMedium,
                  ),
                ),
                SizedBox(height: 30),        // 추가된 코드
                const Icon(
                    Icons.access_alarm,
                    color: Colors.green,
                    size: 100,
                ),
                SizedBox(height: 30),        // 추가된 코드
                Row(
                  children: [
                    SizedBox(width: 5),      // 추가된 코드
                    Image.network(
                        "https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg",
                        width: imageWidth,
                    ),
                    SizedBox(width: 10),     // 추가된 코드
                    Image.asset(
                        "images/점프 투 파이썬.jpg",
                        width: imageWidth,
                    ),
                    SizedBox(width: 10),     // 추가된 코드
                    Image.asset(
                        "images/생각하는 프로그래밍.jpg",
                        width: imageWidth,
                    ),
                    SizedBox(width: 5),      // 추가된 코드
                  ],
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      }

     

    • 그래서 답답하던 Widget들 사이에 숨쉴 틈이 생겼습니다.

     

     

    SizedBox class - widgets library - Dart API

    A box with a specified size. If given a child, this widget forces it to have a specific width and/or height. These values will be ignored if this widget's parent does not permit them. For example, this happens if the parent is the screen (forces the child

    api.flutter.dev

     

    [배치(Layout) 관련 Widget]

     

    Column Widget/Row Widget

    • 앞에서 Row Widget과 Column Widget을 사용해 보면서 배웠습니다. 그래서 설명할 것이 많지 않지만 mainAxisAlignment 속성을 보고 가겠습니다.
    • 아래 코드를 보면 Column Widget의 mainAxisAlignment 속성을 MainAxisAlignment.center에서 MainAxisAlignment.spaceAround로 바꾸고, Row Widget의 mainAxisAlignment 속성으로 MainAxisAlignment.spaceAround을 추가한 후 촌스럽던 SizedBox Widget을 모두 삭제하였습니다. (삭제한 곳을 표시하기 위하여 주석처리하였습니다.) 그렇다고 SizedBox Widget의 용도가 폐기되는 것은 아닙니다. 여백을 미세 조정할 때 여전히 효과적입니다.
    • Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → Column을 먼저 보여 주고, Row를 나중에 보여줄 것
    // 변경후
      @override
      Widget build(BuildContext context) {
        double imageWidth = (MediaQuery.of(context).size.width - 20) / 3;
    
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              //mainAxisAlignment: MainAxisAlignment.center,     // 주석 처리
              mainAxisAlignment: MainAxisAlignment.spaceAround,  // spaceAround로 변경
              children: <Widget>[
                Container(
                  width: double.infinity,
                  alignment: Alignment.center,
                  color: Colors.yellow,
                  child: const Text(
                    'You have pushed the button this many times:',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                        color: Colors.pink,
                        fontSize: 40,
                        fontStyle: FontStyle.italic,
                    ),
                  ),
                ),
                //SizedBox(height: 30),        // 추가된 코드 주석 처리
                Container(
                  width: 100,
                  height: 100,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: Colors.blueAccent,
                    border: Border.all(color: Colors.black,width: 8),
                    borderRadius: BorderRadius.only(topLeft: Radius.circular(50)),
                  ),
                  child: Text(
                    '$_counter',
                    style: Theme.of(context).textTheme.headlineMedium,
                  ),
                ),
                //SizedBox(height: 30),        // 추가된 코드 주석 처리
                const Icon(
                    Icons.access_alarm,
                    color: Colors.green,
                    size: 100,
                ),
                //SizedBox(height: 30),        // 추가된 코드 주석 처리
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,  // 추가된 코드
                  children: [
                    //SizedBox(width: 5),      // 추가된 코드 주석 처리
                    Image.network(
                        "https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg",
                        width: imageWidth,
                    ),
                    //SizedBox(width: 10),     // 추가된 코드 주석 처리
                    Image.asset(
                        "images/점프 투 파이썬.jpg",
                        width: imageWidth,
                    ),
                    //SizedBox(width: 10),     // 추가된 코드 주석 처리
                    Image.asset(
                        "images/생각하는 프로그래밍.jpg",
                        width: imageWidth,
                    ),
                    //SizedBox(width: 5),      // 추가된 코드 주석 처리
                  ],
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      }

     

    • 그랬더니 코드가 더욱 간결해지면서 유사한 여백이 추가되는 효과를 얻게 되었습니다.

     

    • Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → mainAxisAlignment 속성으로 표현할 수 있는 것들을 코드를 변경하면서 보여 주고 아래의 설명은 노션으로 제공할 것 ← Row는 감지하기 어려우니 Column으로 보여 줄 것
    • mainAxisAlignment 속성에 지정할 수 있는 속성들을 MainAxisAlignment 열거형(enum)에 맞게 정리해 보면 아래와 같습니다.

     

     

    Column class - widgets library - Dart API

    A widget that displays its children in a vertical array. To cause a child to expand to fill the available vertical space, wrap the child in an Expanded widget. The Column widget does not scroll (and in general it is considered an error to have more childre

    api.flutter.dev

     

     

    Row class - widgets library - Dart API

    A widget that displays its children in a horizontal array. To cause a child to expand to fill the available horizontal space, wrap the child in an Expanded widget. The Row widget does not scroll (and in general it is considered an error to have more childr

    api.flutter.dev

     

    [Stack Widget]

    • Stack Widget은 Widget을 겹쳐 쌓아서(Stack) 다양한 효과를 내게 해 줍니다. 코드에 나오는 Widget의 순서대로 아래에서 위로 쌓입니다.
    • Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → 한 문장씩 추가하며 실행화면을 보여줄 것
    // Column의 child로 Stack Widget이 추가된 코드
    Stack(
      children: [
        Container(width: 150,height: 150,color: Colors.red),
        Container(width: 100,height: 100,color: Colors.yellowAccent),
        Container(width: 70,height: 70,color: Colors.green),
        Container(width: 50,height: 50,color: Colors.blue),
      ],
    ),

     

    • 실행해 보니 초록색이 맨 아래에 노란색이 그위에 그리고 그위에 빨간색 Widget이 쌓였습니다. 실행화면을 보니 아랫쪽에 쌓이는 Widget이 더 커야 한다는 것을 저절로 알게 됩니다.

     

    • Stack Widget의 alignment 속성을 사용하여 다양한 효과를 낼 수 있습니다.
    // Stack Widget에 alignment 속성이 추가된 코드
    Stack(
      alignment: Alignment.center,       // 추가된 코드
      children: [
        Container(width: 150,height: 150,color: Colors.red),
        Container(width: 100,height: 100,color: Colors.yellowAccent),
        Container(width: 70,height: 70,color: Colors.green),
        Container(width: 50,height: 50,color: Colors.blue),
      ],
    ),

     

     

    • 예제. Alignment 열거형(enum)에 지원되는 값들이 어떤 것이 있는지 알아 보려면 안드로이드 스튜디오 편집기에서 어떻게 하면 되는지 생각해 봅시다. 그리고 위의 Stack 코드에 대입하여 실행화면을 확인해 봅시다. 이런 방법이 객체의 속성과 메소드에 어떤 것들이 있는지 알아 보는 것과 어떤 차이가 있을까요?

     

    • Stack Widget을 Positioned Widget과 함께 사용하면 아래 화면의 오른쪽에 빨간색으로 둘러싼 아이콘과 같은 효과를 낼 수 있습니다.

     

    • 위에서 만들어 놓은 Stack Widget에 비슷한 효과를 내도록 만들어 보겠습니다. 아래 코드에서 Positioned Widget의 right 속성과 top 속성이 오른쪽과 윗쪽에서 0만큼 떨어진 위치라는 것을 이해하면 지금까지 배운 것으로 이해가 되는 코드입니다.
    • Instructor Note : 코드 전체를 완성하여 실행화면을 보여 주지 말고 실행화면이 달라지는 것을 보여줄 수 있다면 최대한 단계적으로 보여줄 것 → 아래 코드는 맨 하위의 Text → Positioned → Container → Center의 순으로 보여줄 것
    // Stack Widget에 숫자 뱃지가 추가된 코드
    Stack(
      alignment: Alignment.center,
      children: [
        Container(width: 150,height: 150,color: Colors.red),
        Container(width: 100,height: 100,color: Colors.yellowAccent),
        Container(width: 70,height: 70,color: Colors.green),
        Container(width: 50,height: 50,color: Colors.blue),
    
        Positioned(          // 추가된 Positioned Widget
          right: 0,          // 오른쪽에서 0 논리 픽셀만큼 떨어진 곳
          top: 0,            // 위에서 0 논리 픽셀만큼 떨어진 곳
          child: Container(
            width: 60,
            height: 60,                    
            alignment: Alignment.center,
            decoration: const BoxDecoration(
              color: Colors.blue,
              shape: BoxShape.circle,    // 원을 그리는 코드
            ),
            child: Center(
              child: Text(
                '1',
                style: TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.bold,    // 9 단계의 FontWeight 참조
                  fontSize: 40,
                ),
              ),
            ),
          ),
        ),
      ],
    ),
                
      // 9단계의 폰트 굵기
      // w100: Thin
      // w200: Extra Light
      // w300: Light
      // w400: Regular (Normal)
      // w500: Medium
      // w600: Semi Bold
      // w700: Bold
      // w800: Extra Bold
      // w900: Black

     

    • 실행해 보니 오른쪽과 같은 모양으로 변하였습니다.

     

    • 예제. 위의 Stack Widget의 우측 상단에 배치된 뱃지를 좌측 하단, 우측 하단 그리고 좌측 상단에 각각 배치해 보세요.
     

    Stack class - widgets library - Dart API

    A widget that positions its children relative to the edges of its box. This class is useful if you want to overlap several children in a simple way, for example having some text and an image, overlaid with a gradient and a button attached to the bottom. Ea

    api.flutter.dev

     

    [실습] 코드 팩토리의 플러터 프로그래밍 - 7장 4절 연습용 앱 만들기 : 스플래시 스크린 앱 (175페이지)

    • 프로젝트 이름 : splash_screen
    • 설명 : 아래와 같은 화면을 만듭니다.

     

     

    GitHub - codefactory-co/flutter-golden-rabbit-novice-v2: Must Have 코드팩토리의 플러터 프로그래밍 3쇄 개정판

    Must Have 코드팩토리의 플러터 프로그래밍 3쇄 개정판. Contribute to codefactory-co/flutter-golden-rabbit-novice-v2 development by creating an account on GitHub.

    github.com

     

     

     

    [제스처(Gesture) 관련 Widget]

    • 제스처(Gesture) 관련 Widget을 배우기 위해서는 먼저 제스처에 대한 이해가 선행되어야 합니다. 제스처에 대한 이해를 하기 위해서는 이벤트(Event)를 이해하여야 합니다. 그런데 왠지 그게 그것 같아서 ChatGpt에게 제스처와 이벤트의 차이를 물어 보았습니다.

     

     

    ChatGPT가 열심히 설명을 했는데 프로그래머인 제가 보기에는 “컴퓨터에 어떤 사건(Event)이 발생하는 것” 정도로 이해가 됩니다. 그리고 어떤 이벤트나 제스처가 발생하면 그 이벤트에 맞는 처리를 해 주어야 하는데 그것을 이벤트 처리기(이벤트 핸들러,Event Handler)라고 부르는데 이벤트 처리기는 이벤트를 처리할 목적으로 만들어진 함수입니다.

    말로는 이해가 잘 안되지요? 그럴 때는 코드와 함께 실행화면을 확인해 보면 됩니다.

    ElevatedButton Widget

    Flutter에서 자주 사용하는 ElevatedButton Widget을 하나 만들어 보겠습니다. 어느덧 first_flutter 앱의 화면이 꽉차고 이벤트 처리(Event Handling)에 부담이 생기니 gesture_widgets 앱을 위한 프로젝트를 새로 만들겠습니다. Flutter App을 개발 중에 새로운 프로젝트를 만들 때에는 File > New > New Flutter Projects 메뉴를 선택해서 만들 수 있습니다.

     

    저는 아래 화면과 같이 프로젝트 정보를 입력해 보았습니다.

     

    이전에 사용하던 코드를 빈번하게 다시 보게 될테니 New Window로 프로젝트를 엽시다.

     

    프로젝트를 하나 새로 만들었으니 main.dart의 _MyHomePageState 클래스(Class)를 찾아가 Flutter가 기본적으로 제공하는 코드들을 지웁시다. 아래에 지우고 코드를 추가해야 할 곳을 주석으로 표시해 놓았습니다. 주석도 코드의 가독성을 떨어지게 하니 모두 지웁시다.

    // 삭제할 곳을 삭제 시작과 삭제 종료로 표기한 코드

    class _MyHomePageState extends State<MyHomePage> {

      /* 삭제 시작

      int _counter = 0;

     

      void _incrementCounter() {

        setState(() {

          // This call to setState tells the Flutter framework that something has

          // changed in this State, which causes it to rerun the build method below

          // so that the display can reflect the updated values. If we changed

          // _counter without calling setState(), then the build method would not be

          // called again, and so nothing would appear to happen.

          _counter++;

        });

      }

      삭제 종료*/

      @override

      Widget build(BuildContext context) {

        // This method is rerun every time setState is called, for instance as done

        // by the _incrementCounter method above.

        //

        // The Flutter framework has been optimized to make rerunning build methods

        // fast, so that you can just rebuild anything that needs updating rather

        // than having to individually change instances of widgets.

        return Scaffold(

          appBar: AppBar(

            // TRY THIS: Try changing the color here to a specific color (to

            // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar

            // change color while the other colors stay the same.

            backgroundColor: Theme.of(context).colorScheme.inversePrimary,

            // Here we take the value from the MyHomePage object that was created by

            // the App.build method, and use it to set our appbar title.

            title: Text(widget.title),

          ),

          body: Center(

            // Center is a layout widget. It takes a single child and positions it

            // in the middle of the parent.

            child: Column(

              // Column is also a layout widget. It takes a list of children and

              // arranges them vertically. By default, it sizes itself to fit its

              // children horizontally, and tries to be as tall as its parent.

              //

              // Column has various properties to control how it sizes itself and

              // how it positions its children. Here we use mainAxisAlignment to

              // center the children vertically; the main axis here is the vertical

              // axis because Columns are vertical (the cross axis would be

              // horizontal).

              //

              // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"

              // action in the IDE, or press "p" in the console), to see the

              // wireframe for each widget.

              mainAxisAlignment: MainAxisAlignment.center,

              children: <Widget>[

                /* 삭제 시작

                const Text(

                  'You have pushed the button this many times:',

                ),

                Text(

                  '$_counter',

                  style: Theme.of(context).textTheme.headlineMedium,

                ),

                삭제 종료 */

              ],

            ),

          ),

          /* 삭제 시작

          floatingActionButton: FloatingActionButton(

            onPressed: _incrementCounter,

            tooltip: 'Increment',

            child: const Icon(Icons.add),

          ), // This trailing comma makes auto-formatting nicer for build methods.

          삭제 종료 */

        );

      }

    }

    잘 지우고 문제없이 동작하는지 실행하여 확인해 봅시다. 이뮬레이터의 안정적인 동작을 위하여 기존에 실행되던 first_flutter 프로젝트의 실행을 중단시킨 후 실행시킵시다.

     

    이런!!! 타이틀 수정을 잊었네요. Flutter 프레임워크에 따르면 타이틀은 아래 코드에서 수정하여야 합니다. 프레임워크는 정확한 장소를 찾아 수정하는 것이 중요합니다.

     

    Flutter 프레임워크가 제공하는 타이틀까지 정상화시켰으니 이제 우리 프로젝트의 코딩을 추가해 갑시다.

     

    지울 코드를 다 지우고 나니 아래와 같이 코드가 간결해졌습니다. 주석으로 “// 여기가 코드를 추가할 곳”이라고 표현해 놓은 곳에 프로젝트의 코드를 추가하면 됩니다.

    // Flutter 프레임워크에서 타이틀을 수정할 곳

    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: Center(

            child: Column(

              mainAxisAlignment: MainAxisAlignment.center,

              children: <Widget>[

                // 여기가 코드를 추가할 곳

              ],

            ),

          ),

        );

      }

    }

    코드를 추가할 곳에 Ele까지만 쳐도 안드로이드 스튜디오가 ElevatedButton을 첫번째로 추천해 줍니다. 마우스로 클릭하거나 탭을 누릅시다. 그러면 ElevatedButton(onPressed: onPressed, child: child)까지 코드를 완성해 주는데 필요한 것만 수정합시다.

     

    onPressed: () { print("ElevatedButton 버튼이 클릭되었습니다."); } 에서 onPressed는 탭 제스처와 클릭 이벤트에 해당합니다. () { print("ElevatedButton 버튼이 클릭되었습니다."); }는 함수를 단순화한 익명 함수입니다. print("ElevatedButton 버튼이 클릭되었습니다." 문장을 빼면 () {}만 남는데 이것은 빈 함수를 의미합니다. child: Text("Elevated Button"), 문장은 Text 객체 혹은 Widget으로 ElevatedButton에 나타날 글자를 추가해 주고 있습니다.

    // ElevatedButton을 추가한 코드

    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: Center(

            child: Column(

              mainAxisAlignment: MainAxisAlignment.center,

              children: <Widget>[

                // 여기가 코드를 추가할 곳

                ElevatedButton(

                    onPressed: () { print("ElevatedButton이 클릭되었습니다."); },

                    child: Text("Elevated Button"),

                ),

                // 여기까지 추가됨

              ],

            ),

          ),

        );

      }

    }

    실행화면의 중앙에 버튼이 나타났습니다. 클릭해 봅시다.

    그러면 안드로이드 스튜디오의 하단에 이벤트 처리기(Event Handler)인 () { print("ElevatedButton이 클릭되었습니다."); } 함수가 실행된 결과가 나타납니다.

     

     

    Functional Program의 관점에서 () {}와 같은 함수는 함수가 한문장이거나 비어있을때는 가독성이 매우 높지만 함수가 여러 문장으로 구성되면 코드가 길어지고 가독성도 무척 떨어지게 됩니다. 그래서 이벤트 처리기가 길어지면 별도의 함수로 분리하여야 합니다. 그러면 분리해 봅시다.

    아래 코드를 보고 느끼겠지만 Dart는 함수의 정의형식이 매우 간소화되었습니다. 아무튼 아래와 같이 코드를 함수로 분리하면 되는데 형식은 한줄 함수와 거의 똑같고 함수의 이름을 만드는 수고만 더 하면 됩니다. 그리고 onPressed: _buttonClicked,와 같이 onPressed 이벤트에 _buttonClicked 이벤트 처리기의 이름을 연결해 주기만 하면 됩니다.

    // 이벤트 처리기를 함수로 분리한 코드

    class _MyHomePageState extends State<MyHomePage> {

            // 함수의 추가 시작

      _buttonClicked() {

        print("ElevatedButton이 클릭되었습니다.");

        print("한줄일때는 함수로 분리하지 않고, 여러줄일때는 함수로 분리하는 것이 좋습니다.");

      }

            // 함수 추가의 끝

      @override

      Widget build(BuildContext context) {

        return Scaffold(

          appBar: AppBar(

            backgroundColor: Theme.of(context).colorScheme.inversePrimary,

            title: Text(widget.title),

          ),

          body: Center(

            child: Column(

              mainAxisAlignment: MainAxisAlignment.center,

              children: <Widget>[

                ElevatedButton(

                  //onPressed: () { print("ElevatedButton이 클릭되었습니다."); },

                  onPressed: _buttonClicked,      // 수정된 코드

                  child: Text("Elevated Button"),

                ),

              ],

            ),

          ),

        );

      }

    }

    실행해 보면 변경된 이벤트 처리가 동작하는 것을 확인할 수 있습니다.

     

    ElevatedButton 외에도 음영을 처리하지 않는 단순한 버튼인 TextButton을 포함하여 경계선만 있는 OutlinedButton 등이 있습니다.

    ElevatedButton은 TextButton과 달리 **음영(Shadow)**과 배경색을 포함하여 스타일을 쉽게 커스터마이징할 수 있는 버튼입니다. TextButton은 기본적으로 투명한 배경을 가지며, 강조되지 않은 액션을 위해 사용되지만, 여전히 style 속성을 통해 색상이나 다른 스타일을 지정할 수 있습니다. 여기서는 간단하게 색상 변경을 해 보겠습니다.

    // 변경전 코드

                ElevatedButton(

                    onPressed: _buttonClicked,

                    child: Text("Elevated Button"),

                ),

    style 속성은 TextStyle 객체를 지정할 때 사용해 보았습니다. ButtonStyle 객체로 바뀌었을 뿐 코딩하는 형태는 동일합니다. 다만 ElevatedButton.styleFrom은 ButtonStyle 객체를 반환하는 팩토리 메서드 혹은 생성자 메소드(생성자처럼 객체를 쉽게 생성하는 메서드)로, 버튼의 스타일을 지정할 때 사용됩니다.

    // 변경후 코드

                ElevatedButton(

                    style: ElevatedButton.styleFrom(  // ElevatedButton style로

                      backgroundColor: Colors.blue,   // 버튼 배경색 지정

                    ),

                    onPressed: _buttonClicked,

                    child: Text("Elevated Button"),

                ),

    실행해 보면 버튼의 색이 변경되는 것을 확인할 수 있습니다.

     

    ElevatedButton class - material library - Dart API (flutter.dev)

    OutlinedButton class - material library - Dart API (flutter.dev)

    TextButton class - material library - Dart API (flutter.dev)

    IconButton Widget

    Button 종류의 Widget은 앞에서 설명한 ElevatedButton Widget과 크게 다르지 않습니다. IconButton Widget은 간단한 코드를 보여 주는 것으로 이해가 될 것입니다. Icon Widget과 IconButton Widget의 차이는 버튼 이벤트를 처리할 능력이 있느냐 없느냐 입니다.

    ElevatedButton Widget은 child라는 속성의 이름을 사용하는데 IconButton Widget에서는 icon으로 변경되었습니다. 일관성이 없는 것으로 이해될 수도 있는데 속성의 이름을 구체화할 수 있어서 구체화했다고 받아 들이면 됩니다.

    // IconButton Widget 코드

                IconButton(

                    onPressed: () { print("IconButton이 클릭되었습니다."); },

                    icon: Icon(Icons.add_alert)

                ),

    클릭해 보면 마찬가지로 이벤트 처리기 함수가 실행됩니다.

     

     

    IconButton class - material library - Dart API (flutter.dev)

    FloatingActionButton Widget

    FloatingActionButton Widget은 Scaffold Widget의 floatingActionButton 속성의 지정으로 주로 사용되는데 “객체 지향 Flutter 프레임워크”를 설명할 때 그냥 넘어 갔었습니다만 이제는 이해할 수 있습니다. 앞에서 이벤트와 이벤트 처리기를 배웠으니 코드를 다시 소환하여 보는 것으로 설명을 대신합니다. setState() 함수의 사용은 또 다시 설명하지 않고 넘어가고 설명하기 적절한 시점이 올 때 설명하겠습니다.

    // 변수

      int _counter = 0;

     

    // 이벤트 처리기 코드

      void _incrementCounter() {

        setState(() {

          _counter++;

        });

      }

    FloatingActionButton Widget은 tooltip 기능을 가지고 있어서 기술되어 있습니다. tooltip은 FloatingActionButton을 지그시 누르고 있으면 나타납니다.

     

    // Scaffold Widget의 floatingActionButton 속성의 지정 코드

          floatingActionButton: FloatingActionButton(

            onPressed: _incrementCounter,

            tooltip: 'Increment',

            child: const Icon(Icons.add),

          ),

    FloatingActionButton class - material library - Dart API (flutter.dev)

    GestureDetector Widget

    GestureDetector Widget은 Gesture 관련 Widget이 아닌 Widget들에게 이벤트를 처리할 능력을 부여합니다. 아래 코드의 경우와 같이 이벤트를 처리할 Widget을 GestureDetector Widget의 child로 지정하면 됩니다.

                GestureDetector(

                  onTap: () {print("Image가 클릭되었습니다.");},

                  child: Image.network("<https://image.aladin.co.kr/product/34207/82/cover200/896088457x_1.jpg>"),

                ),

    이제 이벤트 처리 기능이 없는 이미지를 클릭하면 GestureDetector Widget을 통하여 이벤트 처리기가 동작하는 것을 확인할 수 있습니다.

     

     

    GestureDetector class - widgets library - Dart API (flutter.dev)

    Instructor Note : 노션의 “Flutter FAQ와 Tips”에서 “Fonts를 지정하여 사용하는 방법”을 설명할 것

    실습

    자신의 가족을 소개하는 family_intro 앱을 개발하세요.

    • family_intro 프로젝트를 만들어 작업하시기 바랍니다.
    • 지금까지 배운 것들을 기반으로 자신의 가족을 소개하는 하나의 화면으로 구성된 앱을 개발하기 바랍니다.
    • 필수적으로 사용해야 할 Widget : Scaffold, Text, Image, Container, Column, Row, Stack, Positioned
    • 디자인 기준 : 팀 구성원들의 이미지를 모두 추가, Children 3개인 Row와 Children 2개를 포함하는 Row를 모두 사용, 팀 고유의 배경 색상, 한글 Font 사용

    Instructor Note : family_intro 앱의 실행 화면을 스마트폰에서 팀즈로 화면을 공유하여 수강생들에게 보여 줄 것

     

     

     참고 문헌

    [논문]

    • 없음

    [보고서]

    • 없음

    [URL]

    • 없음

     

     문의사항

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

    • sangho.lee.1990@gmail.com

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

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