[Flutter] Flutter 예제- 당근마켓 만들기

류재성's avatar
Jun 04, 2024
[Flutter] Flutter 예제- 당근마켓 만들기
 

1. 기본 세팅

 
notion image
 
pubspec.yaml 파일을 세팅 후 pub get을 누른다.
 

2. 앱 뼈대 만들기

 
main.dart
import 'package:carrot_market_ui/screens/main_screens.dart'; import 'package:carrot_market_ui/theme.dart'; import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: MainScreens(), theme: theme(), ); } }
 
screens/main_screen.dart
import 'package:flutter/material.dart'; class MainScreens extends StatefulWidget { const MainScreens({super.key}); @override State<MainScreens> createState() => _MainScreensState(); } class _MainScreensState extends State<MainScreens> { @override Widget build(BuildContext context) { return Container( child: Center( child: Text('MainScreens'), ), ); } }
 
theme.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), ); } IconThemeData iconTheme() { return const IconThemeData( color: Colors.black, ); } AppBarTheme appBarTheme() { return AppBarTheme( centerTitle: false, color: Colors.white, elevation: 0.0, iconTheme: iconTheme(), titleTextStyle: GoogleFonts.nanumGothic( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black, ), ); } BottomNavigationBarThemeData bottomNavigatorTheme() { return const BottomNavigationBarThemeData( selectedItemColor: Colors.orange, unselectedItemColor: Colors.black54, showUnselectedLabels: true, ); } ThemeData theme() { return ThemeData( scaffoldBackgroundColor: Colors.white, textTheme: textTheme(), appBarTheme: appBarTheme(), bottomNavigationBarTheme: bottomNavigatorTheme(), primarySwatch: Colors.orange, ); }
 
notion image
 

3. BottomNavigationBar 만들기

 

