Flutter
[Flutter] carrot_market_app 만들기
ekkkang
2025. 1. 15. 17:12
yaml 파일 설정
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
# Google Fonts 라이브러리
# Google Fonts에서 제공하는 다양한 폰트를 앱에서 쉽게 사용할 수 있습니다.
google_fonts: ^6.1.0
# Font Awesome Flutter 아이콘 라이브러리
# 이 라이브러리는 Font Awesome 아이콘 세트를 Flutter 앱에서 사용할 수 있도록 지원합니다.
# 예: 소셜 미디어 아이콘(Facebook, Twitter), 기기 아이콘(카메라, 컴퓨터) 등을 앱에 추가할 수 있습니다.
font_awesome_flutter: ^10.5.0
# Internationalization (intl) 라이브러리
# 앱에서 날짜, 시간, 숫자 및 텍스트를 다양한 언어와 형식으로 표시할 수 있도록 도와줍니다.
# 예: 달력 앱에서 '2025년 1월 13일' 또는 'January 13, 2025'와 같은 형식 지원.
intl: ^0.18.1
1단계 작업
theme.dart 파일
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
// 텍스트 테마
TextTheme textTheme() {
return TextTheme(
// 가장 큰 제목 스타일
displayLarge: GoogleFonts.openSans(fontSize: 18.0, color: Colors.black),
// 중간 크기의 제목 스타일
displayMedium: GoogleFonts.openSans(
fontSize: 16.0, color: Colors.black, fontWeight: FontWeight.bold),
// 본문 텍스트 스타일 (기사, 설명)
bodyLarge: GoogleFonts.openSans(fontSize: 16.0, color: Colors.black),
// 부제목, 작은 본문 텍스트 스타일
bodyMedium: GoogleFonts.openSans(fontSize: 14.0, color: Colors.grey),
// 중간 크기의 제목 스타일
titleMedium: GoogleFonts.openSans(fontSize: 15.0, color: Colors.black),
);
}
// AppBar 테마 설정
AppBarTheme appBarTheme() {
return AppBarTheme(
centerTitle: false,
color: Colors.white,
elevation: 0.0,
iconTheme: IconThemeData(color: Colors.black),
titleTextStyle: GoogleFonts.openSans(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black // 앱바 제목 텍스트 색상
),
);
}
// 바텀네비게이션바 테마 설정
BottomNavigationBarThemeData bottomNavigationBarTheme() {
return BottomNavigationBarThemeData(
selectedItemColor: Colors.orange, // 선택된 아이템 색상
unselectedItemColor: Colors.black54, // 선택되지 않은 아이템 색상
showUnselectedLabels: true, // 선택 안된 라벨 표시 여부 설정
);
}
// 전체 ThemeData 설정
ThemeData mTheme() {
return ThemeData(
// 머터리얼 3 때부터 변경 됨..
// 자동 셋팅
// colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange)
// 우리가 직접 지정 함
colorScheme: ColorScheme.fromSwatch(
primarySwatch: Colors.orange,
),
scaffoldBackgroundColor: Colors.white,
textTheme: textTheme(),
appBarTheme: appBarTheme(),
bottomNavigationBarTheme: bottomNavigationBarTheme(),
);
}
main01.dart
import 'package:flutter/material.dart';
import 'screens/main_screen.dart';
import 'theme.dart';
void main() {
runApp(CarrotMarketUI());
}
class CarrotMarketUI extends StatelessWidget {
const CarrotMarketUI({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'carrot_ui',
debugShowCheckedModeBanner: false,
theme: mTheme(),
home: MainScreen(),
);
}
}
main_screen.dart
import 'package:flutter/material.dart';
// 바텀네비게이션을 들고 있다.
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Center(
child: Text('main screen'),
),
),
);
}
}
2단계 작업(패키지 만들고 기본 코드 작성)
import 'package:flutter/material.dart';
class ChattingPage extends StatelessWidget {
const ChattingPage({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text('ChattingPage'),
);
}
}
----------------------------------------------
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text('HomePage'),
);
}
}
----------------------------------------------
import 'package:flutter/material.dart';
class MyCarrotPage extends StatelessWidget {
const MyCarrotPage({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text('MyCarrotPage'),
);
}
}
----------------------------------------------
import 'package:flutter/material.dart';
class NearMePage extends StatelessWidget {
const NearMePage({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text('NearMePage'),
);
}
}
----------------------------------------------
import 'package:flutter/material.dart';
class NeighborhoodLifePage extends StatelessWidget {
const NeighborhoodLifePage({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text('NearMePage'),
);
}
}
----------------------------------------------
3단계 작업(절대 경로, 상대경로 확인)
main03.dart
import 'package:flutter/material.dart';
import 'screens/main_screen.dart';
import 'theme.dart';
void main() {
runApp(CarrotMarketUI());
}
class CarrotMarketUI extends StatelessWidget {
const CarrotMarketUI({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'carrot_ui',
debugShowCheckedModeBanner: false,
theme: mTheme(),
home: MainScreen(),
);
}
}
import 'package:flutter/cupertino.dart';
import 'chatting/chatting_page.dart';
import 'home/home_page.dart';
import 'my_carrot/my_carrot_page.dart';
import 'near_me/near_me_page.dart';
import 'neighborhood_life/neighborhood_life_page.dart';
import 'package:flutter/material.dart';
// import는 가능한 상대경로를 사용하자.
// 바텀네비게이션을 들고 있다.
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _selectedIndex = 0; // 현재 선택된 탭의 인덱스를 저장하는 변수
// 상태(속성)은 행위를 통해서 변경해야 한다.
void changeStackPages(int index) {
setState(() {
_selectedIndex = index; // 탭 변경 시 상태 업데이트 build() 재 호출
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
// 현재 선택된 인덱스 번호를 활용해서 해당하는 위젯을
// 화면에 그려줄 수 있다.
body: IndexedStack(
// 현재 선택된 인덱스 번호 설정
index: _selectedIndex,
children: [
HomePage(),
NeighborhoodLifePage(),
NearMePage(),
ChattingPage(),
MyCarrotPage(),
],
),
bottomNavigationBar: BottomNavigationBar(
backgroundColor: Colors.white,
// 동일한 크기의 탭을 만들어 준다.
type: BottomNavigationBarType.fixed,
// 현재 선택된 탭 번호 명시
currentIndex: _selectedIndex,
onTap: (index) {
changeStackPages(index);
},
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),
),
],
),
),
);
}
}
analysis_options.yaml
#linter란?
# 코드 스타일과 품질을 검사하여 코드에 있는 잠재적 오류
# 스타일 위반, 비효율적 패턴 등을 알려주는 도구입니다.
linter:
rules:
# 상수로 선언 가능한 생성자에 대해 경고를 표시하지 않음으로 설정.
prefer_const_constructors: false
# 변하지 않는 변수에 대해 const로 선언하지 않아도 경고를 표시하지 않음.
prefer_const_declarations: false
# 불변 컬렉션(List, Map, Set 등)에 대해 const를 사용하지 않아도 경고를 표시하지 않음.
prefer_const_literals_to_create_immutables: false
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
4단계 작업
class Product {
String title;
String author;
String address;
String urlToImage;
String publishedAt;
String price;
int heartCount;
int commentsCount;
Product(
{required this.title,
required this.author,
required this.address,
required this.urlToImage,
required this.publishedAt,
required this.price,
required this.heartCount,
required this.commentsCount});
}
// 샘플 데이터
List<Product> productList = [
Product(
title: '니트 조끼',
author: 'author_1',
urlToImage:
'https://github.com/flutter-coder/ui_images/blob/master/carrot_product_7.jpg?raw=true',
publishedAt: '2시간 전',
heartCount: 8,
price: '35000',
address: '좌동',
commentsCount: 3),
Product(
title: '먼나라 이웃나라 12',
author: 'author_2',
urlToImage:
'https://github.com/flutter-coder/ui_images/blob/master/carrot_product_6.jpg?raw=true',
publishedAt: '3시간 전',
heartCount: 3,
address: '중동',
price: '18000',
commentsCount: 1),
Product(
title: '캐나다구스 패딩조',
author: 'author_3',
address: '우동',
urlToImage:
'https://github.com/flutter-coder/ui_images/blob/master/carrot_product_5.jpg?raw=true',
publishedAt: '1일 전',
heartCount: 0,
price: '15000',
commentsCount: 12,
),
Product(
title: '유럽 여행',
author: 'author_4',
address: '우동',
urlToImage:
'https://github.com/flutter-coder/ui_images/blob/master/carrot_product_4.jpg?raw=true',
publishedAt: '3일 전',
heartCount: 4,
price: '15000',
commentsCount: 11,
),
Product(
title: '가죽 파우치 ',
author: 'author_5',
address: '우동',
urlToImage:
'https://github.com/flutter-coder/ui_images/blob/master/carrot_product_3.jpg?raw=true',
publishedAt: '1주일 전',
heartCount: 7,
price: '95000',
commentsCount: 4,
),
Product(
title: '노트북',
author: 'author_6',
address: '좌동',
urlToImage:
'https://github.com/flutter-coder/ui_images/blob/master/carrot_product_2.jpg?raw=true',
publishedAt: '5일 전',
heartCount: 4,
price: '115000',
commentsCount: 0,
),
Product(
title: '미개봉 아이패드',
author: 'author_7',
address: '좌동',
urlToImage:
'https://github.com/flutter-coder/ui_images/blob/master/carrot_product_1.jpg?raw=true',
publishedAt: '5일 전',
heartCount: 8,
price: '85000',
commentsCount: 3,
),
];
product_item.dart
import 'package:flutter/material.dart';
import 'product_detail.dart';
import '../../../models/product.dart';
class ProductItem extends StatelessWidget {
final Product product;
const ProductItem({required this.product, super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 135.0,
padding: EdgeInsets.all(16.0),
child: Row(
children: [
// 이미지
ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Image.network(
product.urlToImage,
width: 115,
height: 115,
fit: BoxFit.cover,
),
),
const SizedBox(width: 16.0),
// 상품 설명 - 위젯
ProductDetail(product: product)
],
),
);
}
}
product_detail.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../../carrot_market_ui_04/theme.dart';
import '../../../models/product.dart';
import 'package:intl/intl.dart';
class ProductDetail extends StatelessWidget {
final Product product;
const ProductDetail({required this.product, super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.title,
style: textTheme().bodyLarge,
),
const SizedBox(height: 4.0),
Text(
'${product.address} · ${product.publishedAt}',
),
const SizedBox(height: 4.0),
Text(
'${numberFormat(product.price)}',
style: textTheme().displayMedium,
),
const Spacer(),
// 좋아요, 리뷰 개수
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Visibility(
// 비교 연산자 --> bool
visible: product.commentsCount > 0,
child: _buildIcons(
product.commentsCount,
CupertinoIcons.chat_bubble_2,
),
),
const SizedBox(width: 8.0),
Visibility(
visible: product.heartCount > 0,
child: _buildIcons(
product.heartCount,
CupertinoIcons.heart,
),
),
],
)
],
),
);
}
// 포멧 함수 만들어 보기
String numberFormat(String price) {
final formatter = NumberFormat('#,###');
// 형변환 처리
return formatter.format(int.parse(price));
}
Widget _buildIcons(int count, IconData mIcon) {
return Row(
children: [
Icon(mIcon, size: 14.0),
const SizedBox(width: 4.0),
Text('$count')
],
);
}
}
home_page.dart
import 'package:class_carrot_app/carrot_market_ui_04/screens/home/components/product_item.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../models/product.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
forceMaterialTransparency: true,
title: Row(
children: [
Text('좌동'),
const SizedBox(width: 4.0),
Icon(
Icons.keyboard_arrow_down,
size: 25,
)
],
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(0.5),
child: Divider(
thickness: 0.5,
height: 0.5,
color: Colors.grey,
),
),
actions: [
IconButton(
onPressed: () {},
icon: Icon(Icons.search),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.list),
),
IconButton(
onPressed: () {},
icon: Icon(CupertinoIcons.bell),
)
],
),
body: ListView.separated(
itemBuilder: (context, index) =>
ProductItem(product: productList[index]),
separatorBuilder: (context, index) => Divider(
height: 0,
indent: 16.0,
endIndent: 16.0,
color: Colors.grey,
),
itemCount: productList.length,
),
);
}
}
5단계 작업
screens/components/appbar_preferred_size.dart
import 'package:flutter/material.dart';
PreferredSize appBarBottomLine() {
var height = 0.5;
return PreferredSize(
preferredSize: Size.fromHeight(height),
child: Divider(
thickness: height,
height: height,
color: Colors.grey,
),
);
}
screens/components/image_container.dart
import 'package:flutter/material.dart';
class ImageContainer extends StatelessWidget {
final double borderRadius;
final String imageUrl;
final double width;
final double height;
const ImageContainer({
required this.borderRadius,
required this.imageUrl,
required this.width,
required this.height,
super.key,
});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: Image.network(
imageUrl,
width: width,
height: height,
fit: BoxFit.cover,
),
);
}
}
models/chat_message.dart
class ChatMessage {
final String sender;
final String profileImage;
final String location;
final String sendDate;
final String message;
final String? imageUri;
ChatMessage({
required this.sender,
required this.profileImage,
required this.location,
required this.sendDate,
required this.message,
this.imageUri,
});
}
// 샘플 데이터
List<ChatMessage> chatMessageList = [
ChatMessage(
sender: '당근이, ',
profileImage: 'https://picsum.photos/id/870/200/100?grayscale',
location: '대부동',
sendDate: '1일전',
message: 'developer 님,근처에 다양한 물품들이 아주 많이있습니다.',
),
ChatMessage(
sender: 'Flutter ',
profileImage: 'https://picsum.photos/id/880/200/100?grayscale',
location: '중동',
sendDate: '2일전',
message: '안녕하세요 지금 다 예약 상태 인가요?',
imageUri: 'https://picsum.photos/id/890/200/100?grayscale',
)
];
chat_container.dart
import 'package:class_carrot_app/carrot_market_ui_05/models/chat_message.dart';
import 'package:class_carrot_app/carrot_market_ui_05/screens/components/image_container.dart';
import 'package:flutter/material.dart';
class ChatContainer extends StatelessWidget {
final ChatMessage chatMessage;
const ChatContainer({required this.chatMessage, super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey, width: 0.5),
),
),
child: Row(
children: [
ImageContainer(
borderRadius: 25,
imageUrl: chatMessage.profileImage,
width: 50,
height: 50,
)
],
),
);
}
}
chatting_page.dart
import 'package:class_carrot_app/carrot_market_ui_05/models/chat_message.dart';
import 'package:class_carrot_app/carrot_market_ui_05/screens/chatting/components/chat_container.dart';
import 'package:flutter/material.dart';
class ChattingPage extends StatelessWidget {
const ChattingPage({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: [
// .. 위젯 만들어서 두개 내려줄 생각
ChatContainer(
chatMessage: chatMessageList[0],
),
],
);
}
}