Contents
출처Preview화면 구조TabBar Sampling🧾 오류 메시지 해석:📌 간단한 설명:📉 예시 (잘못된 코드):✅ 해결 방법 (예시 코드):💬 요약Setting기본 코드LayOutAppbar 만들기Profile 영역 만들기TabBarGridView✅ Flutter에서 흔히 보는 Builder 패턴의 예시들✅ 요약: Flutter에서 Builder란?✅ 다른 예: FutureBuilder✅ 수학적 집합과 Builder의 연결 고리✅ 다시 말해✅ 왜 중요한가?✅ 비슷한 예시: 수학적 집합의 정의와 코드 비교✅ 핵심 차이 요약✅ 예시로 보는 차이점✅ 비유로 설명하면✅ 함께 사용하는 경우✅ 요약DesignProfileHeader DesignSizedBox 사용해서 위젯 사이사이 Gap주기GridView 영역 사진 Gap주기FollowButton & MessageButton DesignTabBar Icon 변경완성출처
만들면서 배우는 플러터 앱 프로그래밍 7가지 모바일 앱 UI 제작 & RiverPod 상태 관리
Preview
- ThemeData Class
- TabBar 위젯
- TabBarView 위젯
- AppBar 위젯
- InkWell 위젯
- GridView 위젯
- Drawer 위젯
- Align 위젯
- Image 위젯으로 network 이미지를 다운 받아서 화면에 표시하는 방법
화면 구조

Setting
기본 코드
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Placeholder(),
);
}
}
LayOut
Appbar 만들기
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Placeholder(),
);
}
}

Appbar에 뒤로 가기(ios), title, 햄버거 버튼 만들기 + 아이콘 색상 바꾸기
- 뒤로가기 아이콘 (ios)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
),
body: Placeholder(),
);
}
}

- title
- 안드로이드는 title이 왼쪽, IOS는 가운데에 위치한다
- 안드로이드 title이 가운데에 위치하게 하려면 centertitle = true
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
),
body: Placeholder(),
);
}
}

- 햄버거 버튼
- endDrawer → 햄버거 버튼이 오른쪽에 위치
- drawer → 햄버거 버튼이 왼쪽에 위치
- ⚠ Scaffold가 Drawer를 제어해야하므로 appbar 안에 만들 수 없음
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: Container(width: 200, color: Colors.blue),
appBar: AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
//iconTheme: IconThemeData(color: Colors.blue),
),
body: Placeholder(),
);
}
}


- 아이콘 색상 바꾸기 → blue
- 아이콘 색상은 iconThem으로 바꾼다
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: Container(width: 200, color: Colors.blue),
appBar: AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
),
body: Placeholder(),
);
}
}

endDrawer를 component로 만들기


profile_drawer
import 'package:flutter/material.dart';
class ProfileDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(width: 200, color: Colors.blue);
}
}
main
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
),
body: Placeholder(),
);
}
}
Appbar 함수로 추출


import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Placeholder(),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}
Profile 영역 만들기
1. ProfileHeader
Row는 block
crossAxisAlignment: CrossAxisAlignment.start로 왼쪽 정렬
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
Row(
children: [
CircleAvatar(),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Getinthere"),
Text("Programmer/Writer/Teacher"),
Text("There Programming"),
],
),
],
),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}

ProfileHeader를 component로 만들기

profile_header
import 'package:flutter/material.dart';
class ProfileHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
CircleAvatar(),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Getinthere"),
Text("Programmer/Writer/Teacher"),
Text("There Programming"),
],
),
],
);
}
}
main
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
import 'package:flutter_profile2/component/profile_header.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
ProfileHeader(),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}
2. ProfileCountInfo 만들기
column으로 만든 후 내부를 mainAxisAlignment: MainAxisAlignment.spaceAround로 정렬
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
import 'package:flutter_profile2/component/profile_header.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
ProfileHeader(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(children: [Text("50"), Text("Posts")]),
Container(width: 2, height: 60, color: Colors.grey),
Column(children: [Text("10"), Text("Likes")]),
Container(width: 2, height: 60, color: Colors.grey),
Column(children: [Text("3"), Text("Share")]),
],
),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}