3.1. BottomNavigationBar 만들기

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class MainScreens extends StatefulWidget { const MainScreens({super.key}); @override State<MainScreens> createState() => _MainScreensState(); } class _MainScreensState extends State<MainScreens> { int _selectedIndex = 0; @override Widget build(BuildContext context) { return Scaffold( body: IndexedStack( index: _selectedIndex, children: [ // HomeScreen(), // NeighborhoodLifeScreen(), // NearMeScreen(), // MyCarrotScreen(), ], ), bottomNavigationBar: BottomNavigationBar( backgroundColor: Colors.white, type: BottomNavigationBarType.fixed, currentIndex: _selectedIndex, onTap: (index) { setState(() { _selectedIndex = 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)), ], ), ); } }
 
notion image
 
main_screen.dart
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import '../CustomBottomNavigationBar.dart'; class MainScreens extends StatefulWidget { const MainScreens({super.key}); @override State<MainScreens> createState() => _MainScreensState(); } class _MainScreensState extends State<MainScreens> { int _selectedIndex = 0; @override Widget build(BuildContext context) { return Scaffold( body: IndexedStack( index: _selectedIndex, children: const [ // HomeScreen(), // NeighborhoodLifeScreen(), // NearMeScreen(), // MyCarrotScreen(), ], ), bottomNavigationBar: CustomBottomNavigationBar( selectedIndex: _selectedIndex, onItemSelected: (index) { setState(() { _selectedIndex = index; }); }, ), ); } }
 
custom_bottom_navigation_bar.dart
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class CustomBottomNavigationBar extends StatelessWidget { final int selectedIndex; final Function(int) onItemSelected; const CustomBottomNavigationBar({ Key? key, required this.selectedIndex, required this.onItemSelected, }) : super(key: key); @override Widget build(BuildContext context) { return BottomNavigationBar( backgroundColor: Colors.white, type: BottomNavigationBarType.fixed, currentIndex: selectedIndex, onTap: onItemSelected, 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)), ], ); } }
 

3.2. BottomNavigationBar 내부 작성하기

 
home_screen.dart
import 'package:flutter/material.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { return Center( child: Text("homeScreen"), ); } }
 
 
notion image
 
동일한 코드로 다른 화면도 만들어준다.
 
notion image
 
 

4. HomeScreen 내부 만들기

 
model/product.dart
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, ), ];
 
screens/home/components/product_detail.dart
import 'package:flutter/cupertino.dart'; import 'package:intl/intl.dart'; import '../../../model/product.dart'; import '../../../theme.dart'; class ProductDetail extends StatelessWidget { final Product product; const ProductDetail({required this.product}); @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( 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 iconData) { return Row( children: [ Icon(iconData, size: 14.0), const SizedBox(width: 4.0), Text('$count'), ], ); } }
 
home/components/product_item.dart
import 'package:carrot_market_ui/screens/home/components/product_detail.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import '../../../model/product.dart'; class ProductItem extends StatelessWidget { final Product product; ProductItem({required this.product}); @override Widget build(BuildContext context) { return Container( height: 135.0, child: Padding( padding: const 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, ), ), SizedBox(width: 16.0), ProductDetail(product : product) ], ), ), ); } }
 
screens/home/home_screen.dart
import 'package:carrot_market_ui/model/product.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'components/product_item.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Row( children: [ Text("좌동"), SizedBox(width: 4.0), Icon( CupertinoIcons.chevron_down, size: 15.0, ), ], ), actions: [ IconButton(icon: Icon(CupertinoIcons.search), onPressed: () {}), IconButton(icon: Icon(CupertinoIcons.list_dash), onPressed: () {}), IconButton(icon: Icon(CupertinoIcons.bell), onPressed: () {}), ], bottom: PreferredSize( preferredSize: Size.fromHeight(0.5), child: Divider(thickness: 0.5, height: 0.5, color: Colors.grey), ), ), body: ListView.separated( separatorBuilder: (context, index) => Divider( height: 0, indent: 16, endIndent: 16, color: Colors.grey, ), itemBuilder: (context, index) { return ProductItem( product: productList[index], ); }, itemCount: productList.length, ), ); } }
 
 
notion image
 

5. MyCarrotScreen 만들기

 
model/icon_menu.dart
import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class IconMenu { final String title; final IconData iconData; IconMenu({required this.title, required this.iconData}); } final List<IconMenu> iconMenu1 = [ IconMenu(title: "내 동네 설정", iconData: FontAwesomeIcons.mapMarkerAlt), IconMenu(title: "동네 인증하기", iconData: FontAwesomeIcons.compressArrowsAlt), IconMenu(title: "내 동네 설정", iconData: FontAwesomeIcons.tag), IconMenu(title: "내 동네 설정", iconData: FontAwesomeIcons.borderAll), ]; final List<IconMenu> iconMenu2 = [ IconMenu(title: "동네생활 글",iconData: FontAwesomeIcons.edit), IconMenu(title: "동네생활 댓글",iconData: FontAwesomeIcons.edit), IconMenu(title: "동네생활 주제 목록",iconData: FontAwesomeIcons.star), ]; final List<IconMenu> iconMenu3 = [ IconMenu(title: "비즈프로필 관리", iconData: FontAwesomeIcons.store), IconMenu(title: "지역광고", iconData: FontAwesomeIcons.bullhorn), ];
 
my_carrot/components/my_carrot_screen.dart
import 'package:flutter/material.dart'; import '../../model/icon_menu.dart'; import 'components/card_item_menu.dart'; import 'components/my_header.dart'; class MyCarrotScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[100], appBar: AppBar( title: Text("나의 당근"), actions: [ IconButton(icon: Icon(Icons.settings), onPressed: () {}), ], bottom: PreferredSize( preferredSize: Size.fromHeight(0.5), child: Divider(thickness: 0.5, height: 0.5, color: Colors.grey), ), ), body: ListView( children: [ MyCarrotHeader(), SizedBox(height: 8.0), CardIconMenu(iconMenuList: iconMenu1), SizedBox(height: 8.0), CardIconMenu(iconMenuList: iconMenu2), SizedBox(height: 8.0), CardIconMenu(iconMenuList: iconMenu3), ], ), ); } }
 
