소소한 개발 공부

[Flutter] Widget 과 Layout 본문

개발/앱 개발

[Flutter] Widget 과 Layout

이내내 2022. 2. 12. 03:50

위젯 Widget

사전 상의 의미는 작은 장치이며 

실제로 사용되는 의미는 아래와 같다.

1. 독립적으로 실행되는 작은 프로그램

2. 주로 바탕화면 등에서 날씨나 뉴스, 생활 정보 등을 보여줌

3. 그래픽이나 데이터 요소를 처리하는 함수를 가지고 있음

 

flutter에서 위젯이란 무엇일까?

flutter의 위젯은 React 에 영감을 받은 최신 프레임 워크를 사용해 빌드되는 UI이다.

1. UI를 만들고 구성하는 모든 기본 단위 요소이다.

2. 눈에 보이지 않는 요소들까지 모두 위젯이다. = UI 디자인과 관련해서 레이아웃을 돕는 요소까지 모두 위젯이다.

 

flutter의 모든 것은 위젯으로 텍스트부터 버튼, 스크린 레이아웃 등이 해당된다.

flutter에 어떤 위젯이 있는 지는 아래의 사이트에서 확인할 수 있다.

flutter widget cheat sheet 

 

Flutter 위젯 목록

 

flutter-ko.dev

 

기본적인 위젯은 다음과 같다.

1. Text

Text Widget은 어플리케이션 내에 텍스트 양식이 적용될 수 있게 만든다.

 

2. Row, Column

이 위젯은 수평 방향(Row)과 수직 방향(Column)에서 유연한 레이아웃을 만들 수 있게 해준다. 이 객체의 디자인은 웹의 flexbox layout 모델에 기반한다.

 

3. Stack

Stack Widget은 선형방향(수평 혹은 수직)으로 위젯을 배치하는 대신 그려지는 순으로 배치할 수 있다. 하위 항목에 Positioned Widget(Positioned class)을 사용해 스택의 상하좌우 모서리를 기준으로 배치할 수 있다. 스택은 웹의 absolute positioning layout 모델에 기반한다. 

 

4. Container

Container Widget은 직사각형 요소를 만들 수 있다. 컨테이너는 배경, 테두리, 그림자와 같은 BoxDecoration으로 표현할 수 있다. 컨테이너의 크기에 margin, padding, 사이즈에 대한 제약을 넣을 수 있으며 또한 컨테이너는 행렬을 사용해 3차원공간에 transform 될 수 있다.


위젯의 종류

위젯은 크게 3가지 종류로 나눌 수 있다.

1. Stateless Widget

2. Stateful Widget

3. Inherited Widget

 

state 는 상태라는 의미를 가지고 있는데 stateless 는 이전 상호작용의 어떠한 값도 저장하지 않음을 의미를 가지고있으며, stateful 은 어떤 value 값을 지속적으로 추적 및 보존한다는 의미를 가지고 있다. 

이를 쉽게 말하면,

Stateless Widget은 상태가 없는(어떤 움직임이나 변화가 없는) 정적인 위젯이고,

Stateful Widget은 계속 움직이거나 변화가 있는 동적인 위젯이라고 할 수 있다.

 

Stateless Widget

1. 스크린상에 존재만 할 뿐 아무것도 하지 않음.

2. 어떠한 실시간 데이터도 저장하지 않음.

3. 어떤 변화(모양, 상태)를 유발하는 value 값을 가지지 않음.

 

Stateful Widget

1. 사용자의 interaction(상호작용)에 따라 모양이 바뀜. ex) 체크박스, 텍스트 필드 등

 

위젯은 계층 구조를 가지며 tree 형태로 정리할 수 있다.

그렇기 때문에 한 widget 내에 얼마든지 다른 widget들이 child로 포함될 수 있다.

다시 말해 widget은 부모 위젯과 자식 위젯으로 구성된다.

이 부모 위젯(Parent Widget)을 Widget Container라고 부르기도 한다.

https://dev.to/topeomot/flutter-everything-is-a-widget-series-part-2-composition-is-key-3b6h

 

 


이제 이 위젯을 사용해 화면 레이아웃을 나눠 앱을 디자인해보자.

https://flutter-ko.dev/docs/development/ui/layout/tutorial

 

레이아웃 만들기

레이아웃을 만드는 방법을 배웁니다.

flutter-ko.dev

우선 새로운 flutter 프로젝트를 만들고 만들고자 하는 타겟을 요소별로 보기로 한다.

4개의 요소가 있으며 이는 Column안에 4개의 children으로 넣는다. (main과 stateless 위젯은 생략)

