본문 바로가기

flutter앱개발과정

flutter앱개발 - view위젯

1. View 위젯

1-1. PageView 위젯

PageView 위젯은 책이나 프레젠테이션처럼 가로 또는 세로로 스와이프 할 수 있는 일련의 페이지를 만들기 위한 위젯입니다.

스와이프 제스처로 스크롤 하거나 페이징 할 수 있는 위젯입니다. 앱에서 대표적으로 사용되는 예로는 앱을 처음 설치하게 되면 튜토리얼 페이지가 옆으로 스와이프 하면서 사용법을 전달하는 UI가 있는데 PageView 위젯을 사용하면 손쉽게 개발할 수 있습니다.

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: PageView(
        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),
              ),
            ),
          ),
        ],
      )),
    );
  }
}

예제 소스처럼 아주 쉽게 슬라이드 가능한 3가지 레이아웃 위젯을 만들 수 있습니다.

 

- 자주 사용되는 PageView 위젯의 옵션

  • children

슬라이드를 사용할 자식 위젯을 만들어줄 수 있습니다. 위젯 타입 리스트라서 간단한 이미지부터 복잡한 레이아웃 위젯 UI도 넣을 수 있습니다.

  • scrollDirection
  • 슬라이드 방향을 결정할 수 있습니다. 기본값은 horizontal(가로) 방향입니다. Axis.vertical 옵션을 설정하면 세로 방향으로 슬라이드로 페이지를 전환 시킬 수 있게 됩니다.
scrollDirection: Axis.vertical,

세로스크롤 옵션

  • controller

controller 옵션에 등록할 PageController 클래스를 통해서 원하는 스크롤 위치나 페이지로 이동하게 하거나 현재 스크롤링 되고 있는 위치를 실시간으로 전달받는 등의 이벤트 처리를 할 수 있습니다.

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: SampleWidget()));
  }
}

class SampleWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _SampleWidgetState();
}

class _SampleWidgetState extends State<SampleWidget> {
  final _controller = PageController();

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      if (_controller.position.maxScrollExtent == _controller.offset) {
        showDialog(
          context: context,
          builder: (context) => const CupertinoAlertDialog(
            content: Text('마지막에 도달했습니다.'),
          ),
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: SafeArea(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Padding(
            padding: const EdgeInsets.all(15.0),
            child: ElevatedButton(
              onPressed: () {
                _controller.jumpToPage(1);
              },
              child: Text('2페이지로 가기'),
            ),
          ),
          Expanded(
            child: PageView(
              scrollDirection: Axis.vertical,
              controller: _controller,
              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),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    ));
  }
}

PageViewController 활용

 

  • pageSnapping

PageView 페이지 전환 시 자동으로 페이지 전체를 볼 수 있도록 자석 효과가 있는데 이 옵션을 false를 주게 되면 사용자가 움직이는 만큼 이동하고 고정되게 처리할 수 있습니다.

 pageSnapping: false,

  • onPageChanged

이 옵션을 콜백 함수를 등록할 수 있습니다. 해당 옵션을 사용하면 사용자가 스와이프를 이용해 페이지를 이동하거나 버튼을 통해 페이지 이동을 했을 때 콜백 함수가 호출되며 현재 보고 있는 페이지의 번호(index)를 반환해 줍니다. 이를 통해 각 페이지마다 특정 이벤트를 수행하거나 페이지 인디케이터를 제어할 수 있습니다.

onPageChanged: (int index) {
  showDialog(
    context: context,
    builder: (context) => CupertinoAlertDialog(
      content: Text('$index 페이지 활성화'),
    ),
  );
},

 

1-2. ListView 위젯

ListView 위젯은 스크롤 가능한 위젯 목록을 표시하는 데 사용됩니다. 가로/세로 스크롤 할 수 있는 긴 항목 목록을 만드는 데 사용할 수 있습니다.

 

- 자주 사용되는 ListView 위젯의 옵션

  • scrollDirection

scrollDirection 옵션은 Axis enum 타입의 값을 지정해야 하며 horizontal 과 vertical 두 가지를 설정할 수 있습니다. 기본값은 세로(vertical)입니다.

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: ListView(
          scrollDirection: Axis.horizontal,
          children: List.generate(
            10,
            (index) => Container(
              width: 100,
              height: 100,
              margin: const EdgeInsets.all(5),
              color: Colors.red.withAlpha((index + 1) * 25),
            ),
          ),
        ),
      ),
    );
  }
}

ListView위젯 scrollDirection 예

 

  • reverse

