디펜던시 추가
google_fonts: ^6.1.0
font_awesome_flutter: ^10.5.0
intl: ^0.18.1
intl
→ 숫자 포맷 라이브러리
폴더 구조


위젯
BottomNavigationBar
- 하단에 주요 화면 간 이동을 위한 탭 네비게이션 바를 제공하는 위젯
기본 정렬
mainAxisAlignment
: 없음 (자동 정렬)
crossAxisAlignment
: 없음 (항목 간 동일 간격)
레이아웃
- 가로 → 부모(Scaffold 등)의 전체 너비 사용
- 세로 → 항목 수 및 스타일에 따라 높이 자동 결정 (보통 약 56~80px)
속성
items
→List<BottomNavigationBarItem>
| 각 탭 항목 정의 (아이콘과 라벨 포함)
currentIndex
→int
| 현재 선택된 항목 인덱스
onTap
→void Function(int)?
| 항목을 탭했을 때 호출되는 콜백
type
→BottomNavigationBarType
| 탭 스타일(fixed
,shifting
)
selectedItemColor
→Color?
| 선택된 항목의 색상
unselectedItemColor
→Color?
| 선택되지 않은 항목의 색상
backgroundColor
→Color?
| 바의 배경색
iconSize
→double
| 아이콘 크기 (기본값: 24.0)
selectedLabelStyle
→TextStyle?
| 선택된 항목 라벨의 스타일
unselectedLabelStyle
→TextStyle?
| 선택되지 않은 항목 라벨의 스타일
showSelectedLabels
→bool
| 선택된 항목 라벨 표시 여부 (기본값: true)
showUnselectedLabels
→bool
| 선택되지 않은 항목 라벨 표시 여부 (기본값: false)
elevation
→double
| 바의 그림자 깊이 (기본값: 8.0)


BottomNavigationBarItem
- BottomNavigationBar에서 사용할 개별 탭 항목을 정의하는 위젯
기본 정렬
- 자체 정렬 개념 없음 (BottomNavigationBar에서 위치 결정)
- 아이콘과 라벨은 세로로 정렬됨 (Column 구조)
레이아웃
- 가로 → BottomNavigationBar에서 균등 분배
- 세로 → 아이콘과 라벨 크기에 따라 자동 결정
속성
icon
→Widget
| 기본 상태에서 표시할 아이콘
label
→String
| 아이콘 아래에 표시되는 텍스트 라벨
tooltip
→String?
| 아이템을 길게 눌렀을 때 보여줄 설명 텍스트
activeIcon
→Widget?
| 선택된 상태에서 사용할 아이콘 (기본값:icon
)
backgroundColor
→Color?
|BottomNavigationBarType.shifting
에서 배경색 지정


IndexedStack
- 여러 자식을 겹쳐 놓되, 하나의 자식만 보여주는 스택 레이아웃 위젯
- 네비게이션 바에서 인덱스를 바꿔도 rebuild 를 하지 않으면 화면이 변하지 않는다 tapbar랑 다르다
- 한번에 모든 스택이 빌드 된다 → 성능 저하
기본 정렬
mainAxisAlignment
: 없음
crossAxisAlignment
: 없음
(Stack 기반이므로 자식은 겹쳐지고, 정렬보다 위치 지정이 중요)
레이아웃
- 가로 → 부모의 가로 제약을 그대로 따름
- 세로 → 부모의 세로 제약을 그대로 따름
- 모든 자식이 존재하지만, 인덱스에 해당하는 자식만 보임
- 보이지 않는 자식도 레이아웃에는 참여함 (메모리에 유지됨)
속성
index
→int
| 표시할 자식 위젯의 인덱스
children
→List<Widget>
| 겹쳐서 놓을 자식 위젯 리스트
alignment
→AlignmentGeometry
| 자식의 정렬 위치 (기본값:Alignment.topStart
)
textDirection
→TextDirection?
| 정렬 시 텍스트 방향 고려
clipBehavior
→Clip
| 자식이 영역을 넘었을 때 잘라낼지 여부 (기본값:Clip.hardEdge
)


