[플러터] 9. 레시피 앱 만들기

KangHo Lee's avatar
Dec 23, 2024
[플러터] 9. 레시피 앱 만들기

원하는 화면

notion image

1. 화면 구조를 안드로이드로 설정

class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, // 우측 상단 디버그 배너 안 보이게 설정 home: RecipePage(), theme: ThemeData( // 폰트 전역 설정 textTheme: GoogleFonts.patuaOneTextTheme(), ), // ThemeData ); // MaterialApp } }
  • debugShowCheckedModeBanner → 우측 상단 debug 표시 제거
  • theme → ThemeData → textTheme
    • 화면 전체 텍스트 설정

Dart 라이브러리

notion image
  • google_fonts 라이브러리
    • pubspec.yaml 에서 dependencies에 google_fonts: ^6.2.1 추가 후 pub get
    • dependencies 추가했으면 미리 보기 재실행 해야 반영됩니다.
    • With Flutter를 터미널에 입력해도 됩니다.
      • With Dart를 쓰지 않도록 주의

2. AppBar 설정

AppBar 구성
AppBar 구성
RecipePage
class RecipePage extends StatelessWidget { const RecipePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: _appbar(), // 메서드 추출 사용 body: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: ListView( // listview는 스크롤이 가능, scrollDirection: Axis.horizontal로 스크롤 방향 설정 가능 // column -> 디폴트 가운데 정렬, ListView -> 디폴트 왼쪽 정렬 children: [ Title(), Menu(), RecipeItem("coffee"), RecipeItem("burger"), RecipeItem("pizza"), ], ), // ListView ), // Padding ); // Scaffold } AppBar _appbar() { // _ 는 private 표시 -> 여기만 쓸거니까 // 메서드 추출로 분리 return AppBar( actions: [ Icon(Icons.search), SizedBox(width: 16), Icon(CupertinoIcons.heart, color: Colors.redAccent), // 자동 완성을 해서 cupertino.dart import 되게, 필요한 속성은 ( ) 안에서 컨트롤 스페이스 SizedBox(width: 16), ], ); // AppBar } }
  • 메서드 추출 방법
    • 마우스 우클릭 → Refactor → Extract Method
    • 이 화면에서만 쓸 예정이라 _ 로 private 표시를 해줬습니다.
    • 여러 번 재사용하려면 Widget으로 만드는 게 좋습니다.
  • CupertinoIcons
    • 자동 완성을 하면 자동으로 import 해주기 때문에 좋습니다.
  • ListView
    • Column과 달리 스크롤 바를 생성시킵니다.
    • scrollDirection: Axis.horizontal로 스크롤 방향 설정 가능

3. 타이틀

Title
class Title extends StatelessWidget { const Title({ super.key, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 20), child: Text( "Recipes", // style: GoogleFonts.patuaOne(fontSize: 30), style: TextStyle(fontSize: 30), ), // Text ); // Padding } }
  • 재사용 가능한 위젯으로 분리하는 방법
    • 마우스 우클릭 → Refactor → Extract Flutter Widget

4. 메뉴 요소 모음

Menu
class Menu extends StatelessWidget { const Menu({ super.key, }); @override Widget build(BuildContext context) { return Row( children: [ // MenuItem -> Component MenuItem(mIcon: Icons.food_bank, mText: "ALL"), SizedBox(width: 25), MenuItem(mIcon: Icons.emoji_food_beverage, mText: "Coffee"), SizedBox(width: 25), MenuItem(mIcon: Icons.fastfood, mText: "Burger"), SizedBox(width: 25), MenuItem(mIcon: Icons.local_pizza, mText: "Pizza"), ], ); } }
MenuItem
// 위젯으로 분리 class MenuItem extends StatelessWidget { // final 선언 시 무조건 값을 받아야 한다. final mIcon; // m 추가해서 다른 Icon과 구별 final mText; // 순서 헷갈리니까 선택적 매개 변수로 -> required 추가해서 무조건 값을 넣도록 MenuItem({required this.mIcon, required this.mText}); @override Widget build(BuildContext context) { return Container( // <div> width: 60, height: 80, decoration: BoxDecoration( border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(30), ), // BoxDecoration child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(mIcon, color: Colors.redAccent, size: 30), SizedBox(height: 3), // ${} 쓰는 이유 -> 값이 없을 경우 공백으로 들어가서 null로 프로그램 터지는 거 방지 Text("${mText}", style: TextStyle(color: Colors.black87)), ], ), // Column ); // Container } }
  • 필드와 생성자 설정
    • final 선언으로 무조건 값을 받도록 설정했습니다.
    • 선택적 매개변수는 값을 받지 않을 수 있으니 required를 추가합니다.
  • mainAxisAlignment
    • Column에서는 Y축을 말하고 Row에선 X축을 말합니다.

5. 레서피 아이템

RecipeItem
class RecipeItem extends StatelessWidget { final text; RecipeItem(this.text); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 30), // AspectRatio Image -> 비율에 맞기 액자 만들기 AspectRatio( aspectRatio: 2 / 1, // 가로 세로 비율 child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Image.asset( // assets 알트 엔터 -> AspectRatio추가, 1번 더 -> ClipRRect추가 "assets/${text}.jpeg", fit: BoxFit.cover, ), // Image.asset ), // ClipRRect ), // AspectRatio SizedBox(height: 10), Text("made ${text}", style: TextStyle(fontSize: 20)), Text( "Have you ever made your own ${text}? Once you've tried a homemade ${text}, you'll never go back.", style: TextStyle(color: Colors.grey, fontSize: 12), ), // Text ], ); // Column } }

AspectRatio

  • 사진 액자 느낌입니다.
  • aspectRatio: 2 / 1
    • 가로 / 세로 비율을 설정합니다.
    • 화면 크기에 맞춰 동적으로 크기가 변해야 하므로 비율로 설정합니다.
  • fit: BoxFit.cover
    • AspectRatio와 쌍으로 많이 씁니다.
    • 원본 사진의 가로 세로 비율을 유지한 채로 지정한 영역에 사진을 맞춥니다.
    • 사진이 지정한 크기를 벗어나면 잘립니다.

전체 코드

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, // 우측 상단 디버그 배너 안 보이게 설정 home: RecipePage(), theme: ThemeData( // 폰트 전역 설정 textTheme: GoogleFonts.patuaOneTextTheme(), ), // ThemeData ); // MaterialApp } } class RecipePage extends StatelessWidget { const RecipePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: _appbar(), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: ListView( children: [ Title(), Menu(), RecipeItem("coffee"), RecipeItem("burger"), RecipeItem("pizza"), ], ), // ListView ), // Padding ); // Scaffold } AppBar _appbar() { return AppBar( actions: [ Icon(Icons.search), SizedBox(width: 16), Icon(CupertinoIcons.heart, color: Colors.redAccent), SizedBox(width: 16), ], ); // AppBar } } class RecipeItem extends StatelessWidget { final text; RecipeItem(this.text); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 30), AspectRatio( aspectRatio: 2 / 1, child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Image.asset( "assets/${text}.jpeg", fit: BoxFit.cover, ), // Image.asset ), // ClipRRect ), // AspectRatio SizedBox(height: 10), Text("made ${text}", style: TextStyle(fontSize: 20)), Text( "Have you ever made your own ${text}? Once you've tried a homemade ${text}, you'll never go back.", style: TextStyle(color: Colors.grey, fontSize: 12), ), // Text ], ); // Column } } class Menu extends StatelessWidget { const Menu({ super.key, }); @override Widget build(BuildContext context) { return Row( children: [ // MenuItem -> Component MenuItem(mIcon: Icons.food_bank, mText: "ALL"), SizedBox(width: 25), MenuItem(mIcon: Icons.emoji_food_beverage, mText: "Coffee"), SizedBox(width: 25), MenuItem(mIcon: Icons.fastfood, mText: "Buger"), SizedBox(width: 25), MenuItem(mIcon: Icons.local_pizza, mText: "Pizza"), ], ); } } class Title extends StatelessWidget { const Title({ super.key, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 20), child: Text( "Recipes", style: TextStyle(fontSize: 30), ), // Text ); // Padding } } // 위젯으로 분리 class MenuItem extends StatelessWidget { final mIcon; final mText; MenuItem({required this.mIcon, required this.mText}); @override Widget build(BuildContext context) { return Container( // <div> width: 60, height: 80, decoration: BoxDecoration( border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(30), ), // BoxDecoration child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(mIcon, color: Colors.redAccent, size: 30), SizedBox(height: 3), Text("${mText}", style: TextStyle(color: Colors.black87)), ], ), // Column ); // Container } }
Share article

devleekangho