guricode

Stateless,Stateful/ view 위젯/레이아웃 본문

앱/Flutter&Dart

Stateless,Stateful/ view 위젯/레이아웃

agentrakugaki 2025. 6. 17. 20:43

1. StatelessWidget vs StatefulWidget

단계 StatelessWidget StatefulWidget
생성 - 생성자 → createState()
초기화 - initState()
의존성 변경 - didChangeDependencies()
UI 그리기 build() build()
위젯 재구성 부모 위젯이 바뀔 때마다 didUpdateWidget()
상태 변경 반영 - setState() → build() 재호출
임시 제거 - deactivate()
완전 제거 - dispose()

1-1 .StatelessWidget 특징

  • 단 하나의 메서드만 실행됨: build()
  • 위젯 트리에 삽입되거나, 부모 위젯이 변경될 때 호출됨
  • UI만 반환하며, 초기화나 정리 코드 삽입 불가
class MyStateless extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('안녕하세요');
  }
}

1-2. StatefulWidget 구조와 생명주기

두 부분으로 구성됨:

  • StatefulWidget 클래스
  • State 클래스

▶ StatefulWidget 단계

  • 생성자(Constructor): MyStateful({ Key? key }) : super(key: key);
  • createState(): 실제 상태를 관리할 State 객체 생성

▶ State 단계

  • initState(): 최초 한 번만 호출됨 (예: 초기 API 호출)
  • didChangeDependencies(): InheritedWidget 변경 시 호출
  • build(): UI 렌더링
  • setState(): 상태 변경 후 build 재호출
  • didUpdateWidget(): 부모 위젯의 속성이 변경되었을 때 호출
  • deactivate(): 위젯이 트리에서 임시 제거될 때
  • dispose(): 위젯이 완전히 제거될 때 (예: 리소스 정리)

📌 예제 코드: StatefulWidget

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: MyWidget(),
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('StatefulWidget 예제'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Counting : $_counter'),
            ElevatedButton(
              onPressed: _incrementCounter,
              child: Text('더하기'),
            ),
          ],
        ),
      ),
    );
  }
}

** Stateless Stateful 요약**

  • StatelessWidget
    • 상태 없음. UI만 담당.
    • 메서드: build() 한 개만 존재 (UI 렌더링 전용).
  • StatefulWidget
    • 내부 상태(State)를 관리할 수 있음.
    • 메서드 흐름:
      • 생성자 → createState()
      • 초기화 → initState()
      • 상태 변경 시 → setState() 호출 후 build() 재호출
      • 의존성 변경 → didChangeDependencies()
      • 위젯 갱신 → didUpdateWidget()
      • 제거 시 → dispose()
    • 동적 UI 변경에 적합.

이해가 좀 어려운 부분이 있지만 직접 사용해보면서 익혀야 할 듯함..


2.View 위젯

2-1.page view

아래의 코드를 실행했을때 예시 화면

예제 코드는 아래 더보기 클릭

 

더보기

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

void main() {
  runApp(const MyApp()); // 앱 실행, MyApp을 루트 위젯으로 사용
}

// 최상위 위젯: StatelessWidget
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false, // debug 배너 숨기기
      home: Scaffold(
        body: SampleWidget(), // SampleWidget을 화면에 띄움
      ),
    );
  }
}

// SampleWidget: 상태를 가지는 페이지 뷰 예제
class SampleWidget extends StatefulWidget {
  @override
  State createState() => _SampleWidgetState();
}

// 실제 상태와 로직을 관리하는 State 클래스
class _SampleWidgetState extends State {
  // PageView 제어용 컨트롤러
  final _controller = PageController();