ProfileCountInfo를 component로 만들기

profile_count_info
import 'package:flutter/material.dart';
class ProfileCountInfo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(children: [Text("50"), Text("Posts")]),
Container(width: 2, height: 60, color: Colors.grey),
Column(children: [Text("10"), Text("Likes")]),
Container(width: 2, height: 60, color: Colors.grey),
Column(children: [Text("3"), Text("Share")]),
],
);
}
}
main
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/profile_count_info.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
import 'package:flutter_profile2/component/profile_header.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
ProfileHeader(),
ProfileCountInfo(),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}
3. ProfileButtons 만들기
< 버튼 종류 >
- ElevatedButton
- 특징: 입체적인 느낌의 **음영(shadow)**이 있는 버튼
- 용도: 사용자 주의를 끌어야 할 주요 액션에 사용
- 배경색 있음, 기본적으로 Material 디자인 그림자 포함
- TextButton
- 특징: 투명한 배경 + 텍스트만 있는 평면 버튼
- 용도: 보조 액션에 사용 (예: 취소, 더보기, 간단한 링크 등)
- 그림자, 테두리 없음. 터치 시만 살짝 효과
- OutlineButton
- 특징: 테두리가 있는 버튼 (배경 없음)
- 용도: 중간 강조 정도. 보조이지만 텍스트보단 강조하고 싶을 때
- 경계선(Border)이 있고, 클릭 시 반응 효과
- 모두
onPressed
를null
로 하면 자동으로 비활성화됨 (회색 처리) - 공통적으로
style: ButtonStyle(...)
로 커스터마이징 가능
버튼 종류 | 배경 | 테두리 | 그림자 | 사용 목적 |
ElevatedButton | O | X | O | 주요 동작 |
TextButton | X | X | X | 부가/링크 스타일 |
OutlinedButton | X | O | X | 보조 강조 동작 |
💡 참고 팁:
ElevatedButton - Follow
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/profile_count_info.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
import 'package:flutter_profile2/component/profile_header.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
ProfileHeader(),
ProfileCountInfo(),
Row(
children: [
ElevatedButton(
onPressed: () {},
child: Text("Follow"),
),
],
),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}

Container로 Button 만들기 - Message
- Text를 Align 위젯으로 감싼다
- Container를 InkWell 위젯으로 만든다
- InkWell 안에 onTap: () {}를 넣어서 Button으로 만들어준다
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/profile_count_info.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
import 'package:flutter_profile2/component/profile_header.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
ProfileHeader(),
ProfileCountInfo(),
Row(
children: [
ElevatedButton(
onPressed: () {},
child: Text("Follow"),
),
InkWell(
child: Container(
width: 150,
height: 45,
child: Align(child: Text("Message")),
),
),
],
),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}
Follow Button과 Message Button을 component로 만들기
ElevatedButton → FollowButton / InkWell → MessageButton
follow_button


import 'package:flutter/material.dart';
class FollowButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {},
child: Text("Follow"),
);
}
}
message_button


