1. 시작하기에 앞서
1. 샘플코드 확인하기
- 처음 다루는 위젯은 샘플 코드를 통해 구조를 파악하는 것이 먼저
import 'package:flutter/material.dart';
/// Flutter code sample for [TabBar].
void main() => runApp(const TabBarApp());
class TabBarApp extends StatelessWidget {
const TabBarApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const TabBarExample(),
);
}
}
class TabBarExample extends StatelessWidget {
const TabBarExample({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
initialIndex: 1, // 탭의 시작점 >> 시작 값 0
length: 3, // 탭의 갯수
child: Scaffold(
appBar: AppBar(
title: const Text('TabBar Sample'),
bottom: const TabBar(
// 탭의 갯수 만큼 생성
tabs: <Widget>[
Tab(
icon: Icon(Icons.cloud_outlined),
),
Tab(
icon: Icon(Icons.beach_access_sharp),
),
Tab(
icon: Icon(Icons.brightness_5_sharp),
),
],
),
),
// 각 탭의 내용
body: const TabBarView(
children: <Widget>[
Center(
child: Text("It's cloudy here"),
),
Center(
child: Text("It's rainy here"),
),
Center(
child: Text("It's sunny here"),
),
],
),
),
);
}
}

2. 변형시켜 확인하기
- 탭의 개수와 탭 아이콘, 내용을 변형시켜 확인
import 'package:flutter/material.dart';
/// Flutter code sample for [TabBar].
void main() => runApp(const TabBarApp());
class TabBarApp extends StatelessWidget {
const TabBarApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const TabBarExample(),
);
}
}
class TabBarExample extends StatelessWidget {
const TabBarExample({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
initialIndex: 1,
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('TabBar Sample'),
bottom: const TabBar(
tabs: <Widget>[
Tab(
icon: Icon(Icons.directions_subway),
),
Tab(
icon: Icon(Icons.directions_car),
),
],
),
),
body: TabBarView(
children: <Widget>[
Column(
children: [
Container(height: 100, color: Colors.red),
Container(height: 100, color: Colors.yellow),
Container(height: 100, color: Colors.blue),
],
),
Center(
child: Text("It's sunny here"),
),
],
),
),
);
}
}

2. 적용시켜 만들기
- 탭 안에 사진들 넣기
- 색깔 컨테이너 대신 사진들을 그리드로 넣어야 함

TabBarView( // 탭 전환에 따라 다른 화면을 보여주는 위젯
children: <Widget>[ // 각 탭에 해당하는 콘텐츠를 정의하는 리스트
GridView.builder( // 유동적인 그리드 레이아웃을 생성하는 위젯
physics: NeverScrollableScrollPhysics(), // 그리드 자체 스크롤을 비활성화
itemCount: count, // 그리드 아이템의 개수
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 열의 개수를 3으로 고정
),
itemBuilder: (context, index) { // 각 아이템을 빌드하는 함수
return Image.network( // 네트워크에서 이미지를 가져오는 위젯
"https://picsum.photos/id/${200 + index}/100/100", // 이미지 URL, 각 index에 따라 변경, 더미 사진가져오는 사이트
fit: BoxFit.cover, // 이미지가 위젯 크기에 맞게 채워짐
);
},
),
Center( // 중앙에 정렬된 위젯을 배치
child: Text("It's sunny here"), // 텍스트를 화면에 표시
),
],
);