Card
- 모서리가 둥근 그림자 박스로 구성된 머티리얼 디자인 컨테이너 위젯
- 자식 위젯을 감싸 시각적으로 강조할 때 사용
- 내용이 많아져도 내부 스크롤은 제공하지 않으며, padding이 없어 직접 감싸야 함
기본 정렬
mainAxisAlignment
: 없음
crossAxisAlignment
: 없음
(단일 child를 받기 때문에 정렬 속성 없음, 내부에 Column 등 써야 정렬 가능)
레이아웃
- 가로 → 부모의 가로 제약을 그대로 따름
- 세로 → 자식 높이에 맞게 늘어남
- 내부 padding 없음 → 직접
Padding
위젯으로 감싸야 적절한 여백 생김
속성
child
→Widget?
| 카드 안에 표시할 위젯
color
→Color?
| 배경색 (기본값: 테마의cardColor
)
elevation
→double
| 그림자 깊이 (기본값:1.0
)
margin
→EdgeInsetsGeometry?
| 카드 외부 여백 (기본값:EdgeInsets.all(4.0)
)
shape
→ShapeBorder?
| 외곽 모양 (기본값:RoundedRectangleBorder
)
clipBehavior
→Clip
| 넘치는 영역을 자를지 여부 (기본값:Clip.none
)


Visibility
- 위젯을 조건에 따라 보이거나 숨기는 조건부 렌더링 위젯
- visible이 false이면 위젯이 사라짐 (기본적으로 공간도 사라짐)
- 옵션을 조합하면 상태, 크기, 애니메이션, 인터랙션 등을 유지한 채 숨길 수 있음
기본 정렬
mainAxisAlignment
: 없음
crossAxisAlignment
: 없음
(단일 child 위젯을 감싸므로 정렬은 내부 위젯에 따라 결정됨)
레이아웃
- 가로 → 자식 위젯의 너비에 따름
- 세로 → 자식 위젯의 높이에 따름
visible: false
이면 기본적으로 완전히 레이아웃에서 제거됨
- 단,
maintainSize
등 옵션을 켜면 공간만 유지하거나 상태도 유지 가능
속성
visible
→bool
| 위젯 표시 여부 (true
시 보임, 기본값:true
)
child
→Widget
| 표시 대상 위젯
replacement
→Widget
| visible이 false일 때 대신 보여줄 위젯 (기본:SizedBox.shrink()
)
maintainState
→bool
| 숨겨져도 State를 유지할지 (기본값:false
)
maintainAnimation
→bool
| 숨겨져도 애니메이션 상태 유지 (기본값:false
)
maintainSize
→bool
| 숨겨져도 공간을 차지할지 여부 (기본값:false
)
maintainSemantics
→bool
| 숨겨져도 접근성 정보 유지 (기본값:false
)
maintainInteractivity
→bool
| 숨겨져도 클릭 등 상호작용 가능 여부 (기본값:false
)
TextField
진행
1. 틀 잡기
MainScreens → const 사용법


