본문 바로가기
Frontend, Client/Flutter

[Flutter] 플러터 - 위젯(Widget) 알아보기

by ggyongi 2022. 11. 2.
반응형

Flutter UI Docs : https://flutter-ko.dev/docs/development/ui/widgets-intro#basic-widgets
위젯 카탈로그 : https://flutter-ko.dev/docs/development/ui/widgets


목차
1. 위젯이란?
2. stateful widget vs stateless widget
3. 기본 widget
4. Material Components
5.입력에 반응하여 stateful 위젯 변경하기
6. layout 만들기
7. Lifecycle
8. examples


1. 위젯이란?

위젯은 현재 위젯의 상태(State)를 유지하며, 어떻게 화면에 보여지는 지에 대한 정보를 가지고 있다. 쉽게 위젯으로부터 UI를 구성한다고 생각하면 된다. 위젯의 상태(State)가 변하면, 이전 상태와의 차이점을 비교하여 변화를 최소화하면서 상태를 변경한다.

import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

플러터는 main() 함수에서 runApp()을 기본적으로 호출하게 된다.
runApp()에서 작성된 위젯을 위젯트리의 루트 위젯으로 놓고, 그 하위에 자식 위젯들을 정의해 나간다.

2. stateful widget vs stateless widget

  • stateful widget
의미 : 위젯의 라이프사이클동안 특정 state를 유지. 이 state는 변경될 수 있다.
생성 방법 : **State 클래스 인스턴스를 만드는 StatefulWidget 클래스를 만들면 된다.**
StatefulWidget은 그 자체로는 immutable이지만 State가 상태(변경될 수 있는)를 유지한다.

사용자와 계속 상호작용해야하고, 그에 따라 화면 변경이 필요하면 statful widget을 사용하면 된다. 
class RandomWords extends StatefulWidget {
    const RandomWords({super.key});

    @override
    State<RandomWords> createState() => _RandomWordsState();
}

class _RandomWordsState extends State<RandomWords> {
    @override
    Widget build(BuildContext context) {
      return Container();
    }
}

위의 코드를 보고 뭐가 이렇게 많아라고 할 수 있지만 사실 IDE가 모든 것을 작성해준다. stful을 적고 엔터를 누르면 된다.

  • stateless widget
의미 : Immutable ⇒ 모든 프로퍼티는 final
부모 위젯으로부터 인자를 입력받아 final 변수에 이를 저장하고, build() 시에 이를 사용한다.
화면이 로드될 때 한번만 그려진다. state가 없다.

사용자와 상호작용하지 않고 변하지 않으면 stateless widget을 사용하면 된다.
class RandomWords extends StatelessWidget {
  const RandomWords({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

 

3. 기본 widget

Text : 텍스트를 앱에 보여주는 위젯
Row, Column : 유연한 레이아웃을 작성하는 기초가 되는 위젯
Stack : 말그대로 다른 위젯에 쌓을 수 있는 위젯
Container : 직사각형 요소를 만들어냄. margins, padding, and constraints과 같은 성분을 가짐.

위젯 사용 예시

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  MyAppBar({this.title});

  // Fields in a Widget subclass are always marked "final".

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 56.0, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: BoxDecoration(color: Colors.blue[500]),
      // Row is a horizontal, linear layout.
      child: Row(
        // <Widget> is the type of items in the list.
        children: <Widget>[
          IconButton(
            icon: Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null disables the button
          ),
          // Expanded expands its child to fill the available space.
          Expanded(
            child: title,
          ),
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Material is a conceptual piece of paper on which the UI appears.
    return Material(
      // Column is a vertical, linear layout.
      child: Column(
        children: <Widget>[
          MyAppBar(
            title: Text(
              'Example title',
              style: Theme.of(context).primaryTextTheme.title,
            ),
          ),
          Expanded(
            child: Center(
              child: Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    title: 'My app', // used by the OS task switcher
    home: MyScaffold(),
  ));
}


Material Icons 사용하기
pubspec.yaml파일에 다음을 추가

name: my_app
flutter:
  uses-material-design: true

*위의 코드와 같이 Material Design Widget을 사용하기 위해서는 *runApp()*에서 *MaterialApp()*을 만들어줘야 한다(MaterialApp 안에서 작동해야 Theme data를 적절히 상속받을 수 있다고 함).
더 많은 위젯 보기 : https://flutter-ko.dev/docs/development/ui/widgets

 

4. Material Components

플러터는 수많은 위젯을 제공해준다. 특히 MaterialApp()을 사용하는 앱은 유용한 위젯들을 많이 사용할 수 있다.
위의 코드를 MeterialApp Widget을 사용하여 아래 코드처럼 바꿀 수 있다.
MyScaffold, MyAppBar와 같이 직접 만들어주었던 것을 플러터에서 제공하는 Scaffold, AppBar로 바꾸었다.

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Flutter Tutorial',
    home: TutorialHome(),
  ));
}

class TutorialHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Scaffold is a layout for the major Material Components.
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),
        title: Text('Example title'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
      // body is the majority of the screen.
      body: Center(
        child: Text('Hello, world!'),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Add', // used by assistive technologies
        child: Icon(Icons.add),
        onPressed: null,
      ),
    );
  }
}

 

5. 입력에 반응하여 stateful 위젯 변경하기

setState() 메서드를 사용하여 State를 변경하면 된다.
그러면 build()가 다시 실행되며 화면에는 변경된 결과를 반영한다.

class Counter extends StatefulWidget {
  // This class is the configuration for the state. It holds the
  // values (in this case nothing) provided by the parent and used by the build
  // method of the State. Fields in a Widget subclass are always marked "final".

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    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 you change _counter without calling
      // setState(), then the build method won't 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 _increment 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 Row(
      children: <Widget>[
        RaisedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
        Text('Count: $_counter'),
      ],
    );
  }
}