3. 상단 부와 합치기
1. 상단 부 코드
- 제작과정은 생략(전과 비슷함)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp()); // 앱의 진입점, MyApp 위젯 실행
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, // 디버그 배너 숨김
theme: ThemeData(useMaterial3: true), // Material 3 디자인 적용
home: TabBarExample(), // 메인 화면으로 TabBarExample 설정
);
}
}
class TabBarExample extends StatelessWidget {
const TabBarExample({super.key});
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width; // 화면의 너비 가져오기
double gridBoxHeight = width / 3; // 3등분한 그리드 박스 높이 계산
int count = 30; // 그리드 아이템 개수
double tabbarHeight = 50; // 탭바 높이
double tabControllerHeight = tabbarHeight + // 탭 컨트롤러 총 높이 계산
(gridBoxHeight * ((count ~/ 3) + (count % 3 > 0 ? 1 : 0)));
// 행의 개수에 따라 추가 높이 설정
return Scaffold(
endDrawer: Container( // 오른쪽에 슬라이드로 열리는 드로어
width: 200, // 드로어의 너비
color: Colors.yellow, // 드로어 배경색 설정
),
appBar: AppBar( // 상단 앱바 설정
leading: Icon(Icons.arrow_back_ios), // 뒤로가기 아이콘
title: Text("Profile"), // 앱바 제목 설정
centerTitle: true, // 제목을 가운데 정렬
),
body: ListView( // 스크롤 가능한 리스트뷰
children: [
ProfileImageAndName(), // 프로필 이미지와 이름 위젯
ProfileCountInfo(), // 프로필 관련 정보 위젯
SizedBox(
height: 10, // 리스트뷰 내 간격 추가
),
ButtonBox(), // 버튼 관련 위젯
],
),
);
}
}
class ProfileImageAndName extends StatelessWidget {
const ProfileImageAndName({
super.key,
});
@override
Widget build(BuildContext context) {
return Row( // 가로 방향으로 정렬되는 위젯
mainAxisAlignment: MainAxisAlignment.start, // 가로 방향으로 왼쪽 정렬
children: [
SizedBox(
width: 25, // 프로필 이미지 왼쪽에 여백 추가
),
SizedBox(
width: 100, // 프로필 이미지의 너비 설정
height: 100, // 프로필 이미지의 높이 설정
child: CircleAvatar( // 원형 아바타 위젯
backgroundImage: AssetImage("avatar.png"), // 로컬 이미지 파일 설정
),
),
SizedBox(
width: 25, // 프로필 이미지와 텍스트 간의 여백 추가
),
Column( // 세로 방향으로 정렬되는 위젯
crossAxisAlignment: CrossAxisAlignment.start, // 텍스트를 왼쪽 정렬
children: [
Text(
"GetinThere",
style: TextStyle(
fontSize: 25, // 글자 크기
fontWeight: FontWeight.w700, // 글자 굵기
),
),
Text(
"프로그래머/작가/강사",
style: TextStyle(
fontSize: 20, // 글자 크기
),
),
Text("데어 프로그래밍"),
],
),
],
);
}
}
class ProfileCountInfo extends StatelessWidget {
const ProfileCountInfo({
super.key,
});
@override
Widget build(BuildContext context) {
return Row( // 가로로 정렬되는 위젯
mainAxisAlignment: MainAxisAlignment.spaceAround, // 자식 위젯들 사이의 간격을 고르게 분배
children: [
ProfileInfo( // 첫 번째 정보 (포스트 개수)
myCount: "50",
myText: "Posts",
),
GetLine(),
ProfileInfo( // 두 번째 정보 (좋아요 개수)
myCount: "10", // 숫자 정보
myText: "Likes", // 텍스트 정보
),
GetLine(),
ProfileInfo( // 세 번째 정보 (공유 개수)
myCount: "3", // 숫자 정보
myText: "Share", // 텍스트 정보
),
],
);
}
}
class GetLine extends StatelessWidget {
const GetLine({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
width: 2,
height: 60,
color: Colors.blue,
);
}
}
class ProfileInfo extends StatelessWidget {
final myCount;
final myText;
const ProfileInfo({required this.myCount, required this.myText});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("${myCount}"),
Text("${myText}"),
],
);
}
}
class ButtonBox extends StatelessWidget {
const ButtonBox({
super.key,
});
@override
Widget build(BuildContext context) {
return Row( // 가로로 정렬되는 위젯
mainAxisAlignment: MainAxisAlignment.spaceAround, // 자식 위젯들 간의 간격을 고르게 분배
children: [
TextButton( // 첫 번째 버튼
onPressed: () {}, // 버튼 클릭 시 실행될 콜백 (현재 비어 있음)
child: Text("Follow"), // 버튼 텍스트
style: TextButton.styleFrom( // TextButton의 스타일 설정
backgroundColor: Colors.blueAccent, // 버튼 배경색
foregroundColor: Colors.white, // 버튼 텍스트 색상
shape: RoundedRectangleBorder( // 버튼 테두리 모양 설정
borderRadius: BorderRadius.circular(10), // 모서리를 둥글게 처리
),
minimumSize: Size(170, 40), // 버튼의 최소 크기 설정 (너비, 높이)
),
),
OutlinedButton( // 두 번째 버튼
onPressed: () {}, // 버튼 클릭 시 실행될 콜백 (현재 비어 있음)
child: Text("Message"), // 버튼 텍스트
style: OutlinedButton.styleFrom( // OutlinedButton의 스타일 설정
minimumSize: Size(170, 40), // 버튼의 최소 크기 설정 (너비, 높이)
foregroundColor: Colors.black, // 버튼 텍스트 및 테두리 색상
shape: RoundedRectangleBorder( // 버튼 테두리 모양 설정
borderRadius: BorderRadius.circular(10), // 모서리를 둥글게 처리
),
),
),
],
);
}
}