import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/screens/chatting/chatting_screen.dart';
import 'package:flutter_carrot_market/screens/home/home_screen.dart';
import 'package:flutter_carrot_market/screens/my_carrot/my_carrot_screen.dart';
import 'package:flutter_carrot_market/screens/near_me/near_me_screen.dart';
import 'package:flutter_carrot_market/screens/neighborhood_life/neighborhood_life_screen.dart';
class MainScreens extends StatefulWidget {
@override
State<MainScreens> createState() => _MainScreensState();
}
class _MainScreensState extends State<MainScreens> {
int selectedIndex = 0;
List<int> loadPages = [0]; // 배열의 크기 1
void selectedBottomMenu(int index) {
if (!loadPages.contains(index)) {
loadPages.add(index); // [0, 1]
}
selectedIndex = index;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: selectedIndex,
children: [
loadPages.contains(0) ? const HomeScreen() : Container(),
loadPages.contains(1) ? const NeighborhoodLifeScreen() : Container(),
loadPages.contains(2) ? const NearMeScreen() : Container(),
loadPages.contains(3) ? const ChattingScreen() : Container(),
loadPages.contains(4) ? const MyCarrotScreen() : Container(),
],
),
bottomNavigationBar: _bottomNavigationBar(),
);
}
BottomNavigationBar _bottomNavigationBar() {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
// showUnselectedLabels: false, -> 레이블 글자 가리기
// showSelectedLabels: false,
selectedItemColor: Colors.orange,
unselectedItemColor: Colors.black54,
selectedFontSize: 12,
unselectedFontSize: 12,
currentIndex: selectedIndex,
onTap: selectedBottomMenu,
items: [
BottomNavigationBarItem(label: "홈", icon: Icon(CupertinoIcons.home)),
BottomNavigationBarItem(label: "동네생활", icon: Icon(CupertinoIcons.square_on_square)),
BottomNavigationBarItem(label: "내 근처", icon: Icon(CupertinoIcons.placemark)),
BottomNavigationBarItem(label: "채팅", icon: Icon(CupertinoIcons.chat_bubble_2)),
BottomNavigationBarItem(label: "나의 당근", icon: Icon(CupertinoIcons.person)),
],
);
}
}
- IndexedStack → 기본적으로 이 위젯은 children에 있는 모든 위젯을 한번에 build 한다
- 따라서 성능 저하가 발생할 수 있다
- 일단, 위와 같은 방법으로 (const) 위젯을 재사용하면 성능 저하를 막을 수 있다
- const → 메모리에 있는 데이터를 재사용 하는 방법
MainScreens → 배열 사용에서 사용하는 방법
import 'package:carrot_market/screens/chatting/chatting_screen.dart';
import 'package:carrot_market/screens/home/home_screen.dart';
import 'package:carrot_market/screens/my_carrot/my_carrot_screen.dart';
import 'package:carrot_market/screens/near_me/near_me_screen.dart';
import 'package:carrot_market/screens/neighborhood_life/neighborhood_life_screen.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class MainScreens extends StatefulWidget {
@override
State<MainScreens> createState() => _MainScreensState();
}
class _MainScreensState extends State<MainScreens> {
int selectedIndex = 0;
final List<Widget?> _screens = List.filled(5, null);
@override
void initState() {
super.initState();
_screens[0] = HomeScreen(); // 처음은 미리 생성
}
void selectBottomMenu(int index) {
if (_screens[index] == null) {
_screens[index] = _buildScreen(index); // 처음 클릭 시 생성
}
setState(() {
selectedIndex = index;
});
}
Widget _buildScreen(int index) {
switch (index) {
case 0:
return HomeScreen();
case 1:
return NeighborhoodLifeScreen();
case 2:
return NearMeScreen();
case 3:
return ChattingScreen();
case 4:
return MyCarrotScreen();
default:
return Container();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: selectedIndex,
children: _screens.map((screen) => screen ?? Container()).toList(),
),
bottomNavigationBar: _bottomNavigationBar(),
);
}
BottomNavigationBar _bottomNavigationBar() {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
selectedFontSize: 12.0,
unselectedFontSize: 12.0,
selectedItemColor: Colors.orange,
unselectedItemColor: Colors.black54,
currentIndex: selectedIndex,
onTap: selectBottomMenu,
items: const [
BottomNavigationBarItem(label: "홈", icon: Icon(CupertinoIcons.home)),
BottomNavigationBarItem(label: "동네생활", icon: Icon(CupertinoIcons.square_on_square)),
BottomNavigationBarItem(label: "내근처", icon: Icon(CupertinoIcons.placemark)),
BottomNavigationBarItem(label: "채팅", icon: Icon(CupertinoIcons.chat_bubble_2)),
BottomNavigationBarItem(label: "나의당근", icon: Icon(CupertinoIcons.person)),
],
);
}
}
2. 홈 화면
HomeScreen

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/models/product.dart';
import 'package:flutter_carrot_market/screens/home/components/list_item.dart';
import 'package:flutter_carrot_market/screens/home/detail/home_detail_screen.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen();
@override
Widget build(BuildContext context) {
print("HomeScreen build!");
return Scaffold(
appBar: _appBar(),
body: ListView.separated(
itemBuilder: (context, index) {
Product p = productList[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: InkWell(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => HomeDetailScreen()));
},
child: ListItem(p: p),
),
);
},
separatorBuilder: (context, index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Divider(thickness: .5, color: Colors.grey),
),
itemCount: productList.length,
),
);
}
AppBar _appBar() {
return AppBar(
title: Row(
children: [
Text("좌동"),
SizedBox(width: 4),
Icon(CupertinoIcons.chevron_down),
],
),
actions: [
IconButton(icon: const Icon(CupertinoIcons.search), onPressed: () {}),
IconButton(icon: const Icon(CupertinoIcons.list_dash), onPressed: () {}),
IconButton(icon: const Icon(CupertinoIcons.bell), onPressed: () {}),
],
bottom: PreferredSize(
preferredSize: Size(double.infinity, .5),
child: Divider(thickness: .5, color: Colors.grey),
),
);
}
}
ListItem

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/_core/my_util.dart';
import 'package:flutter_carrot_market/models/product.dart';
class ListItem extends StatelessWidget {
const ListItem({required this.p});
final Product p;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Container(
height: 115,
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
"https://picsum.photos/id/237/200/300",
width: 115,
fit: BoxFit.cover,
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
Text("${p.title}", style: TextStyle(fontWeight: FontWeight.bold)),
Text("${p.address} • ${p.publishedAt}", style: TextStyle(color: Colors.grey, fontSize: 12)),
Text("${p.price.toMoney()}원", style: TextStyle(fontWeight: FontWeight.bold)),
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
spacing: 3,
children: [
Icon(CupertinoIcons.chat_bubble_2, color: Colors.grey, size: 16),
Text("${p.commentsCount}", style: TextStyle(color: Colors.grey)),
SizedBox(width: 3),
InkWell(
onTap: () {},
child: Icon(CupertinoIcons.heart, color: Colors.grey, size: 16),
),
Text("${p.heartCount}", style: TextStyle(color: Colors.grey)),
],
),
],
),
),
],
),
),
);
}
}
숫자 포매터
import 'package:intl/intl.dart';
// 그냥 함수로 빼서 사용하는 방법
String formatToMoney(String price) {
final formatter = NumberFormat('#,###');
return formatter.format(int.parse(price));
}
// String 클래스의 내장 함수처럼 사용하는 방
extension MoneyFormatter on String {
/// Created By JAEWON, 2025.06.02
///
/// email : won8070@naver.com
///
/// tip : 콤마(,) 붙여줌
String toMoney() {
final formatter = NumberFormat('#,###');
return formatter.format(int.parse(this));
}
}
3. 나의 당근
MyCarrotScreen

