Chapter 06. Flutter_profile

김미숙's avatar
Jun 10, 2025
Chapter 06. Flutter_profile

출처

만들면서 배우는 플러터 앱 프로그래밍 7가지 모바일 앱 UI 제작 & RiverPod 상태 관리
 

Preview

  • ThemeData Class
  • TabBar 위젯
  • TabBarView 위젯
  • AppBar 위젯
  • InkWell 위젯
  • GridView 위젯
  • Drawer 위젯
  • Align 위젯
  • Image 위젯으로 network 이미지를 다운 받아서 화면에 표시하는 방법

화면 구조

notion image
 

TabBar Sampling

0. Sample Code

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 const MaterialApp( debugShowCheckedModeBanner: false, home: TabBarExample(), ); } } class TabBarExample extends StatelessWidget { const TabBarExample({super.key}); @override Widget build(BuildContext context) { return DefaultTabController( initialIndex: 1, 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")), ], ), ), ); } }
notion image

1. Tab 두개만 두기

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 const MaterialApp( debugShowCheckedModeBanner: false, home: 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.cloud_outlined)), Tab(icon: Icon(Icons.beach_access_sharp)), ], ), ), body: const TabBarView( children: <Widget>[ Center(child: Text("It's cloudy here")), Center(child: Text("It's rainy here")), ], ), ), ); } }
notion image

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 const MaterialApp( debugShowCheckedModeBanner: false, home: TabBarExample(), ); } } class TabBarExample extends StatelessWidget { const TabBarExample({super.key}); @override Widget build(BuildContext context) { return DefaultTabController( initialIndex: 0, length: 2, 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)), ], ), ), body: const TabBarView( children: <Widget>[ Center(child: Text("It's cloudy here")), Center(child: Text("It's rainy here")), ], ), ), ); } }
notion image

3. body 영역의 위젯