Scaffold 위젯 안에 Column의 children으로 첫번째는 Image 그 다음은 titleSection, buttonSection, textSection으로 나눴다. 각 요소 사이는 Padding으로 간격을 줬다.

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    var titleSection;
    var buttonSection;
    var textSection;
  
    return Scaffold(
      body: Column(
        children: <Widget>[
          Image.network(
              "https://images.unsplash.com/photo-1537225228614-56cc3556d7ed?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTJ8fGNhbXBpbmd8ZW58MHx8MHx8&w=1000&q=80",
              height: 240,
              width: 600,
              fit: BoxFit.cover),
          Padding(padding: EdgeInsets.all(15.0)),
          titleSection,
          Padding(padding: EdgeInsets.all(15.0)),
          buttonoSection,
          Padding(padding: EdgeInsets.all(15.0)),
          textSection
        ],
      ),
    );
  }
}

 

titleSection

titleSection에 들어갈 내용은 한 Row 안에 한 개의 Column 요소(그 안에 두개의 Children), Icon, Text가 들어있다.

var titleSection 부분을 정의한다.

var titleSection = Row(
      mainAxisAlignment: MainAxisAlignment.center,	// 요소 전체는 가운데 정렬
      children: <Widget>[
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,	// text는 좌측 정렬
          children: <Widget>[
            Text('Oeschinen Lake Campground',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 26)),
            Text('Kadnresteg, Switzerland',
                style: TextStyle(color: Colors.grey, fontSize: 26))
          ],
        ),
        Padding(padding: EdgeInsets.all(20.0)),	// 간격주기
        Icon(
          Icons.star,
          size: 35,
          color: Colors.deepOrange,
        ),
        Text(
          '41',
          style: TextStyle(fontSize: 30),
        )
      ],
    );

buttonSection

buttonSection은 한 Row 안에 3개의 버튼 children이 있고 이 children은 각각 Column(children을 두개 가짐, 위는 아이콘, 아래는 텍스트)으로 이뤄져있다.

var buttonSection = Row(
      mainAxisAlignment: MainAxisAlignment.center,	// 전체는 가운데 정렬
      children: <Widget>[
        Column(
          children: <Widget>[
            Icon(Icons.call, size: 45, color: Colors.lightBlue),
            Text('CALL', style: TextStyle(color: Colors.lightBlue))
          ],),
        Padding(padding: EdgeInsets.all(30.0)),
        Column(
          children: <Widget>[
            Icon(Icons.near_me, size: 45, color: Colors.lightBlue),
            Text('ROUTE', style: TextStyle(color: Colors.lightBlue))
          ],),
        Padding(padding: EdgeInsets.all(30.0)),
        Column(
          children: <Widget>[
            Icon(Icons.share, size: 45, color: Colors.lightBlue),
            Text('SHARE', style: TextStyle(color: Colors.lightBlue))
          ],),
      ],	// children
    );	// Row

 괄호가 많아서 길어보이지만 하나하나 보면 Column 안에 Icon과 Text가 children으로 들어간 요소가 3개 있는 아주 간단한 형태이다.

 

textSection

textSection은 안에 들어가는 텍스트를 넣었다. 전체는 Container 위젯으로 감싸고 그 안에 단일 자식으로 Text를 넣고 상하좌우 padding을 씩 줬다.

var textSection = Container(
      child: Text(
          'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
          'Alps. Situated 1,578 meters above sea level, it is one of the '
          'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
          'half-hour walk through pastures and pine forest, leads you to the '
          'lake, which warms to 20 degrees Celsius in the summer. Activities '
          'enjoyed here include rowing, and riding the summer toboggan run.',
          style: TextStyle(fontSize: 20)),
      padding: EdgeInsets.all(15.0),
    );

 

이에 대한 결과물은 아래이다. (사진은 구글 free 이미지를 썼다.)