import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/models/icon_menu.dart';
import 'package:flutter_carrot_market/screens/my_carrot/components/card_icon_menu.dart';
import 'package:flutter_carrot_market/screens/my_carrot/components/my_carrot_header.dart';
class MyCarrotScreen extends StatelessWidget {
const MyCarrotScreen();
@override
Widget build(BuildContext context) {
print("MyCarrotScreen build!");
return Scaffold(
appBar: _appBar(),
body: ListView(
children: [
MyCarrotHeader(),
SizedBox(height: 8),
CardIconMenu(iconMenuList: iconMenu1),
SizedBox(height: 8),
CardIconMenu(iconMenuList: iconMenu2),
SizedBox(height: 8),
CardIconMenu(iconMenuList: iconMenu3),
],
),
);
}
AppBar _appBar() {
return AppBar(
title: Text("나의 당근"),
actions: [IconButton(onPressed: () {}, icon: Icon(Icons.settings))],
bottom: PreferredSize(
preferredSize: Size(double.infinity, .5),
child: Divider(thickness: .5, color: Colors.grey),
),
);
}
}
MyCarrotHeader

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class MyCarrotHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
elevation: .5,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
child: Column(
spacing: 30,
children: [
_buildProfileRow(),
_buildProfileButton(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildRoundTextButton(title: "판매내역", iconData: CupertinoIcons.square_list_fill),
_buildRoundTextButton(title: "구매내역", iconData: CupertinoIcons.bag_fill),
_buildRoundTextButton(title: "관심목록", iconData: CupertinoIcons.heart),
],
),
],
),
),
);
}
Container _buildRowIconItem() {
return Container(
height: 50,
child: Row(
spacing: 20,
children: [
Icon(Icons.place, size: 17),
Text("내 동네 설정"),
],
),
);
}
Container _buildProfileButton() {
return Container(
width: double.infinity,
child: OutlinedButton(
onPressed: () {},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text("프로필 보기"),
),
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
);
}
Row _buildProfileRow() {
return Row(
children: [
Stack(
children: [
Container(
width: 65,
height: 65,
child: CircleAvatar(
backgroundImage: NetworkImage("https://picsum.photos/id/237/200/300"),
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(15), color: Colors.white),
child: Icon(Icons.camera_enhance, size: 15),
),
),
],
),
SizedBox(width: 16),
Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("developer", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
SizedBox(height: 10),
Text("좌동 #00912", style: TextStyle(color: Colors.grey, fontSize: 12)),
],
),
),
],
);
}
InkWell _buildRoundTextButton({required String title, required IconData iconData}) {
return InkWell(
onTap: () {},
child: Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: Colors.grey, width: .5),
),
child: Icon(iconData, color: Colors.orange, size: 20),
),
SizedBox(height: 10),
Text(title),
],
),
);
}
}
CardIconMenu