import 'package:flutter/material.dart';
class MessageButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {},
child: Container(
width: 150,
height: 45,
child: Align(child: Text("Message")),
),
);
}
}
main
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/follow_button.dart';
import 'package:flutter_profile2/component/message_button.dart';
import 'package:flutter_profile2/component/profile_count_info.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
import 'package:flutter_profile2/component/profile_header.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
ProfileHeader(),
ProfileCountInfo(),
Row(
children: [
FollowButton(),
MessageButton(),
],
),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}
TabBar
Sampling 했던 코드 들고오기
class TabBarExample extends StatelessWidget {
const TabBarExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TabBar Sample'),
),
body: DefaultTabController(
initialIndex: 0,
length: 2,
child: Column(
children: [
Container(
height: 300,
color: Colors.yellow,
),
DefaultTabController(
length: 2,
child: Expanded(
child: Column(
children: [
TabBar(
tabs: <Widget>[
Tab(icon: Icon(Icons.cloud_outlined)),
Tab(icon: Icon(Icons.beach_access_sharp)),
], // length의 길이만큼 아이콘을 가진다
),
Expanded(
child: TabBarView(
children: <Widget>[
Center(child: Text("It's cloudy here")),
Center(child: Text("It's rainy here")),
], // Widget안의 Tab 개수만큼 자식을 가진다
),
),
],
),
),
),
],
),
),
);
}
}
Sampling한 코드를 component 만들기
proflie_tab
import 'package:flutter/material.dart';
/**
* Created By JOOHO, 2025.05.27
* email : getinthere@naver.com
* tip: 탭바와 탭바뷰는 높이 지정이 되어있지 않아서, 사용하는 곳에서 높이 지정이 필요함
*/
class ProfileTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
initialIndex: 0,
length: 2,
child: Column(
children: [
DefaultTabController(
length: 2,
child: Expanded(
child: Column(
children: [
TabBar(
tabs: <Widget>[
Tab(icon: Icon(Icons.cloud_outlined)),
Tab(icon: Icon(Icons.beach_access_sharp)),
], // length의 길이만큼 아이콘을 가진다
),
Expanded(
child: TabBarView(
children: <Widget>[
Center(child: Text("It's cloudy here")),
Center(child: Text("It's rainy here")),
], // Widget안의 Tab 개수만큼 자식을 가진다
),
),
],
),
),
),
],
),
),
);
}
}
main
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/follow_button.dart';
import 'package:flutter_profile2/component/message_button.dart';
import 'package:flutter_profile2/component/profile_count_info.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
import 'package:flutter_profile2/component/profile_header.dart';
import 'package:flutter_profile2/component/profile_tab.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
ProfileHeader(),
ProfileCountInfo(),
Row(
children: [
FollowButton(),
MessageButton(),
],
),
Expanded(child: ProfileTab()),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}
GridView
GridView란
GridView
는 Flutter에서 격자 형태(그리드 레이아웃)로 위젯들을 나열할 수 있는 위젯입니다. 가로 방향 정렬이 있는 리스트라고 보면 됩니다.✅ 대표적인 2가지 방식
1. GridView.count
- 열(column) 개수를 지정해서 사용하는 방식
dart
코드 복사
GridView.count(
crossAxisCount: 2, // 열 개수
crossAxisSpacing: 10, // 열 사이 간격
mainAxisSpacing: 10, // 행 사이 간격
children: List.generate(6, (index) {
return Container(
color: Colors.blue,
child: Center(child: Text('Item $index')),
);
}),
)
2. GridView.builder
- 데이터 개수에 따라 동적으로 생성할 때 사용 (더 일반적)
dart
코드 복사
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: 20,
itemBuilder: (context, index) {
return Container(
color: Colors.green,
child: Center(child: Text("Item $index")),
);
},
)
📌 주요 속성 정리
속성 이름 | 설명 |
crossAxisCount | 열 개수 설정 |
mainAxisSpacing | 행 간격 설정 |
crossAxisSpacing | 열 간격 설정 |
childAspectRatio | 각 셀의 가로/세로 비율 (예: 1.0 = 정사각형) |
shrinkWrap: true | 내부 크기만큼만 차지 (스크롤뷰 안에 넣을 때 사용) |
physics: NeverScrollableScrollPhysics() | 스크롤 막기 |
💡 사용 예
- 갤러리 이미지 목록
- 상품 카드 목록
- 이모지/아이콘 선택 창 등
Builder
- builder는 집합을 수학적으로 표현
- ListView 안에 Text를 builder(수식)으로 표현 → ListView 안에 자식들이 많을 때 수학적 표현 사용해서 한줄로 만든다
- 내부적으로 viewholder 패턴을 쓴다
Flutter에서의 Builder 패턴은 일반적인 GoF 디자인 패턴의 "Builder"보다는 UI 위젯을 동적으로 생성하기 위한 구조로 사용되는 경우가 많습니다. 즉, "Builder 패턴"이라는 말은 Flutter에서는 주로 콜백 함수 형태로 위젯을 반환하는 구조를 의미합니다.
✅ Flutter에서 흔히 보는 Builder
패턴의 예시들
1. Builder
위젯 자체
- BuildContext를 새로 얻기 위해 사용
dart
코드 복사
Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Snackbar!")),
);
},
child: Text("Show Snackbar"),
);
},
)
Scaffold
하위에서 context가 없을 때 유용
2. ListView.builder
- 동적으로 UI 생성할 때 가장 자주 사용
dart
코드 복사
ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
)
3. GridView.builder
- 위에서 설명한
GridView
처럼, 대량의 아이템을 효율적으로 그리기 위한 Builder 구조
✅ 요약: Flutter에서 Builder란?
Flutter 용도 | 의미 |
Builder 위젯 | 새로운 BuildContext 를 생성하기 위한 도우미 위젯 |
ListView.builder | 리스트를 효율적으로 렌더링하기 위한 동적 생성 방식 |
FutureBuilder , StreamBuilder | 비동기 작업을 감지하고 UI를 빌드하는 위젯 |
✅ 다른 예: FutureBuilder
dart
코드 복사
FutureBuilder<String>(
future: getData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
} else {
return Text("Result: ${snapshot.data}");
}
},
)
📌 정리하면
Flutter의 Builder 패턴은 "복잡한 객체 생성"보다는 UI를 조건에 따라 효율적으로 그릴 수 있도록 해주는 구조입니다.
필요하면 GoF 스타일 Builder 패턴을 Flutter 코드에 적용하는 법도 예제로 보여줄 수 있어.
Flutter에서
builder
는 단순히 UI를 만들기 위한 함수형 스타일 콜백으로 보이지만, 수학적 집합 관점에서 바라보면 "정의된 규칙에 따라 원소들을 생성하는 함수" 역할을 합니다.✅ 수학적 집합과 Builder의 연결 고리
▶ 수학적 집합에서의 정의
수학에서 집합은 보통 다음 두 가지 방식 중 하나로 정의됩니다:
- 명시적 표기 (Roster form)
{1, 2, 3, 4}
처럼 원소를 직접 나열- 기호적 정의 (Set-builder form)
{ x ∈ ℕ | x < 5 }
→ 규칙(조건)을 따라 원소를 정의▶ Flutter에서의 Builder는?
Flutter의
ListView.builder
, GridView.builder
등은 바로 Set-builder 방식을 닮았습니다.dart
코드 복사
ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return Text("Item $index");
},
)
이는 수학적으로 다음과 같이 생각할 수 있어요:
ini
코드 복사
S = { widget(i) | i ∈ ℕ, 0 ≤ i < 10 }
즉,
itemBuilder
는 어떤 **규칙 함수widget(i)
*이고,
itemCount
는 생성할 원소의 범위 조건입니다.
ListView.builder
는 이 규칙에 따라 원소(위젯)를 동적으로 생성해서 리스트를 구성합니다.
✅ 다시 말해
수학적 구성 요소 | Flutter builder에서의 대응 요소 |
원소 (element) | 위젯 (Widget) |
생성 규칙 (rule) | builder 함수 ( itemBuilder ) |
범위 조건 | itemCount 등 (조건 제한) |
집합 (Set) | 위젯 리스트 (List of Widgets) |
✅ 왜 중요한가?
이 개념을 이해하면 다음과 같은 이점이 있어요:
- 불필요한 위젯 생성 방지 (렌더링 성능 최적화)
- 동일한 규칙에 따른 반복 UI 작성이 직관적으로 가능
- 조건 기반 위젯 생성이 수학적 사고와 연계돼 깔끔하게 구현됨
✅ 비슷한 예시: 수학적 집합의 정의와 코드 비교
수학 표현 | Flutter 표현 |
`{ x ∈ ℕ | x < 5 }` |
`{ f(i) | i ∈ ℕ, 0 ≤ i < n }` |
📌 결론:
Flutter의
builder
는 단순한 UI 도구가 아니라 조건(rule)에 따라 무한히 확장 가능한 집합 생성 도구로 볼 수 있습니다.이걸 이해하면 Flutter UI 코드를 수학적 사고로 더 논리적이고 효율적으로 설계할 수 있어요.
Builder 예시
import 'package:flutter/material.dart';
import 'package:flutter_profile/component/detail_tab.dart';
import 'package:flutter_profile/component/m_button.dart';
import 'package:flutter_profile/component/m_info.dart';
import 'package:flutter_profile/component/m_navigator.dart';
import 'component/m_header.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: MNavigator(),
appBar: _appBar(),
body: Column(
children: [
SizedBox(width: 16),
MHeader(),
MInfo(),
MButton(),
SizedBox(height: 500,
child: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => Text("안녕"))),
Expanded(child: DetailTab()),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(
onPressed: () {
print("appbar leading button");
},
icon: Icon(Icons.arrow_back_ios),
),
title: Text("Profile"),
centerTitle: true,
);
}
}
Builder와 Container 차이
컨테이너는 viewholder 패턴 안씀
for문은 리턴 안됨, 출력만 ~ → statement
for문 중괄호 안쓰면 컨테이너 전달가능 → expression
컨테이너를 사용할거면 for문으로 뿌림 → 근데 잘 안쓴다
좋은 질문입니다.
Builder
와 Container
는 Flutter에서 전혀 다른 역할을 하는 위젯입니다. 헷갈릴 수 있지만, 목적도, 쓰임도 완전히 다릅니다.✅ 핵심 차이 요약
구분 | Builder | Container |
정의 | UI를 동적으로 생성하기 위한 콜백 제공 위젯 | 스타일을 적용하는 레이아웃 박스 위젯 |
주요 역할 | BuildContext 가 필요한 위젯 빌드 | 패딩, 마진, 색상, 크기, 정렬 등 적용 |
자식 생성 | builder: (context) => 위젯 형태로 동작 | child: 속성에 위젯 전달 |
렌더링 | 자기 자신은 UI에 보이지 않음 | 화면에 보이는 요소 (스타일 포함) |
대표 사용처 | Scaffold 하위 context 필요할 때, 동적 UI 만들 때 | 색상 배경, 정렬, 사이즈, 박스 레이아웃 |
✅ 예시로 보는 차이점
1. Builder
dart
코드 복사
Builder(
builder: (context) {
// 여기 context는 새로운 BuildContext
return Text("Hello Builder");
},
)
📌 의도: BuildContext가 필요한 작업 (ex: Scaffold.of(context) 등)
2. Container
dart
코드 복사
Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text("Hello Container")),
)
📌 의도: 특정 크기, 색상, 정렬 등을 가진 박스 UI
✅ 비유로 설명하면
Builder
는 설계자. “이 조건일 땐 이런 UI를 만들자!”고 말해줌.
Container
는 건축물. 만들어진 UI 요소에 꾸밈(색상, 크기, 정렬 등)을 적용함.
✅ 함께 사용하는 경우
두 개를 같이 쓸 수도 있습니다:
dart
코드 복사
Builder(
builder: (context) {
return Container(
padding: EdgeInsets.all(20),
color: Colors.green,
child: Text("Hello"),
);
},
)
✅ 요약
질문 | 답변 |
UI 그리려면 어떤 걸 써야 하나? | Container |
context가 필요할 때는? | Builder |
눈에 보이는가? | Builder ❌ / Container ✅ |
문서