screens/my_carrot/components/my_carrot_header.dart
import 'package:carrot_market_ui/theme.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class MyCarrotHeader extends StatelessWidget { const MyCarrotHeader({super.key}); @override Widget build(BuildContext context) { return Card( elevation: 0.5, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)), child: Column( children: [ _buildProfileRow(), SizedBox(height: 30), _buildProfileButton(), SizedBox(height: 30), Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildRoundTextButton('판매내역', FontAwesomeIcons.receipt), _buildRoundTextButton('구매내역', FontAwesomeIcons.shoppingBag), _buildRoundTextButton('관심목록', FontAwesomeIcons.heart), ], ), ), ], ), ); } Widget _buildRoundTextButton(String title, IconData iconData) { return Column( children: [ Container( width: 60, height: 60, decoration: BoxDecoration( borderRadius: BorderRadius.circular(30.0), color: Color.fromRGBO(255, 226, 208, 1), border: Border.all(color: Color(0xFFD4D5DD), width: 0.5)), child: Icon( iconData, color: Colors.orange, ), ), SizedBox(height: 10), Text( title, style: textTheme().titleMedium, ) ], ); } Widget _buildProfileButton() { return InkWell( onTap: () {}, child: Container( decoration: BoxDecoration( border: Border.all( color: Color(0xFFD4D5DD), width: 1.0, ), borderRadius: BorderRadius.circular(6.0), ), height: 45, child: Text( "프로필 보기", style: textTheme().titleMedium, ), ), ); } Widget _buildProfileRow() { return Row( children: [ Stack( children: [ SizedBox( width: 65, height: 65, child: ClipRRect( borderRadius: BorderRadius.circular(32.5), child: Image.network( "https://picsum.photos/200/100", fit: BoxFit.cover, ), ), ), Positioned( bottom: 0, right: 0, child: Container( width: 20, height: 20, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: Colors.grey[100]), child: Icon( Icons.camera_alt_outlined, size: 15, ), ), ) ], ), SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("developer",style: textTheme().displayMedium), SizedBox(height: 10), Text("좌동 #00912"), ], ), ], ); } } import 'package:carrot_market_ui/theme.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class MyCarrotHeader extends StatelessWidget { const MyCarrotHeader({super.key}); @override Widget build(BuildContext context) { return Card( elevation: 0.5, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)), child: Column( children: [ _buildProfileRow(), SizedBox(height: 30), _buildProfileButton(), SizedBox(height: 30), Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildRoundTextButton('판매내역', FontAwesomeIcons.receipt), _buildRoundTextButton('구매내역', FontAwesomeIcons.shoppingBag), _buildRoundTextButton('관심목록', FontAwesomeIcons.heart), ], ), ), ], ), ); } Widget _buildRoundTextButton(String title, IconData iconData) { return Column( children: [ Container( width: 60, height: 60, decoration: BoxDecoration( borderRadius: BorderRadius.circular(30.0), color: Color.fromRGBO(255, 226, 208, 1), border: Border.all(color: Color(0xFFD4D5DD), width: 0.5)), child: Icon( iconData, color: Colors.orange, ), ), SizedBox(height: 10), Text( title, style: textTheme().titleMedium, ) ], ); } Widget _buildProfileButton() { return InkWell( onTap: () {}, child: Container( decoration: BoxDecoration( border: Border.all( color: Color(0xFFD4D5DD), width: 1.0, ), borderRadius: BorderRadius.circular(6.0), ), height: 45, child: Text( "프로필 보기", style: textTheme().titleMedium, ), ), ); } Widget _buildProfileRow() { return Row( children: [ Stack( children: [ SizedBox( width: 65, height: 65, child: ClipRRect( borderRadius: BorderRadius.circular(32.5), child: Image.network( "https://picsum.photos/200/100", fit: BoxFit.cover, ), ), ), Positioned( bottom: 0, right: 0, child: Container( width: 20, height: 20, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: Colors.grey[100]), child: Icon( Icons.camera_alt_outlined, size: 15, ), ), ) ], ), SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("developer",style: textTheme().displayMedium), SizedBox(height: 10), Text("좌동 #00912"), ], ), ], ); } }
 
screens/my_carrot/componets/card_item_menu.dart
import 'package:carrot_market_ui/theme.dart'; import 'package:flutter/material.dart'; import '../../../model/icon_menu.dart'; class CardIconMenu extends StatelessWidget { final List<IconMenu> iconMenuList; CardIconMenu({required this.iconMenuList}); @override Widget build(BuildContext context) { return Card( elevation: 0.5, margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)), child: Column( children: List.generate( iconMenuList.length, (index) => _buildRowIconItem( iconMenuList[0].title, iconMenuList[0].iconData), ), ), ); } Widget _buildRowIconItem(String title, IconData iconData) { return Container( height: 50, child: Row( children: [ Icon(iconData, size: 17), SizedBox(width: 20), Text( title, style: textTheme().titleMedium, ) ], ), ); } }
notion image