스크롤 방향을 반대로 하여 정렬한다는 뜻입니다. 기본 값은 false이며 true로 설정할 경우 가장 마지막에 배치된 것이 가장 위로 보이게 됩니다. 보통 채팅 앱에서 최신 데이터가 가장 밑에 보이는 것이 reverse 옵션을 true 설정하면 처리가 되겠지요.

 

reverse: true,
  • controller

controller 옵션에 등록할 ScrollController 클래스를 통해서 원하는 스크롤 위치로 이동하게 하거나 현재 스크롤링 되고 있는 위치를 실시간으로 전달받는 등의 이벤트 처리를 할 수 있습니다. 보통의 경우 controller 없이 등록을 하여 사용하게 되지만 특정 요구사항이나 무한 스크롤링의 경우 마지막 스크롤 위치에서 다음 페이지를 호출하게 하기 위한 이벤트 처리를 scrollController를 등록함으로 처리할 수 있습니다.

 

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: SampleWidget(),
      ),
    );
  }
}

class SampleWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _SampleWidgetState();
}

class _SampleWidgetState extends State<SampleWidget> {
  final _controller = ScrollController(); // 1번

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            SizedBox(
              height: 50,
              child: ElevatedButton(
                onPressed: () {
                  _controller.jumpTo(330); // 3번
                },
                child: const Text('3번영역으로 이동'),
              ),
            ),
            Expanded(
              child: ListView(
                controller: _controller, // 2번
                children: List.generate(
                  10,
                  (index) => Container(
                    width: 100,
                    height: 100,
                    margin: const EdgeInsets.all(5),
                    color: Colors.red.withAlpha((index + 1) * 25),
                    child: Center(child: Text(index.toString())),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

ListView옵션 controller예

 

1번에서 ScrollController를 생성하며 2번에서 ListView에 controller를 1번에서 만든 controller를 등록해 줍니다. 그때부터 _controller를 통해 스크롤을 제어할 수 있게 됩니다. 상단에 버튼 클릭(3번 영역으로 이동)을 통해 jumpTo 함수를 통해 3번 UI 위젯에 자동으로 배치할 수 있습니다

 

.

  • physicsphysics 옵션에 들어갈 수 있는 다양한 효과의 경우 다음과 같이 정리할 수 있습니다.
    • BouncingScrollPhysics
    • 스크롤 범위가 내부 콘텐츠보다 큰 경우 스크롤 끝에서 반사 효과를 제공합니다. iOS 스크롤 동작과 유사합니다.
    • ClampingScrollPhysics
    • 스크롤 범위가 내부 컨텐츠보다 클 때 끝에서 반사 효과 대신 스크롤을 클램핑합니다. Android 스크롤 동작과 유사합니다.
    • FixedExtentScrollPhysics
    • 모든 아이템이 동일한 크기를 가지는 경우 사용됩니다. 스크롤을 균일한 단위로 이동합니다.
    • NeverScrollableScrollPhysics
    • 스크롤이 비활성화된 상태입니다.
    각각 직접 넣어 보면서 어떻게 동작되는지 확인을 하는 것을 추천 드립니다. 사용방법에 대한 소스 코드는 대표적으로 ClampingScrollPhysics 를 예를 들겠습니다.
  • physics 옵션은 ListView 위젯에서 스크롤 동작을 결정하는 데 사용되는 속성입니다. 이 속성을 통해 스크롤 동작을 커스터마이즈 하여 사용자가 앱에서 더 나은 스크롤 경험을 할 수 있도록 할 수 있습니다.
ListView(
    controller: _controller,
    physics: const ClampingScrollPhysics(), //다른 설정의 클래스를 넣어주시면 됩니다.
    children: List.generate(
      10,
      (index) => Container(
        width: 100,
        height: 100,
        margin: const EdgeInsets.all(5),
        color: Colors.red.withAlpha((index + 1) * 25),
        child: Center(child: Text(index.toString())),
      ),
    ),
),

ListView옵션 physics 예

 

  • padding

리스트 뷰 내부에 padding을 통해 간격을 설정 할 수 있습니다.

 

  • cacheExtent예를 들어, cacheExtent를 1000.0으로 설정하면, 사용자가 리스트 뷰의 하단으로 스크롤 할 때 1000.0 높이의 캐시가 생성됩니다. 이는 다음에 사용자가 상단으로 스크롤 할 때, 이전에 캐시 된 아이템이 즉시 로드되어 보여줄 수 있습니다.
  • cacheExtent 속성은 리스트 뷰의 성능을 향상시키는 데 도움이 됩니다. 그러나 이 값이 너무 크면 리스트 뷰를 초기화할 때 시간이 오래 걸릴 수 있습니다. 따라서 적절한 cacheExtent 값은 리스트 뷰의 크기와 컨텐츠 양에 따라 달라집니다.
  • ListView 위젯에서 cacheExtent 옵션은 리스트 뷰에서 양쪽 방향으로 스크롤 할 때 캐시 할 수 있는 추가적인 영역을 정의하는 속성입니다. 캐시 된 아이템은 스크롤 시 더 빠르게 로드됩니다.

1-3. GridView 위젯

GridView 위젯은 자식 위젯을 행과 열이 있는 그리드 형식으로 레이아웃을 구성할 수 있도록 도와주는 위젯입니다. 이 위젯으로 이미지 또는 텍스트 항목 모음과 같은 동일한 유형의 위젯 모음을 만드는데 유용합니다.

 

- 자주사용되는 GridView 위젯의 옵션

 

  • gridDelegate

이 매개변수는 열 수, 크기 및 열 사이의 간격을 포함하여 그리드의 레이아웃을 정의합니다. gridDelegate에는 두 가지 방식으로 열을 정하게 됩니다.

  1. SliverGridDelegateWithFixedCrossAxisCount

타일 크기에 관계없이 열 수가 고정된 그리드를 만듭니다. 그리드의 열 수를 지정하는 crossAxisCount라는 매개변수를 사용합니다. 예를 들어 crossAxisCount가 3으로 설정된 경우 그리드에는 3개의 열이 있고 행 수는 타일 수를 기준으로 계산됩니다.

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: GridView(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            mainAxisSpacing: 2,
            crossAxisSpacing: 2,
          ),
          children: List.generate(
            100,
            (index) => Center(
              child: Container(
                color: Colors.grey,
                child: Center(child: Text(index.toString())),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

SliverGridDelegateWithFixedCrossAxisCount 예제

2. SliverGridDelegateWithMaxCrossAxisExtent

최대 너비가 고정된 타일로 그리드를 만들고 타일의 너비를 기준으로 열 수를 계산합니다. 타일의 최대 너비를 지정하는 maxCrossAxisExtent라는 매개 변수를 사용합니다. 예를 들어 maxCrossAxisExtent가 200으로 설정된 경우 각 타일의 너비는 200을 넘지 않으며 그리드의 열 수는 사용 가능한 너비를 기준으로 계산됩니다.

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: GridView(
          gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 300,
            mainAxisSpacing: 2,
            crossAxisSpacing: 2,
          ),
          children: List.generate(
            100,
            (index) => Center(
              child: Container(
                color: Colors.grey,
                child: Center(child: Text(index.toString())),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

 

  • mainAxisSpacing & crossAxisSpacing

셀 간 간격을 설정합니다.

 

  • scrollDirection

scrollDirection 옵션은 Axis enum 타입의 값을 지정해야 하며 horizontal 과 vertical 두 가지를 설정할 수 있습니다. 값에서 알 수 있듯 가로 스크롤을 원할때는 horizontal을, 세로 스크롤을 원할 때는 vertical 옵션을 사용합니다.

 

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: GridView(
          scrollDirection: Axis.horizontal,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            mainAxisSpacing: 5,
            crossAxisSpacing: 5,
          ),
          children: List.generate(
            100,
            (index) => Center(
              child: Container(
                color: Colors.grey,
                child: Center(child: Text(index.toString())),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

 

  • reverse

스크롤 방향을 반대로 하여 정렬한다는 뜻입니다. 기본 값은 false이며 true로 설정할 경우 가장 마지막에 배치된 것이 가장 위로 보이게 됩니다.

 

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: GridView(
          scrollDirection: Axis.vertical,
          reverse: true,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            mainAxisSpacing: 5,
            crossAxisSpacing: 5,
          ),
          children: List.generate(
            100,
            (index) => Center(
              child: Container(
                color: Colors.grey,
                child: Center(child: Text(index.toString())),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

 

  • controller

controller 옵션에 등록할 ScrollController 클래스를 통해서 원하는 스크롤 위치로 이동하게 하거나 현재 스크롤링 되고 있는 위치를 실시간으로 전달받는 등의 이벤트 처리를 할 수 있습니다. 보통의 경우 controller 없이 등록을 하여 사용하게 되지만 특정 요구사항이나 무한 스크롤링의 경우 마지막 스크롤 위치에서 다음 페이지를 호출하게 하기 위한 이벤트 처리를 scrollController를 등록함으로 처리할 수 있습니다.

 

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> {
  final _controller = ScrollController();

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      // 1번
      if (_controller.position.maxScrollExtent == _controller.offset) {
        showDialog(
          context: context,
          builder: (context) => const CupertinoAlertDialog(
            content: Text('마지막에 도달했습니다.'),
          ),
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: [
            ElevatedButton(
                onPressed: () {
                  _controller.jumpTo(800);
                },
                child: const Text('28번째로 이동')),
            Expanded(
              child: GridView(
                controller: _controller,
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 3,
                  mainAxisSpacing: 5,
                  crossAxisSpacing: 5,
                ),
                children: List.generate(
                  100,
                  (index) => Center(
                    child: Container(
                      color: Colors.grey,
                      child: Center(child: Text(index.toString())),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

 

 

  • padding

그리드 뷰 내부에 padding을 통해 간격을 설정 할 수 있습니다.

 

 

1-4. TabBar 위젯

 

TabBar 위젯은 2가지 위젯이 필요합니다. 메뉴 위젯과 메뉴와 매칭이 되는 뷰 위젯입니다. TabBar 위젯과 TabBarView 위젯 이렇게 2개를 구성을 해야만 사용할 수 있습니다. 또한 두 개의 위젯을 커뮤니케이션을 도와주는 녀석을 연결해 줘야 합니다. 그것이 바로 TabController입니다. 고로 TabBar를 사용하기 위해서는 아래 3가지 정의가 필요합니다.

 

  • TabController 정의
  • TabBar 위젯 정의
  • TabBarView 위젯 정의

TabController 정의

다른 스크롤 컨트롤과 다르게 controller을 생성할 때에 초기값을 설정해 줘야 합니다.

 

- TabController를 초기값을 정의할 수 있는 옵션

class _SampleWidgetState extends State<SampleWidget>
    with TickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(
      length: 3,
      vsync: this,
    );
  }
  .... 이하 생략

TabBar  Controller 초기화 예제

 

  • initialIndex
  • 말 그대로 초기 Tab 메뉴와 TabView 가 어떤 페이지를 먼저 보여줄 것인지 설정하는 값입니다. 보통은 첫 번째 탭이 활성화되어 사용되기 때문에 특정할 때아니고는 initialIndex는 설정하지 않습니다.
  • animationDuration
  • animationDuration 설정을 통해 탭 메뉴를 눌렀을 때 탭 메뉴 하단의 인디케이터의 애니메이션 효과 시간을 정할 수 있습니다. Duration 객체를 통해 조절할 수 있습니다.
  • length
  • 메뉴가 몇 개인지를 설정하는 옵션입니다. 반드시 넣어야 하는 필수 값입니다.

* TabBar의 tabs에는 4개를 등록하고 controller의 length 값을 4가 아닌 다른 값을 넣으면 오류가 발생됩니다.

 

  • vsync
  • 'TabController'의 'vsync' 옵션은 탭 컨트롤러의 렌더링을 장치 디스플레이의 수직 동기화와 동기화할지 여부를 결정합니다. 이 부분의 경우 this를 넣어주면 됩니다. 또한 이를 this 클래스에 매칭하기 위해서는 TickerProviderStateMixin라는 클래스를 with라는 키워드로 포함시켜야 합니다.

TabBar 위젯 정의

TabBar 위젯을 선언하고 controller를 반드시 넣어야 합니다. 또한 초기에 탭 메뉴를 3개를 사용한다고 정의하였기 때문에 탭 메뉴를 3개를 등록해 줘야 합니다.

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 {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(
      length: 3,
      vsync: this,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TabBar(
        controller: _tabController,
        tabs: const [
          Text('메뉴1'),
          Text('메뉴2'),
          Text('메뉴3'),
        ],
      ),
    );
  }
}

TabBar 위젯

 

- 자주 사용되는 TabBar의 옵션

  • labelColor
  • 메뉴 색상을 말합니다.
  • unselectedLabelColor
  • 선택된 메뉴외의 색상을 말합니다.
  • labelPadding
  • 메뉴의 간격을 조정을 합니다.
  • indicatorWeight
  • 선택된 메뉴의 인디게이터의 두께를 조절합니다.
  • labelStyle , unselectedLabelStyle
  • 활성화된 메뉴 텍스트 스타일과 비활성중인 텍스트 스타일 설정
TabBar(
  controller: _tabController,
  labelColor: Colors.blue,
  unselectedLabelColor: Colors.grey,
  labelPadding: const EdgeInsets.symmetric(vertical: 20),
  tabs: const [
    Text('메뉴1'),
    Text('메뉴2'),
    Text('메뉴3'),
  ],
),

 

 

TabBarView 위젯

이제 메뉴를 구성했으니 화면이 될 부분을 정의할 차례입니다. 탭 메뉴와 상호작용이 되기 위해서 controller를 연결해 주고 또한 메뉴가 3개이기 때문에 뷰 화면도 3개를 등록해 줍니다.

 

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 {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(
      length: 3,
      vsync: this,
    );
  }

  @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 페이지 ')),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

TabBarView 위젯예제