예제코드
GridView.count(
primary: false,
padding: const EdgeInsets.all(20),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 2,
children: <Widget>[
Container(
padding: const EdgeInsets.all(8),
color: Colors.teal[100],
child: const Text("He'd have you all unravel at the"),
),
Container(
padding: const EdgeInsets.all(8),
color: Colors.teal[200],
child: const Text('Heed not the rabble'),
),
Container(
padding: const EdgeInsets.all(8),
color: Colors.teal[300],
child: const Text('Sound of screams but the'),
),
Container(
padding: const EdgeInsets.all(8),
color: Colors.teal[400],
child: const Text('Who scream'),
),
Container(
padding: const EdgeInsets.all(8),
color: Colors.teal[500],
child: const Text('Revolution is coming...'),
),
Container(
padding: const EdgeInsets.all(8),
color: Colors.teal[600],
child: const Text('Revolution, they...'),
),
],
)
Builder 사용해서 GridView 만들기
gridDelegate, itemBuilder, SliverGridDelegateWithFixedCrossAxisCount
✅ 1.
itemBuilder
- 각 **그리드 셀(아이템)**을 어떻게 만들지를 정의하는 콜백 함수
- 리스트처럼 인덱스를 기반으로 UI를 반복 생성
dart
코드 복사
itemBuilder: (context, index) {
return Text("Item $index");
}
📌 역할 요약:
index를 받아서 해당 index의 위젯을 생성
✅ 2.
gridDelegate
GridView
의 **격자 구조(레이아웃 방식)**을 설정하는 속성
- 열 개수, 간격, 비율 등 그리드의 형태를 결정함
dart
코드 복사
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1.0,
)
📌 역할 요약:
그리드의 열 수, 간격, 아이템 비율 등을 설정하는 레이아웃 규칙
✅ 3.
SliverGridDelegateWithFixedCrossAxisCount
gridDelegate
에 전달되는 구체적인 레이아웃 전략 클래스 중 하나
crossAxisCount
를 기준으로 고정된 열 개수로 그리드를 설정
dart
코드 복사
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 열 개수 (예: 2열)
mainAxisSpacing: 10.0, // 행 사이 간격
crossAxisSpacing: 10.0, // 열 사이 간격
childAspectRatio: 1.0, // 가로:세로 비율
)
📌 다른 gridDelegate도 있음:
SliverGridDelegateWithMaxCrossAxisExtent
: 최대 너비 기준으로 그리드 구성
✅ 요약표
요소 | 설명 |
itemBuilder | 각 셀을 어떤 UI로 구성할지 정의 (index 기반)
for문 돌리면서 뭐를 뿌릴건지 수식화 |
gridDelegate | 그리드 레이아웃 규칙 전체를 설정하는 속성
만드는 방식이 많아서 책임분리 후 위임 |
SliverGridDelegateWith... | 실제 레이아웃 전략 클래스 (고정열/최대너비 방식 등)
배치 전략 |
proflie_tab
- SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3) → 사진이 3개씩 나열
- itemCount: 42 → 사진이 GridView에 보여질 갯수
- itemBuilder: (context, index) ⇒ Image.network("https://picsum.photos/id/${index + 30}/200/200") → ID가 1부터 72까지의 사진을 가져오고 높이와 넓이를 각각 200으로 지정
- initialIndex: 1이면 재실행 했을 때 아무것도 안나오고 해당 탭을 눌러야 통신되면서 그림이 그려진다
import 'package:flutter/material.dart';
class ProfileTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
initialIndex: 0,
length: 2,
child: Column(
children: [
DefaultTabController(
length: 2,
child: Expanded(
child: Column(
children: [
TabBar(
tabs: <Widget>[
Tab(icon: Icon(Icons.cloud_outlined)),
Tab(icon: Icon(Icons.beach_access_sharp)),
], // length의 길이만큼 아이콘을 가진다
),
Expanded(
child: TabBarView(
children: <Widget>[
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemBuilder: (context, index) =>
Image.network("https://picsum.photos/id/${index + 30}/200/200"),
itemCount: 42,
),
Center(child: Text("It's rainy here")),
], // Widget안의 Tab 개수만큼 자식을 가진다
),
),
],
),
),
),
],
),
),
);
}
}