6. chatting_screen 페이지 만들기

 
model/chatting_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", ) ];
 
 
screens/chatting_screen.dart
import 'package:carrot_market_ui/model/chatting_message.dart'; import 'package:flutter/material.dart'; import '../../components/appbar_preferred_size.dart'; import 'components/chat_container.dart'; class ChattingScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("채팅"), bottom: appBarBottomLine(), ), body: ListView( children: List.generate( chatMessageList.length, (index) => ChatContainer(chatMessage: chatMessageList[index])), ), ); } }
 
screens/components/chat_container.dart
import 'package:carrot_market_ui/components/image_container.dart'; import 'package:carrot_market_ui/model/chatting_message.dart'; import 'package:carrot_market_ui/theme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class ChatContainer extends StatelessWidget { final ChatMessage chatMessage; const ChatContainer({ Key? key, required this.chatMessage, }) : super(key: key); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( border: Border(bottom: BorderSide(color: Colors.grey, width: 0.5)), ), height: 100, child: Row( children: [ ImageContainer( width: 50, height: 50, borderRadius: 25, imageUrl: chatMessage.profileImage, ), SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Spacer(), Text.rich( TextSpan(children: [ TextSpan( text: chatMessage.sender, style: textTheme().bodyLarge), TextSpan(text: chatMessage.location), TextSpan(text: "•${chatMessage.sendDate}"), ]), ), Spacer(), Text( chatMessage.message, style: textTheme().bodyLarge, overflow: TextOverflow.ellipsis, ), Spacer(), ], ), ), Visibility(visible: chatMessage.imageUri != null, child: Padding( padding: const EdgeInsets.only(left: 8.0), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: ImageContainer( width: 50, height: 50, borderRadius: 8, imageUrl: chatMessage.imageUri ?? '', ), ), ),) ], ), ); } }
 
components/appbar_preferred_size.dart
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; // 앱바 하단에 선을 추가하는 기능 PreferredSize appBarBottomLine() { final height = 0.5; return PreferredSize( preferredSize: Size.fromHeight(height), child: Divider( thickness: height, height: height, color: Colors.grey, ), ); }
 
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; 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, ), ); } }
 
notion image
 

7. neighborhood_life 페이지 만들기

 
model/neighborhood_life.dart
class NeighborhoodLife { final String category; final String profileImgUri; final String userName; final String location; final String content; final String contentImgUri; final int commentCount; final int authCount; final String date; NeighborhoodLife({ required this.category, required this.profileImgUri, required this.userName, required this.location, required this.content, required this.contentImgUri, required this.commentCount, required this.authCount, required this.date, }); } // 샘플 데이터 1 String lifeTitle = '이웃과 함께 만드는 봄 간식 지도 마음까지 따듯해지는 봄 간식을 만나보세요.'; // 샘플 데이터 2 List<NeighborhoodLife> neighborhoodLifeList = [ NeighborhoodLife( category: '우리동네질문', profileImgUri: 'https://picsum.photos/id/871/200/300?grayscale', // TODO 06 수정 userName: '헬로비비', location: '좌동', content: '예민한 개도 미용할 수 있는 곳이나 동물 병원 어디 있을까요?\n' '내일 유기견을 데려오기로 했는데 아직 성향을 잘 몰라서 걱정이 돼요 ㅜㅜ.', contentImgUri: 'https://picsum.photos/id/872/200/300?grayscale', commentCount: 11, authCount: 3, date: '3시간전', ), NeighborhoodLife( category: '우리동네소식', profileImgUri: 'https://picsum.photos/id/873/200/100?grayscale', userName: '당근토끼', location: '우동', content: '이명 치료 잘 아시는 분 있나요?', contentImgUri: 'https://picsum.photos/id/874/200/100?grayscale', commentCount: 2, authCount: 1, date: '1일전', ), NeighborhoodLife( category: '분실', profileImgUri: 'https://picsum.photos/id/875/200/100?grayscale', userName: 'flutter', location: '중동', content: '롯데캐슬 방향으로 재래시장 앞쪽 지나 혹시 에어팟 오른쪽 주우신 분 있나요ㅜㅜ', contentImgUri: '', commentCount: 11, authCount: 8, date: '1일전', ), NeighborhoodLife( category: '우리동네질문', profileImgUri: 'https://picsum.photos/id/880/200/100', userName: '구름나드리', location: '우동', content: '밤부터 새벽까지 하던 토스트 아저씨 언제 다시 오나요ㅜㅠ', contentImgUri: '', commentCount: 0, authCount: 7, date: '3일전', ), NeighborhoodLife( category: '우리동네질문', profileImgUri: 'https://picsum.photos/id/730/200/100?grayscale', userName: '아는형', location: '만덕동', content: '아니 이 시간에 마이크 들고 노래하는 사람은 정상인가요?', contentImgUri: 'https://picsum.photos/id/885/200/100', commentCount: 11, authCount: 2, date: '5일전', ), ];
 