import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/models/icon_menu.dart';
class CardIconMenu extends StatelessWidget {
final List<IconMenu> iconMenuList;
CardIconMenu({required this.iconMenuList});
@override
Widget build(BuildContext context) {
return Card(
elevation: .5,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: List.generate(
iconMenuList.length,
(index) => _buildRowIconItem(title: iconMenuList[index].title, iconData: iconMenuList[index].iconData),
),
),
),
);
}
Container _buildRowIconItem({required String title, required IconData iconData}) {
return Container(
height: 50,
child: Row(
spacing: 20,
children: [
Icon(iconData, size: 17),
Text(title),
],
),
);
}
}
4. 채팅
appBar

import 'package:flutter/material.dart';
class ChattingScreen extends StatelessWidget {
const ChattingScreen();
@override
Widget build(BuildContext context) {
print("ChattingScreen build!");
return Scaffold(appBar: _appBar(), body: ListView());
}
AppBar _appBar() => AppBar(
title: Text("채팅"),
bottom: PreferredSize(
preferredSize: Size(double.infinity, .5),
child: Divider(
thickness: .5,
color: Colors.grey,
),
),
);
}
ImageContainer

import 'package:flutter/material.dart';
class ImageContainer extends StatelessWidget {
final double borderRadius;
final String imageUrl;
final double width;
final double height;
ImageContainer({
required this.borderRadius,
required this.imageUrl,
required this.width,
required this.height,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: Image.network(
imageUrl,
width: width,
height: height,
fit: BoxFit.cover,
),
);
}
}
ChatContainer

import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/models/chat_message.dart';
import 'package:flutter_carrot_market/screens/_components/image_container.dart';
class ChatContainer extends StatelessWidget {
final ChatMessage chatMessage;
ChatContainer({required this.chatMessage});
@override
Widget build(BuildContext context) {
return Container(
height: 100,
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey, width: .5)),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
spacing: 16,
children: [
ImageContainer(
borderRadius: 999,
height: 50,
width: 50,
imageUrl: chatMessage.profileImage,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RichText(
text: TextSpan(
children: [
TextSpan(text: "${chatMessage.sender} "),
TextSpan(
text: "${chatMessage.location} ",
style: TextStyle(color: Colors.grey, fontSize: 12),
),
TextSpan(
text: "• ",
style: TextStyle(color: Colors.grey, fontSize: 12),
),
TextSpan(
text: "${chatMessage.sendDate}",
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
),
Text(
"${chatMessage.message}",
overflow: TextOverflow.ellipsis,
),
],
),
),
Visibility(
visible: chatMessage.imageUri != null,
child: ImageContainer(
borderRadius: 8,
height: 50,
width: 50,
imageUrl: chatMessage.imageUri ?? "",
),
),
],
),
),
);
}
}
ChattingScreen