2. 탭바도 합친 전체 코드
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,
theme: ThemeData(useMaterial3: true),
home: TabBarExample(),
);
}
}
class TabBarExample extends StatelessWidget {
const TabBarExample({super.key});
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double gridBoxHeight = width / 3;
int count = 30;
double tabbarHeight = 50;
double tabControllerHeight = tabbarHeight +
(gridBoxHeight * ((count ~/ 3) + (count % 3 > 0 ? 1 : 0)));
return Scaffold(
endDrawer: Container(
width: 200,
color: Colors.yellow,
),
appBar: AppBar(
leading: Icon(Icons.arrow_back_ios),
title: Text("Profile"),
centerTitle: true,
),
body: ListView(
children: [
ProfileImageAndName(),
ProfileCountInfo(),
SizedBox(
height: 10,
),
ButtonBox(),
ProfileTabbar(tabControllerHeight: tabControllerHeight, count: count),
],
),
);
}
}
class ProfileTabbar extends StatelessWidget {
const ProfileTabbar({
super.key,
required this.tabControllerHeight,
required this.count,
});
final double tabControllerHeight;
final int count;
@override
Widget build(BuildContext context) {
return SizedBox(
height: tabControllerHeight,
child: DefaultTabController(
initialIndex: 0, // 시작 인덱스 0
length: 2,
child: Column(
children: [
const TabBar(
tabs: <Widget>[
Tab(
icon: Icon(Icons.directions_subway),
),
Tab(
icon: Icon(Icons.directions_car),
),
],
),
Expanded(
child: TabBarView(
children: <Widget>[
GridView.builder(
physics: NeverScrollableScrollPhysics(),
itemCount: count,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3),
itemBuilder: (context, index) {
return Image.network(
"https://picsum.photos/id/${200 + index}/100/100",
fit: BoxFit.cover,
);
},
),
Center(
child: Text("여긴 또 다른 탭의 영역"),
),
],
),
),
],
),
),
);
}
}
class ButtonBox extends StatelessWidget {
const ButtonBox({
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
onPressed: () {},
child: Text("Follow"),
style: TextButton.styleFrom(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
minimumSize: Size(170, 40),
),
),
OutlinedButton(
onPressed: () {}, // 버튼 클릭 시 동작
child: Text("Message"), // 버튼의 텍스트
style: OutlinedButton.styleFrom(
minimumSize: Size(170, 40),
foregroundColor: Colors.black, // 텍스트 색상
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // 모서리 둥근 정도
),
),
),
],
);
}
}
class ProfileCountInfo extends StatelessWidget {
const ProfileCountInfo({
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ProfileInfo(
myCount: "50",
myText: "Posts",
),
GetLine(),
ProfileInfo(
myCount: "10",
myText: "Likes",
),
GetLine(),
ProfileInfo(
myCount: "3",
myText: "Share",
),
],
);
}
}
class GetLine extends StatelessWidget {
const GetLine({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
width: 2,
height: 60,
color: Colors.blue,
);
}
}
class ProfileImageAndName extends StatelessWidget {
const ProfileImageAndName({
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: 25,
),
SizedBox(
width: 100,
height: 100,
child: CircleAvatar(
backgroundImage: AssetImage("avatar.png"),
),
),
SizedBox(
width: 25,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"GetinThere",
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w700,
),
),
Text(
"프로그래머/작가/강사",
style: TextStyle(
fontSize: 20,
),
),
Text("데어 프로그래밍"),
],
),
],
);
}
}
class ProfileInfo extends StatelessWidget {
final myCount;
final myText;
const ProfileInfo({required this.myCount, required this.myText});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("${myCount}"),
Text("${myText}"),
],
);
}
}