screens/neighborhood_life_screen.dart
import 'package:carrot_market_ui/components/appbar_preferred_size.dart'; import 'package:carrot_market_ui/model/neighborhood_life.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'components/life_body.dart'; import 'components/life_header.dart'; class NeighborhoodLifeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("동네생활"), actions: [ IconButton(icon: Icon(CupertinoIcons.search), onPressed: () {}), IconButton( icon: Icon(CupertinoIcons.plus_rectangle_on_rectangle), onPressed: () {}), IconButton(icon: Icon(CupertinoIcons.bell), onPressed: () {}), ], bottom: appBarBottomLine(), ), body: ListView( children: [ LifeHeader(), ...List.generate( neighborhoodLifeList.length, (index) => Padding(padding: const EdgeInsets.only(bottom: 8.0), child: LifeBody( neighborhoodLife: neighborhoodLifeList[index], ),), ), ], ), ); } }
 
screens/neighborhood_life/components/life_header.dart
import 'package:carrot_market_ui/components/image_container.dart'; import 'package:carrot_market_ui/model/neighborhood_life.dart'; import 'package:carrot_market_ui/theme.dart'; import 'package:flutter/material.dart'; class LifeHeader extends StatelessWidget { const LifeHeader({super.key}); @override Widget build(BuildContext context) { return Card( margin: EdgeInsets.only(bottom: 12.0), elevation: 0.5, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)), child: Row( children: [ ImageContainer( borderRadius: 6.0, imageUrl: "https://picsum.photos/id/780/200/1000", width: 45.0, height: 45.0, ), SizedBox(width: 16.0), Expanded( child: Text( lifeTitle, style: textTheme().bodyLarge, maxLines: 2, overflow: TextOverflow.ellipsis, )) ], ), ); } }
 
screens/neighborhood_life/components/life_body.dart
import 'package:carrot_market_ui/components/image_container.dart'; import 'package:carrot_market_ui/model/neighborhood_life.dart'; import 'package:carrot_market_ui/theme.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class LifeBody extends StatelessWidget { final NeighborhoodLife neighborhoodLife; LifeBody({required this.neighborhoodLife}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: Colors.white, border: Border( bottom: BorderSide(width: 0.5, color: Color(0xFFD4D5DD)), ), ), child: Column( children: [ _buildTop(), _buildWriter(), _buildWrting(), _buildImage(), Divider( height: 1, thickness: 1, color: Colors.grey[300], ), _buildTail(neighborhoodLife.commentCount), ], ), ); } Padding _buildTop() { return Padding( padding: EdgeInsets.symmetric( vertical: 16, horizontal: 16, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( decoration: BoxDecoration( shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(4)), color: Color.fromRGBO(247, 247, 247, 1), ), child: Text(neighborhoodLife.category, style: textTheme().displayMedium), ), Text( neighborhoodLife.date, style: textTheme().displayMedium, ) ], ), ); } Padding _buildWriter() { return Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ ImageContainer( width: 30, height: 30, borderRadius: 15, imageUrl: neighborhoodLife.profileImgUri, ), Text.rich( TextSpan( children: [ TextSpan( text: "${neighborhoodLife.userName}", style: textTheme().bodyLarge), TextSpan( text: "${neighborhoodLife.location}", style: textTheme().bodyMedium), TextSpan( text: "인증 ${neighborhoodLife.authCount}", style: textTheme().bodyMedium), ], ), ), ], ), ); } Padding _buildWrting() { return Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Align( alignment: Alignment.centerLeft, child: Text( neighborhoodLife.content, style: textTheme().bodyLarge, maxLines: 3, overflow: TextOverflow.ellipsis, textAlign: TextAlign.start, ), ), ); } Visibility _buildImage() { return Visibility( visible: neighborhoodLife.contentImgUri != "", child: Padding( padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), child: Image.network( neighborhoodLife.contentImgUri, height: 200, width: double.infinity, fit: BoxFit.cover, ), ), ); } Padding _buildTail(int commentCount) { return Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon( FontAwesomeIcons.smile, color: Colors.grey, size: 22, ), SizedBox(width: 8), Text( "공감하기", style: TextStyle(fontSize: 16, color: Colors.black), ), Icon( FontAwesomeIcons.commentAlt, color: Colors.grey, size: 22, ), SizedBox(width: 8), Text( "${"댓글쓰기"} $commentCount", style: TextStyle(fontSize: 16, color: Colors.black), ) ], ), ); } }
notion image
 