import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/models/chat_message.dart';
import 'package:flutter_carrot_market/screens/_components/appbar_preferred_size.dart';
import 'package:flutter_carrot_market/screens/chatting/components/chat_container.dart';
class ChattingScreen extends StatelessWidget {
const ChattingScreen();
@override
Widget build(BuildContext context) {
print("ChattingScreen build!");
return Scaffold(
appBar: _appBar(),
body: ListView(
children: List.generate(
chatMessageList.length,
(index) => ChatContainer(chatMessage: chatMessageList[index]),
),
),
);
}
AppBar _appBar() => AppBar(
title: Text("채팅"),
bottom: appBarBottomLine(),
);
}
5. 동네생활
appBar

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/screens/_components/appbar_preferred_size.dart';
class NeighborhoodLifeScreen extends StatelessWidget {
const NeighborhoodLifeScreen();
@override
Widget build(BuildContext context) {
print("NeighborhoodLifeScreen build!");
return Scaffold(
appBar: _appBar(),
body: Center(child: Text("neighborhood life")),
);
}
AppBar _appBar() {
return AppBar(
title: Text("동네생활"),
actions: [
IconButton(onPressed: () {}, icon: Icon(CupertinoIcons.search)),
IconButton(onPressed: () {}, icon: Icon(CupertinoIcons.plus_rectangle_on_rectangle)),
IconButton(onPressed: () {}, icon: Icon(CupertinoIcons.bell)),
],
bottom: appBarBottomLine(),
);
}
}
LifeHeader

import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/screens/_components/image_container.dart';
class LifeHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
elevation: .5,
margin: EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
spacing: 16,
children: [
ImageContainer(
borderRadius: 6,
imageUrl: "https://picsum.photos/id/780/200/100",
width: 45,
height: 45,
),
Expanded(
child: Text(
"이웃과 함께 만드는 봄 간식 지도 마음까지 따뜻해지는 봄 간식을 만나보아요",
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
}
LifeBody
class LifeBody extends StatelessWidget {
const LifeBody({
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildTop(),
_buildWriter(),
_buildWriting(),
_buildImage(),
Divider(),
_buildTail(),
],
);
}
...
}
_buildTop()

Widget _buildTop() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black12,
borderRadius: BorderRadius.circular(4),
),
child: Text("우리동네질문", style: textTheme().bodyMedium),
),
Text("3시간전", style: textTheme().bodyMedium),
],
),
);
}
_buildWriter()

Widget _buildWriter() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
ImageContainer(
width: 30,
height: 30,
borderRadius: 999,
imageUrl: 'https://picsum.photos/id/780/200/100',
),
Text.rich(
TextSpan(
children: [
TextSpan(text: ' 헬로비비'),
TextSpan(text: ' 좌동'),
TextSpan(text: ' 인증 3회'),
],
),
),
],
),
);
}
_buildWriting()

Widget _buildWriting() {
return Padding(
padding: const EdgeInsets.all(16),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'예민한 개도 미용할 수 있는 곳이나 동물 병원 어디 있을까요? 내일 유기견을 데려오기로 했는데 아직 성향을 잘 몰라서 걱정이 돼요ㅜㅜ',
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
);
}
_buildImage()

Widget _buildImage() {
return Visibility(
visible: true,
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
child: Image.network(
'https://picsum.photos/id/780/200/100',
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
),
);
}
_buildTail()

