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