첫 번째 TabBarView를 Text로 바꿔보기
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 const MaterialApp( debugShowCheckedModeBanner: false, home: TabBarExample(), ); } } class TabBarExample extends StatelessWidget { const TabBarExample({super.key}); @override Widget build(BuildContext context) { return DefaultTabController( initialIndex: 0, // 최초 선택할 번지수 length: 2, // TabBar 아이콘의 개수 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)), ], // length의 길이만큼 아이콘을 가진다 ), ), body: const TabBarView( children: <Widget>[ Text("It's cloudy here"), Center(child: Text("It's rainy here")), ], // Widget안의 Tab 개수만큼 자식을 가진다 ), ), ); } }
notion image
일반 Text 타입이면 body 영역의 상단 맨 위 왼쪽으로 위치함
Center 는 body 영역의 중간에 위치

4. ProfileApp처럼 TabBar를 밑으로 내려보기

  1. TabBar 밑에 영역 잡기
    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 const MaterialApp( debugShowCheckedModeBanner: false, home: TabBarExample(), ); } } class TabBarExample extends StatelessWidget { const TabBarExample({super.key}); @override Widget build(BuildContext context) { return DefaultTabController( initialIndex: 0, // 최초 선택할 번지수 length: 2, // TabBar 아이콘의 개수 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)), ], // length의 길이만큼 아이콘을 가진다 ), ), body: Column( children: [ Container( height: 300, color: Colors.yellow, ), TabBarView( children: <Widget>[ Center(child: Text("It's cloudy here")), Center(child: Text("It's rainy here")), ], // Widget안의 Tab 개수만큼 자식을 가진다 ), ], ), ), ); } }
      notion image
  1. Column을 ListView로 바꿔보기
    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 const MaterialApp( debugShowCheckedModeBanner: false, home: TabBarExample(), ); } } class TabBarExample extends StatelessWidget { const TabBarExample({super.key}); @override Widget build(BuildContext context) { return DefaultTabController( initialIndex: 0, // 최초 선택할 번지수 length: 2, // TabBar 아이콘의 개수 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)), ], // length의 길이만큼 아이콘을 가진다 ), ), body: ListView( children: [ Container( height: 300, color: Colors.yellow, ), Expanded( child: TabBarView( children: <Widget>[ Center(child: Text("It's cloudy here")), Center(child: Text("It's rainy here")), ], // Widget안의 Tab 개수만큼 자식을 가진다 ), ), ], ), ), ); } }
      notion image
      notion image
      오류 해석

      🧾 오류 메시지 해석:

      Horizontal viewport was given unbounded height.
      👉 수평 뷰포트(Viewport)에 무제한 높이가 주어졌습니다.
      Viewports expand in the cross axis to fill their container and constrain their children to match their extent in the cross axis.
      👉 뷰포트는 교차 축(cross axis) 방향으로 컨테이너를 채우기 위해 확장되고, 자식 위젯들을 그 축의 범위 안에 맞추도록 제한합니다.
      In this case, a horizontal viewport was given an unlimited amount of vertical space in which to expand.
      👉 이 경우, 수평 뷰포트가 확장할 수 있도록 무제한(vertical) 세로 공간이 주어졌습니다.

      📌 간단한 설명:

      Flutter에서 ListViewSingleChildScrollView 같은 스크롤 가능한 위젯은 내부적으로 Viewport를 사용합니다.
      • scrollDirection: Axis.horizontal이면 주축은 가로, 교차 축은 세로입니다.
      • 그런데 교차 축(세로 방향)의 크기제한되지 않으면(=unbounded), Flutter는 어느 정도 높이로 위젯을 그려야 할지 몰라서 오류를 발생시킵니다.

      📉 예시 (잘못된 코드):

      dart 코드 복사 SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ Container(width: 100, color: Colors.red), Container(width: 100, color: Colors.green), ], ), )
      ➡️ 이 경우, SingleChildScrollView는 위에서 높이를 전달받지 못해서, 자식이 무한히 커질 수 있다고 판단해 오류가 납니다.

      ✅ 해결 방법 (예시 코드):

      🔧 해결법 1: SizedBox 또는 Container높이 제한

      dart 코드 복사 SizedBox( height: 100, // 높이 명시 child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ Container(width: 100, color: Colors.red), Container(width: 100, color: Colors.green), ], ), ), )

      💬 요약

      구분
      설명
      오류 원인
      수평 스크롤 위젯이 높이 제약 없이 배치되었음
      해결책
      SizedBoxContainer 등으로 명시적인 height를 줘야 함
      핵심 포인트
      수평 스크롤 뷰는 세로 방향 크기 제한이 반드시 필요함
      ListView는 무한대로 늘어나나, 내부에 아이템이 들어가면 높이가 결정된다 지금 터진 이유는 ListView가 무한대인데 Expanded 때문에 터짐 (남는 공간을 차지하려하는데 이미 ListView가 무한대여서 차지할 수 있는 공간이 없다)
      → 이 경우는 높이를 제한해서 해결
      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 const MaterialApp( debugShowCheckedModeBanner: false, home: TabBarExample(), ); } } class TabBarExample extends StatelessWidget { const TabBarExample({super.key}); @override Widget build(BuildContext context) { return DefaultTabController( initialIndex: 0, // 최초 선택할 번지수 length: 2, // TabBar 아이콘의 개수 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)), ], // length의 길이만큼 아이콘을 가진다 ), ), body: ListView( children: [ Container( height: 300, color: Colors.yellow, ), SizedBox( height: 500, child: TabBarView( children: <Widget>[ Center(child: Text("It's cloudy here")), Center(child: Text("It's rainy here")), ], // Widget안의 Tab 개수만큼 자식을 가진다 ), ), ], ), ), ); } }
      notion image
  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 const MaterialApp( debugShowCheckedModeBanner: false, home: TabBarExample(), ); } } class TabBarExample extends StatelessWidget { const TabBarExample({super.key}); @override Widget build(BuildContext context) { return DefaultTabController( initialIndex: 0, // 최초 선택할 번지수 length: 2, // TabBar 아이콘의 개수 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)), ], // length의 길이만큼 아이콘을 가진다 ), ), body: ListView( children: [ Container( height: 300, color: Colors.yellow, ), SizedBox( height: 900, child: TabBarView( children: <Widget>[ Center(child: Text("It's cloudy here")), Center(child: Text("It's rainy here")), ], // Widget안의 Tab 개수만큼 자식을 가진다 ), ), ], ), ), ); } }
      스크롤 생김
      스크롤 생김
      데이터의 개수만큼 높이가 늘어나야하므로 높이는 동적으로 변해야함

Sampling 완료

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 const MaterialApp( debugShowCheckedModeBanner: false, home: TabBarExample(), ); } } 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 개수만큼 자식을 가진다 ), ), ], ), ), ), ], ), ), ); } }
 
 
 
 
 
 
 
 