8. near_me 페이지 만들기

 
model/recommend_store.dart
class RecommendStore { String storeName; String location; String description; int commentCount; int likeCount; String comment; String commentUser; List storeImages; RecommendStore( {required this.storeName, required this.location, required this.description, required this.commentCount, required this.likeCount, required this.comment, required this.commentUser, required this.storeImages}); } final List searchKeyword = ['인테리어', '학원', '이사', '카페', '용달', '네일', '에어콘']; List<RecommendStore> recommendStoreList = [ RecommendStore( storeName: '네일가게', location: '좌동', description: '꼼꼼한시술로 유지력높은 네일샵입니다. 좌동에 위치하고 있습니다.', commentCount: 1, likeCount: 8, commentUser: '이엘리아님', comment: '너무편하게 시술해주셔서 잠들었었네요 직모에 짧은 눈썹이라 펌이 잘 안되는 타입인데 너무 이쁘게 됐네요', storeImages: [ 'https://github.com/flutter-coder/ui_images/blob/master/carrot_store_1_1.jpg?raw=true', 'https://github.com/flutter-coder/ui_images/blob/master/carrot_store_1_2.jpg?raw=true', ]), RecommendStore( storeName: '아미아미주먹밥', location: '우동', description: '2012년 오픈한 해운대도서관 분관쪽에 위치하고 있습니다.', commentCount: 2, likeCount: 2, commentUser: '둘리님', comment: '도서관이 근처라 시험기간마다 이용하는데 너무 좋습니다.', storeImages: [ 'https://github.com/flutter-coder/ui_images/blob/master/carrot_store_2_1.jpg?raw=true', 'https://github.com/flutter-coder/ui_images/blob/master/carrot_store_2_2.jpg?raw=true', ]), RecommendStore( storeName: '영어원어민 논술', location: '중동', description: '원어민 영어 고급논술&디베이트&스피치 전문', commentCount: 7, likeCount: 1, commentUser: 'kkglo님', comment: '저희 아들은 학원 주입식이 아닌 살아있는 영어 수업을 할 수 있어서 너무 좋네요', storeImages: [ 'https://github.com/flutter-coder/ui_images/blob/master/carrot_store_3_1.jpg?raw=true', 'https://github.com/flutter-coder/ui_images/blob/master/carrot_store_3_2.jpg?raw=true', ]), RecommendStore( storeName: '삘레빙/코인워시 우동점', location: '우동', description: '빨래방 / 크린토비아 코인워시 우동점 신설했습니다 많은 이용 바랍니다.', commentCount: 11, likeCount: 5, commentUser: '코인님', comment: '처음 방문때 건조기 무료로 서비스 해주셔서 너무 감사 하네요. 앞으로 자주 이용 합니다.', storeImages: [ 'https://github.com/flutter-coder/ui_images/blob/master/carrot_store_4_1.jpg?raw=true', 'https://github.com/flutter-coder/ui_images/blob/master/carrot_store_4_2.jpg?raw=true', ]) ];
 
screens/near_me/components/search_text_field.dart
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(), filled: true, fillColor: Colors.grey[200], prefixIcon: Icon( CupertinoIcons.search, color: Colors.grey, ), contentPadding: EdgeInsets.only(left: 0, bottom: 15, top: 15, right: 0), hintText: "좌동 주변 가게를 찾아보세요.", hintStyle: TextStyle(fontSize: 18), ), ), ); } OutlineInputBorder _buildOutLineInputBorder() { return OutlineInputBorder( borderSide: BorderSide(width: 0.5, color: Color(0xFFD4D5DD)), borderRadius: BorderRadius.circular(8.0), ); } }
 