Widget _buildTail() {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Icon(FontAwesomeIcons.smile),
SizedBox(width: 8),
Text('공감하기'),
SizedBox(width: 22),
Icon(FontAwesomeIcons.commentAlt),
SizedBox(width: 8),
Text('댓글쓰기 11'),
],
),
);
}
NeighborhoodLifeScreen
class NeighborhoodLifeScreen extends StatelessWidget {
const NeighborhoodLifeScreen();
@override
Widget build(BuildContext context) {
print("NeighborhoodLifeScreen build!");
return Scaffold(
appBar: _appBar(),
body: ListView(
children: [
LifeHeader(),
...List.generate(
10,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: LifeBody(),
),
),
],
),
);
}
...
}
- List.generate 생성자는 length 만큼 반복문을 돌면서 List 형의 자료구조를 생성함
- (…) 스프레드 연산자를 사용해서 하나씩 꺼내서 출력함
6. 내 근처 화면 만들기
appBar

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/screens/_components/appbar_preferred_size.dart';
class NearMeScreen extends StatelessWidget {
const NearMeScreen();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar(),
body: Center(child: Text("near me")),
);
}
AppBar _appBar() {
return AppBar(
title: Text('내 근처'),
actions: [
IconButton(onPressed: () {}, icon: const Icon(CupertinoIcons.pencil)),
IconButton(onPressed: () {}, icon: const Icon(CupertinoIcons.bell)),
],
bottom: appBarBottomLine(),
);
}
}
SearchTextField

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class SearchTextField extends StatelessWidget {
const SearchTextField({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
child: TextField(
cursorColor: Colors.grey,
decoration: InputDecoration(
disabledBorder: _buildOutLineInputBorder(),
enabledBorder: _buildOutLineInputBorder(),
focusedBorder: _buildOutLineInputBorder(),
fillColor: Colors.black12,
filled: true,
prefixIcon: const Icon(CupertinoIcons.search, color: Colors.grey),
contentPadding: const EdgeInsets.symmetric(vertical: 15),
hintText: '좌동 주변 가게를 찾아 보세요',
hintStyle: TextStyle(fontSize: 18),
),
),
);
}
OutlineInputBorder _buildOutLineInputBorder() {
return OutlineInputBorder(
borderSide: BorderSide(width: .5, color: Colors.black26),
borderRadius: BorderRadius.circular(8),
);
}
}
NearMeScreen
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/screens/_components/appbar_preferred_size.dart';
import 'package:flutter_carrot_market/screens/near_me/components/search_text_field.dart';
class NearMeScreen extends StatelessWidget {
const NearMeScreen();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar(),
body: ListView(
children: [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SearchTextField(),
),
],
),
);
}
...
}
- 부모가 패딩을 준다
RoundBorderText

import 'package:flutter/material.dart';
class RoundBorderText extends StatelessWidget {
final String title;
final int position;
const RoundBorderText({
required this.title,
required this.position,
});
@override
Widget build(BuildContext context) {
var paddingValue = position == 0 ? 16.0 : 8.0;
return Padding(
padding: EdgeInsets.only(left: paddingValue),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
border: Border.all(width: .5, color: Colors.grey),
),
child: Text(
'학원',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
),
);
}
}
NearMeScreen

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/screens/_components/appbar_preferred_size.dart';
import 'package:flutter_carrot_market/screens/near_me/components/round_border_text.dart';
import 'package:flutter_carrot_market/screens/near_me/components/search_text_field.dart';
class NearMeScreen extends StatelessWidget {
const NearMeScreen();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar(),
body: ListView(
children: [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SearchTextField(),
),
_buildHorizonScroll(),
Divider(thickness: 10, color: Colors.grey),
],
),
);
}
SizedBox _buildHorizonScroll() {
return SizedBox(
height: 66,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 8,
itemBuilder: (context, index) {
return Center(
child: RoundBorderText(title: 'title', position: index),
);
},
),
);
}
...
}
- center 를 사용하면 예쁘게 모양이 잡힌다
bottomTitleIcon

import 'package:flutter/cupertino.dart';
class BottomTitleIcon extends StatelessWidget {
const BottomTitleIcon({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
width: 80,
child: Column(
children: [
Icon(CupertinoIcons.person, size: 30),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'구인구직',
style: TextStyle(fontSize: 14),
),
),
],
),
);
}
}
NearMeScreen

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/screens/_components/appbar_preferred_size.dart';
import 'package:flutter_carrot_market/screens/near_me/components/bottom_title_icon.dart';
import 'package:flutter_carrot_market/screens/near_me/components/round_border_text.dart';
import 'package:flutter_carrot_market/screens/near_me/components/search_text_field.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class NearMeScreen extends StatelessWidget {
const NearMeScreen();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar(),
body: ListView(
children: [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SearchTextField(),
),
_buildHorizonScroll(),
Divider(thickness: 10, color: Colors.grey),
Padding(
padding: const EdgeInsets.only(left: 16, top: 30),
child: Wrap(
alignment: WrapAlignment.start,
spacing: 22.0,
runSpacing: 30,
children: [
const BottomTitleIcon(title: '구인구직', iconData: FontAwesomeIcons.user),
const BottomTitleIcon(title: '과외/클래스', iconData: FontAwesomeIcons.edit),
const BottomTitleIcon(title: '농수산물', iconData: FontAwesomeIcons.appleAlt),
const BottomTitleIcon(title: '부동산', iconData: FontAwesomeIcons.hotel),
const BottomTitleIcon(title: '중고차', iconData: FontAwesomeIcons.car),
const BottomTitleIcon(title: '전시/행사', iconData: FontAwesomeIcons.chessBishop),
],
),
),
SizedBox(height: 50),
],
),
);
}
...
}
StoreItem