  @override
  void initState() {
    super.initState();
    // PageController에 리스너 추가
    _controller.addListener(() {
      // 현재 스크롤 위치가 마지막 페이지일 때
      if (_controller.position.maxScrollExtent == _controller.offset) {
        // 다이얼로그로 알림
        showDialog(
          context: context,
          builder: (context) =>
              const CupertinoAlertDialog(content: Text('마지막에 도달했습니다.')),
        );
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose(); // 컨트롤러 해제
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        // 노치나 상태바 영역을 피해 배치
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // 상단 버튼: 2페이지로 즉시 이동
            Padding(
              padding: const EdgeInsets.all(15.0),
              child: ElevatedButton(
                onPressed: () {
                  _controller.jumpToPage(1); // 인덱스 1 페이지로 점프
                },
                child: const Text('2페이지로 가기'),
              ),
            ),
            // 남은 영역을 PageView가 채움
            Expanded(
              child: PageView(
                controller: _controller, // 위에서 만든 컨트롤러 연결
                scrollDirection: Axis.vertical, // 수직 스와이프 모드
                // 페이지 전환 시마다 호출
                onPageChanged: (int index) {
                  showDialog(
                    context: context,
                    builder: (context) =>
                        CupertinoAlertDialog(content: Text('$index 페이지 활성화')),
                  );
                },
                children: [
                  // 첫 번째 페이지
                  Container(
                    color: Colors.red,
                    child: const Center(
                      child: Text(
                        "1",
                        style: TextStyle(fontSize: 50, color: Colors.white),
                      ),
                    ),
                  ),
                  // 두 번째 페이지
                  Container(
                    color: Colors.blue,
                    child: const Center(
                      child: Text(
                        "2",
                        style: TextStyle(fontSize: 50, color: Colors.white),
                      ),
                    ),
                  ),
                  // 세 번째 페이지
                  Container(
                    color: Colors.yellow,
                    child: const Center(
                      child: Text(
                        "3",
                        style: TextStyle(fontSize: 50, color: Colors.white),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

코드를 보면서 어떤 기능을 하는지 생각 하자

pageView의 자주쓰는 옵션

  • children: 슬라이드할 위젯 목록을 전달(이미지, 레이아웃 등 자유롭게 사용)
  • scrollDirection: 스와이프 방향 설정(기본 Axis.horizontal, 세로는 Axis.vertical)
  • controller: PageController 연결로 코드 내 페이지 이동(jumpToPage 등)·위치 감지(addListener) 가능
  • pageSnapping: 스와이프 후 페이지 단위 고정 여부(기본 true이고, false면 이동한 만큼만 스크롤)
  • onPageChanged: 페이지 변경 시 호출되는 콜백, 현재 페이지 인덱스를 받아 인디케이터 제어나 이벤트 트리거에 활용

2-2.List view

이해가 안간다면 한번씩 사용해보자!

자주 사용하는 옵션

  • scrollDirection (`Axis.vertical` 기본, `horizontal` 가로 스크롤)
  • reverse (`false` 기본, `true`면 순서 역전 → 채팅 앱에 유용)
  • controller (`ScrollController`로 위치 제어·감시: `jumpTo()`, `addListener()`)
  • physics (`BouncingScrollPhysics`, `ClampingScrollPhysics` 등으로 스크롤 동작 커스터마이징)
  • padding (리스트 내부 여백 설정: `EdgeInsets`)
  • cacheExtent (미리 렌더링 거리 지정, 성능 최적화)

2-3.GridView

 

자주 사용하는 옵션

  • padding
    그리드 뷰 내부 여백 설정 (셀 가장자리와 컨테이너 사이의 간격).
  • controller
    ScrollController 연결로
    • 특정 위치로 스크롤 이동 (jumpTo, animateTo)
    • 현재 스크롤 위치 실시간 감지 (addListener)
    • 무한 스크롤링이나 페이지 로드 트리거에 사용
  • reverse
    스크롤 순서 반전 (false가 기본).
    • true로 설정 시 마지막 아이템이 가장 위(왼쪽)에 배치됨.
  • scrollDirection
    스크롤 축 설정 (Axis.horizontal, Axis.vertical).
    • 가로 스크롤과 세로 스크롤 중 선택 가능.
  • mainAxisSpacing & crossAxisSpacing
    셀 간 수평/수직 간격을 설정.
  • gridDelegate 그리드 레이아웃 정의,그리드뷰 같은 경우에는  이 옵션을  꼭 넣어줘야함
    • SliverGridDelegateWithFixedCrossAxisCount
      • crossAxisCount: 고정 열 수
    • SliverGridDelegateWithMaxCrossAxisExtent
      • maxCrossAxisExtent: 최대 타일 너비, 가용 너비에 따라 열 수 자동 계산

 

 

 

 

2-4.Tabbar

 

탭바 위젯은 우리가 흔히 쓰는 UI이다. 상단이나 하단에 메뉴를 넣고 해당 메뉴로 이동하고 싶을때 주로 사용한다.

이때 메뉴가 들어가는 상단이나 하단은 tabbar위젯을 사용하게 되고 그 컨텐츠는 tabbar view를 사용하게 된다.

 

예제

더보기

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(body: const SampleWidget()),
    );
  }
}

class SampleWidget extends StatefulWidget {
  const SampleWidget({super.key});

  @override
  State<SampleWidget> createState() => _SampleWidgetState();
}

class _SampleWidgetState extends State<SampleWidget>
    with TickerProviderStateMixin {
  //애니메이션 사용 규칙 , vsync와 같이사용함
  late TabController _tabController;
  //다른 스크롤 컨트롤과 다르게 controller을 생성할 때에 초기값을 설정해 줘야 합니다.

  @override
  void initState() {
    super.initState();
    _tabController = TabController(
      length: 3,
      vsync:
          this, //렌더링을 장치 디스플레이의 수직 동기화와 동기화할지 여부를 결정, this를 넣어주면 됩니다. 또한 이를 this 클래스에 매칭하기 위해서는 TickerProviderStateMixin라는 클래스를 with라는 키워드로 포함시켜야 합니다.
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          TabBar(
            controller: _tabController,
            labelColor: Colors.blue,
            unselectedLabelColor: Colors.grey,
            labelPadding: const EdgeInsets.symmetric(vertical: 20),
            tabs: const [Text('메뉴1'), Text('메뉴2'), Text('메뉴3')],
          ),
          Expanded(
            child: TabBarView(
              controller: _tabController,
              children: [
                Container(
                  color: Colors.blue,
                  child: Center(child: Text('메뉴1 페이지 ')),
                ),
                Container(
                  color: Colors.blue,
                  child: Center(child: Text('메뉴2 페이지 ')),
                ),
                Container(
                  color: Colors.blue,
                  child: Center(child: Text('메뉴3 페이지 ')),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

 

 

3. 레이아웃위젯

3-1.Container

플러터에서 가장 많이 쓰는 레이아웃 위젯

예제를 직접 써보며 확인하자.

더보기

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Container(
            padding: const EdgeInsets.only(left: 20, right: 20),
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [
                  Color.fromARGB(255, 255, 59, 98).withOpacity(0.7),
                  Color.fromARGB(255, 255, 59, 98),
                ],
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
              ),
              borderRadius: BorderRadius.circular(10),
              boxShadow: [
                BoxShadow(
                  color: Color.fromARGB(255, 255, 59, 98).withOpacity(0.5),
                  spreadRadius: 5,
                  blurRadius: 7,
                  offset: Offset(0, 3), // changes position of shadow
                ),
              ],
            ),
            width: 200,
            height: 150,
            child: Center(
              child: Text('Container', style: TextStyle(color: Colors.white)),
            ),
          ),
        ),
      ),
    );
  }
}

3-2. SizedBox

위젯과 위젯사이에 간격을 주고 싶을때 사용

더보기

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Row( //Column도 사용 가능
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(color: Colors.red, width: 100, height: 40),
            const SizedBox(width: 10),
            Container(color: Colors.blue, width: 100, height: 40),
          ],
        ),
      ),
    );
  }
}

**Row와 Column

Row

더보기

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center, //start , end, spaceBetween,spaceEvenly, spaceAround
            crossAxisAlignment: CrossAxisAlignment.stretch,//start. end
            children: List.generate(
              5,
              (index) => Container(
                width: 40,
                height: 40,
                color: Colors.red,
                margin: const EdgeInsets.all(5),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Column

더보기

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment
                .center, //end, spaceBetween, spaceAround, spaceEvenly, center
            crossAxisAlignment: CrossAxisAlignment
                .start, //end, spaceBetween, spaceAround, spaceEvenly , center
            children: List.generate(
              5,
              (index) => Container(
                width: 40,
                height: 40,
                color: Colors.red,
                margin: const EdgeInsets.all(5),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

 

3-2.Expanded

공간을 확장해서 사용할때!

컬럼과 로우에서 사용되고 있는 아이템들(위젯)간의 간격을 어떻게 제공하고 설정할 수 있는 강력한 위젯

컬럼과 로우에서 사용된다

flex를 사용해서 어느정도 공간을 가져갈건지 정할수있다.

더보기

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Expanded(
                flex: 1, // 1의 공간을 가져가겠다
                child: Container(
                  height: 40,
                  color: Colors.red,
                  margin: const EdgeInsets.all(5),
                ),
              ),
              Expanded(
                flex: 3, // 2
                child: Container(
                  height: 40,
                  color: Colors.red,
                  margin: const EdgeInsets.all(5),
                ),
              ),
              Expanded(
                flex: 2, // 1
                child: Container(
                  height: 40,
                  color: Colors.red,
                  margin: const EdgeInsets.all(5),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}