round_border_text.dart
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: EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(18.0), border: Border.all(width: 0.5, color: Colors.grey), ), child: Text( title, textAlign: TextAlign.center, style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), ), ); } }
 
bottom_title_icon.dart
import 'package:flutter/material.dart'; class BottomTitleIcon extends StatelessWidget { final IconData iconData; final String title; BottomTitleIcon({required this.iconData, required this.title}); @override Widget build(BuildContext context) { return Container( width: 80, child: Column( children: [ Icon(iconData, size: 30), Padding( padding: EdgeInsets.only(top: 8), child: Text( title, style: TextStyle(fontSize: 14, color: Colors.black), ), ), ], ), ); } }
 
store_item.dart
import 'package:carrot_market_ui/theme.dart'; import 'package:flutter/material.dart'; import '../../../model/recommend_store.dart'; class StoreItem extends StatelessWidget { final RecommendStore recommendStore; const StoreItem({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), SizedBox(width: 2), buildClipRRect(topRight: 10), ], ), Padding( padding: EdgeInsets.all(16), child: Text.rich( TextSpan( children: [ TextSpan( text: "${recommendStore.storeName}", style: textTheme().displayLarge), TextSpan(text: "${recommendStore.location}"), ], ), ), ), SizedBox(height: 8), Text( "${recommendStore.description}", maxLines: 1, overflow: TextOverflow.ellipsis, style: textTheme().titleMedium, ), SizedBox(height: 8), Container( margin: EdgeInsets.only(left: 16, right: 16, bottom: 16), padding: EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular((10))), child: Text.rich( TextSpan( children: [ TextSpan( text: "후기 ${recommendStore.commentCount}", 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( recommendStore.storeImages[0], width: 143, height: 100, fit: BoxFit.cover, ), ); } }
 
near_me_screen.dart
import 'package:carrot_market_ui/components/appbar_preferred_size.dart'; import 'package:carrot_market_ui/model/recommend_store.dart'; import 'package:carrot_market_ui/screens/near_me/components/store_item.dart'; import 'package:carrot_market_ui/theme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'components/bottom_title_icon.dart'; import 'components/round_border_text.dart'; import 'components/search_text.field.dart'; class NearMeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("내 근처"), actions: [ IconButton(icon: Icon(CupertinoIcons.pencil), onPressed: () {}), IconButton(icon: Icon(CupertinoIcons.bell), onPressed: () {}), ], bottom: appBarBottomLine(), ), body: ListView( children: [ SizedBox(height: 10), Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: SearchTextField(), ), SizedBox( height: 66, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: searchKeyword.length, itemBuilder: (context, index) { return Center( child: RoundBorderText( title: searchKeyword[index], position: index, ), ); }, ), ), Divider( color: Colors.grey[100], thickness: 10.0, ), Padding( padding: EdgeInsets.only(left: 16, top: 30), child: Wrap( alignment: WrapAlignment.start, spacing: 22.0, runSpacing: 30, children: [ BottomTitleIcon(title: '구인구직', iconData: FontAwesomeIcons.user), BottomTitleIcon( title: '과외/클래스', iconData: FontAwesomeIcons.edit), BottomTitleIcon( title: '농수산물', iconData: FontAwesomeIcons.appleAlt), BottomTitleIcon(title: '부동산', iconData: FontAwesomeIcons.hotel), BottomTitleIcon(title: '중고차', iconData: FontAwesomeIcons.car), BottomTitleIcon( title: '전시/행사', iconData: FontAwesomeIcons.chessBishop) ], ), ), SizedBox(height: 50), Padding( padding: EdgeInsets.only(left: 16.0), child: Text("이웃들의 추천 가게", style: textTheme().displayMedium), ), SizedBox(height: 20), Container( height: 288, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: recommendStoreList.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only(left: 16), child: StoreItem( recommendStore: recommendStoreList[index], ), ); }, ), ), SizedBox(height: 40), ], ), ); } }
notion image
Share article
RSSPowered by inblog