Design
ProfileHeader Design
1. 프로필 사진 넣기
CircleAvatar를 SizedBox로 감싸서 사진의 넓이와 높이 지정
import 'package:flutter/material.dart';
class ProfileHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(
width: 80,
height: 80,
child: CircleAvatar(
backgroundImage: AssetImage("assets/person.png"),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Getinthere"),
Text("Programmer/Writer/Teacher"),
Text("There Programming"),
],
),
],
);
}
}

2. 간격 주기
import 'package:flutter/material.dart';
class ProfileHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(
width: 80,
height: 80,
child: CircleAvatar(
backgroundImage: AssetImage("assets/person.png"),
),
),
SizedBox(width: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Getinthere"),
Text("Programmer/Writer/Teacher"),
Text("There Programming"),
],
),
],
);
}
}

3. 폰트 크기 + 폰트 굵기 변경
import 'package:flutter/material.dart';
class ProfileHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(
width: 80,
height: 80,
child: CircleAvatar(
backgroundImage: AssetImage("assets/person.png"),
),
),
SizedBox(width: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Getinthere", style: TextStyle(fontSize: 25, fontWeight: FontWeight.w700)),
Text("Programmer/Writer/Teacher", style: TextStyle(fontSize: 20)),
Text("There Programming", style: TextStyle(fontSize: 15)),
],
),
],
);
}
}