StatefulWidget과 State가 따로 존재하게 되는데 이 이유가 뭘까?
이유는 두 오브젝트의 생명주기가 다르기 때문이다.

Widget : **일시적**인 객체로, 현재 상태 안에서 앱의 프레젠테이션 영역을 구성하는 데에 사용됨.
State : build() 호출 간에 **지속적**이다. 그들에게 정보를 기억하도록 한다.

플러터에서,
변경이 발생하면 콜백 방식으로 부모 위젯 방향에게 전달(flow up)된다. 현재 상태는 화면을 나타내는 자식 위젯 방향(stateless widget를 향해)으로 전달(flow down)된다.
아래 코드는 그 내용을 좀 더 잘 보여주고 있다.

class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: onPressed,
      child: Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(children: <Widget>[
      CounterIncrementor(onPressed: _increment),
      CounterDisplay(count: _counter),
    ]);
  }
}

또한 위의 stateless widget인 CounterDisplay, CounterIncrementor를 보면, 관심사를 서로 분리하고 있음을 알 수 있다.

  • displaying의 역할을 담당하는 CounterDisplay
  • changing의 역할을 담당하는 CounterIncrementor

이러한 책임의 분리는 부모에게는 심플함을 유지시켜주는 동시에 복잡성을 캡슐화해준다.

 

6. layout 만들기

아래 링크로 가서 레이아웃 튜토리얼을 따라가면 레이아웃에 대한 감을 잡을 수 있다.
레이아웃 만들기

 

레이아웃 만들기

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

flutter-ko.dev

 

7. Lifecycle

stateless widget은 생명주기가 없지만, stateful widget은 생명주기를 가지고 있다.

initState(): State가 만들어질 때 한 번만 호출됨. Context, Widget에 대한 의존관계를 초기화할 때 호출.
ex) BuildContext에 의존하는 데이터 초기화, 부모 위젯과 같이 연관된 위젯에 의존하는 데이터 초기화

build(): 수시로 호출되며, 필수로 재정의(override)해야하는 메서드. 해당 위젯이 표현되는 UI를 묘사함.
공식 문서에 나와있는 build() 호출의 다양한 케이스

setState() : 오브젝트의 상태가 변경될 시에 호출. 이 곳에서 비동기함수 사용은 불가능.

didUpdateWidget(): 부모 위젯이 변경되어 이 위젯 속성을 변경해야 하는 경우, 해당 메서드가 호출되고 이전 위젯을 인자로 사용함.

dispose() : 이 메서드가 호출되면 State가 제거됨.

라이프사이클 참고:
https://api.flutter.dev/flutter/widgets/State-class.html
https://devmg.tistory.com/186

8. examples

위젯의 사용 예시를 보여준다.

  1. 버튼을 누르면 숫자가 증가하는 앱
import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
    title: 'Tap Example',
    home: TapExampleHome(),
  ));
}

class TapExampleHome extends StatefulWidget {
  const TapExampleHome({Key? key}) : super(key: key);

  @override
  State<TapExampleHome> createState() => _TapExampleHomeState();
}

class _TapExampleHomeState extends State<TapExampleHome> {
  int _count = 0;

  void _increment(){
    _count ++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body : Center(
          child : Text(
              _count.toString(),
              style: const TextStyle(fontSize: 50),
          )
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() {
          _increment();
        }),
        child: const Icon(Icons.add),
      ),
    );
  }
}


2. 좋아요 버튼을 클릭하면 좋아요 아이콘의 색깔이 바뀌는 앱

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
    title: 'Like Button Example',
    home: LikeBtnHome(),
  ));
}

class LikeBtnHome extends StatefulWidget {
  const LikeBtnHome({Key? key}) : super(key: key);

  @override
  State<LikeBtnHome> createState() => _LikeBtnHomeState();
}

class _LikeBtnHomeState extends State<LikeBtnHome> {

  static const Color _normalIconColor = Colors.grey;
  static const Color _activeIconColor = Colors.blue;
  bool _isActive = false;

  void _switchActiveState(){
    setState((){
      _isActive = !_isActive;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body : Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
           Row(
             mainAxisAlignment: MainAxisAlignment.center,
             children : [
               Column(
                 children: [
                   Icon(Icons.thumb_up, color: _isActive ? _activeIconColor : _normalIconColor),
                    ElevatedButton(
                        onPressed: _switchActiveState,
                        child: const Text( "좋아요",
                          style: TextStyle(
                            color : Colors.white,
                            fontSize: 12,
                          ),
                        )
                    )
                 ],
               ),
             ]
           )
         ],
    )
    );
  }
}

 

 

비전공자 네카라 신입 취업 노하우

시행착오 끝에 얻어낸 취업 노하우가 모두 담긴 전자책!

kmong.com

댓글