4. 주의사항
Grid
와ListView
를 혼용하면 앱에서 스크롤이 2개가 되고Grid
가 화면을 모두 차지하면ListView
의 스크롤이 사라져 프로필 상단부를 다시 못보게 됨 +Grid
의 영역을 미리 구해두지 않으면 앱에서 해당 영역의 사이즈를 몰라 무너지게 됨
- 이를 방지하기 위해
physics: NeverScrollableScrollPhysics()
을 통해Grid
의 스크롤을 비활성화 시키고 아래 코드의 산수식을 통해 미리Grid
의 영역을 계산하여 넣어둠
class TabBarExample extends StatelessWidget {
const TabBarExample({super.key});
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double gridBoxHeight = width / 3; // 정사각형이 가로로 3개 들어감
int count = 30; // Grid에 들어갈 아이템 수
double tabbarHeight = 50;
double tabControllerHeight = tabbarHeight +
(gridBoxHeight * ((count ~/ 3) + (count % 3 > 0 ? 1 : 0)));
...
class ProfileTabbar extends StatelessWidget {
const ProfileTabbar({
super.key,
required this.tabControllerHeight,
required this.count,
});
final double tabControllerHeight;
final int count;
@override
Widget build(BuildContext context) {
return SizedBox(
height: tabControllerHeight,
child: DefaultTabController(
initialIndex: 0, // 시작 인덱스 0
length: 2,
child: Column(
children: [
const TabBar(
tabs: <Widget>[
Tab(
icon: Icon(Icons.directions_subway),
),
Tab(
icon: Icon(Icons.directions_car),
),
],
),
Expanded(
child: TabBarView(
children: <Widget>[
GridView.builder(
physics: NeverScrollableScrollPhysics(),
itemCount: count,
...
마지막. 추가 정보 정리
1. 단축어(숏컷) 생성하기



2. 선언형 UI
- 정의
- 선언형 UI(Declarative UI)는 UI의 상태를 선언적으로 정의하는 방식.
- UI가 어떻게 보여야 할지에 대한 선언만 이루어지고, 실제 렌더링은 프레임워크나 시스템이 관리.
- 특징
- UI는 상태를 기반으로 렌더링.
- UI와 로직이 분리되어 있음.
- 시스템이 렌더링을 최적화하여 효율적.
- 예시
- Flutter에서 setState(), StreamBuilder를 통해 상태 변경을 선언적으로 처리.
setState(() { _counter++; });
- 장점
- UI와 로직이 분리되어 가독성과 유지보수가 용이.
- 상태에 따라 자동으로 UI가 업데이트되어 직관적이고 오류 가능성 감소.
3. 뷰홀더 패턴
- 정의
- 반복되는 아이템을 다룰 때 성능을 최적화하는 디자인 패턴.
- 뷰를 재사용하여 메모리 사용을 최소화.
2. 특징
- 화면에 표시되지 않는 뷰는 재사용.
- 성능 최적화가 가능.
- 리스트나 그리드에서 대량의 데이터를 처리할 때 유용.
- 예시
- Android에서 RecyclerView와 ViewHolder를 사용하여 뷰를 재사용.
- ViewHolder는 뷰를 재사용하는 객체.
public class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public MyViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.textView);
}
}
4. 장점
- 메모리 효율성 증가.
- 성능 향상.
- 빠르고 효율적인 렌더링 제공.
Share article