Setting

0. pubspec.yaml - assets 경로 설정

assets: - assets/

1. 프로젝트에 assets 폴더 생성

notion image

2. assets 폴더에 사진 넣기

이미지 url: person.png
notion image
notion image
 

기본 코드

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(), ); } }
notion image

Appbar에 뒤로 가기(ios), title, 햄버거 버튼 만들기 + 아이콘 색상 바꾸기

  1. 뒤로가기 아이콘 (ios)
    1. 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(), ); } }
      notion image
  1. title
    1. 안드로이드는 title이 왼쪽, IOS가운데에 위치한다
    2. 안드로이드 title이 가운데에 위치하게 하려면 centertitle = true
    3. 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(), ); } }
      notion image
  1. 햄버거 버튼
    1. endDrawer → 햄버거 버튼이 오른쪽에 위치
    2. drawer → 햄버거 버튼이 왼쪽에 위치
    3. Scaffold가 Drawer를 제어해야하므로 appbar 안에 만들 수 없음
    4. 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(), ); } }
      notion image
      notion image
  1. 아이콘 색상 바꾸기 → blue
    1. 아이콘 색상은 iconThem으로 바꾼다
    2. 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(), ); } }
      notion image

endDrawer를 component로 만들기

notion image
notion image
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 함수로 추출

notion image
notion image
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), ); } }
notion image
ProfileHeader를 component로 만들기
notion image
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), ); } }
notion image
ProfileCountInfo를 component로 만들기
notion image
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)이 있고, 클릭 시 반응 효과
    • 버튼 종류
      배경
      테두리
      그림자
      사용 목적
      ElevatedButton
      O
      X
      O
      주요 동작
      TextButton
      X
      X
      X
      부가/링크 스타일
      OutlinedButton
      X
      O
      X
      보조 강조 동작
      💡 참고 팁:
    • 모두 onPressednull로 하면 자동으로 비활성화됨 (회색 처리)
    • 공통적으로 style: ButtonStyle(...)로 커스터마이징 가능
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), ); } }
notion image
Container로 Button 만들기 - Message
  1. Text를 Align 위젯으로 감싼다
  1. Container를 InkWell 위젯으로 만든다
  1. 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
notion image
notion image
import 'package:flutter/material.dart'; class FollowButton extends StatelessWidget { @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () {}, child: Text("Follow"), ); } }
message_button
notion image
notion image
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의 연결 고리

▶ 수학적 집합에서의 정의

수학에서 집합은 보통 다음 두 가지 방식 중 하나로 정의됩니다:
  1. 명시적 표기 (Roster form)
    1. {1, 2, 3, 4} 처럼 원소를 직접 나열
  1. 기호적 정의 (Set-builder form)
    1. { 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문으로 뿌림 → 근데 잘 안쓴다

좋은 질문입니다. BuilderContainer는 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
 

문서

notion image

예제코드

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 개수만큼 자식을 가진다 ), ), ], ), ), ), ], ), ), ); } }
notion image
 

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"), ], ), ], ); } }
notion image

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"), ], ), ], ); } }
notion image

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)), ], ), ], ); } }
notion image
 

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), ); } }
notion image

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 개수만큼 자식을 가진다 ), ), ], ), ), ), ], ), ), ); } }
notion image
 

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), ); } }
notion image

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 개수만큼 자식을 가진다 ), ), ], ), ), ), ], ), ), ); } }
notion image
 

완성

notion image
Share article

parangdajavous