import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/models/recommend_store.dart';
import 'package:flutter_carrot_market/theme.dart';
class StoreItem extends StatelessWidget {
final RecommendStore recommendStore;
const StoreItem({
super.key,
required this.recommendStore,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(width: 0.3, color: Colors.grey),
),
width: 289,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_buildClipRRect(topLeft: 10),
const SizedBox(width: 2),
_buildClipRRect(topRight: 10),
],
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(text: '${recommendStore.storeName} ', style: textTheme().displayLarge),
TextSpan(text: '${recommendStore.location}'),
],
),
),
const SizedBox(height: 8),
Text(
'${recommendStore.description}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme().titleMedium,
),
const SizedBox(height: 8),
Text.rich(
TextSpan(
children: [
TextSpan(
text: '후기 ${recommendStore.commentCount}',
style: TextStyle(fontSize: 15, color: Colors.blue),
),
TextSpan(
text: ' • 관심 ${recommendStore.likeCount}',
style: textTheme().titleMedium,
),
],
),
),
],
),
),
Expanded(
child: Container(
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.grey[200], borderRadius: BorderRadius.circular(10)),
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '${recommendStore.commentUser},',
style: TextStyle(fontSize: 13, color: Colors.black, fontWeight: FontWeight.bold),
),
TextSpan(
text: '${recommendStore.comment}',
style: TextStyle(fontSize: 12, color: Colors.black),
),
],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
);
}
ClipRRect _buildClipRRect({double topLeft = 0, double topRight = 0}) {
return ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(topLeft),
topRight: Radius.circular(topRight),
),
child: Image.network(
'https://picsum.photos/id/780/200/100',
width: 143,
height: 100,
fit: BoxFit.cover,
),
);
}
}
NearMeScreen

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_carrot_market/models/recommend_store.dart';
import 'package:flutter_carrot_market/screens/_components/appbar_preferred_size.dart';
import 'package:flutter_carrot_market/screens/near_me/components/bottom_title_icon.dart';
import 'package:flutter_carrot_market/screens/near_me/components/round_border_text.dart';
import 'package:flutter_carrot_market/screens/near_me/components/search_text_field.dart';
import 'package:flutter_carrot_market/screens/near_me/components/store_item.dart';
import 'package:flutter_carrot_market/theme.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class NearMeScreen extends StatelessWidget {
const NearMeScreen();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar(),
body: ListView(
children: [
...
SizedBox(height: 50),
Padding(
padding: const EdgeInsets.only(left: 16),
child: Text('이웃들의 추천 가게', style: textTheme().displayMedium),
),
SizedBox(height: 20),
Container(
height: 300,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 2,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(left: 16),
child: StoreItem(
recommendStore: recommendStoreList[index],
),
);
},
),
),
SizedBox(height: 40),
],
),
);
}
SizedBox _buildHorizonScroll() {
return SizedBox(
height: 66,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 8,
itemBuilder: (context, index) {
return Center(
child: RoundBorderText(title: 'title', position: index),
);
},
),
);
}
AppBar _appBar() {
return AppBar(
title: Text('내 근처'),
actions: [
IconButton(onPressed: () {}, icon: const Icon(CupertinoIcons.pencil)),
IconButton(onPressed: () {}, icon: const Icon(CupertinoIcons.bell)),
],
bottom: appBarBottomLine(),
);
}
}
Share article