전체 코드

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

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    var titleSection = Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Oeschinen Lake Campground',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 26)),
            Text('Kadnresteg, Switzerland',
                style: TextStyle(color: Colors.grey, fontSize: 26))
          ],
        ),
        Padding(padding: EdgeInsets.all(20.0)),
        Icon(
          Icons.star,
          size: 35,
          color: Colors.deepOrange,
        ),
        Text(
          '41',
          style: TextStyle(fontSize: 30),
        )
      ],
    );
    var buttonSection = Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Column(
          children: <Widget>[
            Icon(
              Icons.call,
              size: 45,
              color: Colors.lightBlue,
            ),
            Text(
              'CALL',
              style: TextStyle(color: Colors.lightBlue),
            )
          ],
        ),
        Padding(padding: EdgeInsets.all(30.0)),
        Column(
          children: <Widget>[
            Icon(
              Icons.near_me,
              size: 45,
              color: Colors.lightBlue,
            ),
            Text('ROUTE', style: TextStyle(color: Colors.lightBlue))
          ],
        ),
        Padding(padding: EdgeInsets.all(30.0)),
        Column(
          children: <Widget>[
            Icon(
              Icons.share,
              size: 45,
              color: Colors.lightBlue,
            ),
            Text('SHARE', style: TextStyle(color: Colors.lightBlue))
          ],
        ),
      ],
    );
    var textSection = Container(
      child: Text(
          'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
          'Alps. Situated 1,578 meters above sea level, it is one of the '
          'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
          'half-hour walk through pastures and pine forest, leads you to the '
          'lake, which warms to 20 degrees Celsius in the summer. Activities '
          'enjoyed here include rowing, and riding the summer toboggan run.',
          style: TextStyle(fontSize: 20)),
      padding: EdgeInsets.all(15.0),
    );

    return Scaffold(
      body: Column(
        children: <Widget>[
          Image.network(
              "https://images.unsplash.com/photo-1537225228614-56cc3556d7ed?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTJ8fGNhbXBpbmd8ZW58MHx8MHx8&w=1000&q=80",
              height: 240,
              width: 600,
              fit: BoxFit.cover),
          Padding(padding: EdgeInsets.all(15.0)),
          titleSection,
          Padding(padding: EdgeInsets.all(15.0)),
          buttonSection,
          Padding(padding: EdgeInsets.all(15.0)),
          textSection
        ],
      ),
    );
  }
}

flutter docs 코드

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

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

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

  @override
  Widget build(BuildContext context) {
    Widget titleSection = Container(
      padding: const EdgeInsets.all(32),
      child: Row(
        children: [
          Expanded(
            /*1*/
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                /*2*/
                Container(
                  padding: const EdgeInsets.only(bottom: 8),
                  child: const Text(
                    'Oeschinen Lake Campground',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                Text(
                  'Kandersteg, Switzerland',
                  style: TextStyle(
                    color: Colors.grey[500],
                  ),
                ),
              ],
            ),
          ),
          /*3*/
          Icon(
            Icons.star,
            color: Colors.red[500],
          ),
          const Text('41'),
        ],
      ),
    );

    Color color = Theme.of(context).primaryColor;

    Widget buttonSection = Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        _buildButtonColumn(color, Icons.call, 'CALL'),
        _buildButtonColumn(color, Icons.near_me, 'ROUTE'),
        _buildButtonColumn(color, Icons.share, 'SHARE'),
      ],
    );

    Widget textSection = const Padding(
      padding: EdgeInsets.all(32),
      child: Text(
        'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
        'Alps. Situated 1,578 meters above sea level, it is one of the '
        'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
        'half-hour walk through pastures and pine forest, leads you to the '
        'lake, which warms to 20 degrees Celsius in the summer. Activities '
        'enjoyed here include rowing, and riding the summer toboggan run.',
        softWrap: true,
      ),
    );

    return MaterialApp(
        title: 'Flutter layout demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Scaffold(
          appBar: AppBar(
            title: const Text('Flutter layout demo'),
          ),
          body: ListView(
            children: [
              Image.network(
                  "https://images.unsplash.com/photo-1537225228614-56cc3556d7ed?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTJ8fGNhbXBpbmd8ZW58MHx8MHx8&w=1000&q=80",
                  height: 240,
                  width: 600,
                  fit: BoxFit.cover),
              titleSection,
              buttonSection,
              textSection,
            ],
          ),
        )
    );
  }

  Column _buildButtonColumn(Color color, IconData icon, String label) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(icon, color: color),
        Container(
          margin: const EdgeInsets.only(top: 8),
          child: Text(
            label,
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w400,
              color: color,
            ),
          ),
        ),
      ],
    );
  }
}

 

참고:

[입문자를 위한 플러터(flutter) 튜토리얼]#7 위젯과 레이아웃-플러터스튜디오-재즐보프 ~

[입문자를 위한 플러터(flutter) 튜토리얼]#10 레이아웃 튜토리얼 - 마무리-재즐보프 

플러터(flutter) 순한 맛 강좌 5 | 플러터에서 제일 중요하다는 위젯이란 무엇일까요? - 코딩 셰프

flutter docs

[번역] Flutter 위젯 사용해보기-Dan Kim