SizedBox 사용해서 위젯 사이사이 Gap주기
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/follow_button.dart';
import 'package:flutter_profile2/component/message_button.dart';
import 'package:flutter_profile2/component/profile_count_info.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
import 'package:flutter_profile2/component/profile_header.dart';
import 'package:flutter_profile2/component/profile_tab.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
ProfileHeader(),
SizedBox(height: 20),
ProfileCountInfo(),
SizedBox(height: 20),
Row(
children: [
FollowButton(),
MessageButton(),
],
),
SizedBox(height: 20),
Expanded(child: ProfileTab()),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}

GridView 영역 사진 Gap주기
import 'package:flutter/material.dart';
class ProfileTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
initialIndex: 0,
length: 2,
child: Column(
children: [
DefaultTabController(
length: 2,
child: Expanded(
child: Column(
children: [
TabBar(
tabs: <Widget>[
Tab(icon: Icon(Icons.cloud_outlined)),
Tab(icon: Icon(Icons.beach_access_sharp)),
], // length의 길이만큼 아이콘을 가진다
),
Expanded(
child: TabBarView(
children: <Widget>[
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
),
itemBuilder: (context, index) =>
Image.network("https://picsum.photos/id/${index + 30}/200/200"),
itemCount: 42,
),
Center(child: Text("It's rainy here")),
], // Widget안의 Tab 개수만큼 자식을 가진다
),
),
],
),
),
),
],
),
),
);
}
}

FollowButton & MessageButton Design
1. FollowButton
import 'package:flutter/material.dart';
class FollowButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
fixedSize: Size(150, 45),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () {},
child: Text("Follow", style: TextStyle(color: Colors.white)),
);
}
}
2. MessageButton
import 'package:flutter/material.dart';
class MessageButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {},
child: Container(
width: 150,
height: 45,
child: Align(child: Text("Message")),
decoration: BoxDecoration(border: Border.all(), borderRadius: BorderRadius.circular(10)),
),
);
}
}
3. main
FollowButton과 MessageButton을 spaceAround로 정렬
import 'package:flutter/material.dart';
import 'package:flutter_profile2/component/follow_button.dart';
import 'package:flutter_profile2/component/message_button.dart';
import 'package:flutter_profile2/component/profile_count_info.dart';
import 'package:flutter_profile2/component/profile_drawer.dart';
import 'package:flutter_profile2/component/profile_header.dart';
import 'package:flutter_profile2/component/profile_tab.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: ProfileDrawer(),
appBar: _appBar(),
body: Column(
children: [
ProfileHeader(),
SizedBox(height: 20),
ProfileCountInfo(),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
FollowButton(),
MessageButton(),
],
),
SizedBox(height: 20),
Expanded(child: ProfileTab()),
],
),
);
}
AppBar _appBar() {
return AppBar(
leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_ios)),
title: Text("Profile"),
centerTitle: true,
iconTheme: IconThemeData(color: Colors.blue),
);
}
}

TabBar Icon 변경
import 'package:flutter/material.dart';
class ProfileTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
initialIndex: 0,
length: 2,
child: Column(
children: [
DefaultTabController(
length: 2,
child: Expanded(
child: Column(
children: [
TabBar(
tabs: <Widget>[
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
], // length의 길이만큼 아이콘을 가진다
),
Expanded(
child: TabBarView(
children: <Widget>[
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
),
itemBuilder: (context, index) =>
Image.network("https://picsum.photos/id/${index + 30}/200/200"),
itemCount: 42,
),
Center(child: Text("It's rainy here")),
], // Widget안의 Tab 개수만큼 자식을 가진다
),
),
],
),
),
),
],
),
),
);
}
}

완성

Share article