Chapter 05. Flutter_Recipe

김미숙's avatar
Jun 10, 2025
Chapter 05. Flutter_Recipe

출처

만들면서 배우는 플러터 앱 프로그래밍 7가지 모바일 앱 UI 제작 & RiverPod 상태 관리
 

Project 생성

notion image

Setting

notion image
formatter: trailing_commas: preserve

5장에 있는 사진 가져오기

사진만 복사
사진만 복사
assets 폴더 생성 → 사진 붙여넣기
assets 폴더 생성 → 사진 붙여넣기
pubspec.yaml 설정 / 주석풀면 띄어쓰기가 하나 더 되있으므로 주의하기
pubspec.yaml 설정 / 주석풀면 띄어쓰기가 하나 더 되있으므로 주의하기

라이브러리 가져오기

notion image
플러터 관련 모든 라이브러리를 다운 받는 곳
notion image
shift+insert+enter → ctrl + v 금지
shift+insert+enter → ctrl + v 금지
notion image
notion image
현재 이 상태가 문제 없는지 Test
notion image
test 파일 삭제
 

Preview

  • AppBar 위젯
  • Conctainer 위젯
  • Icon 위젯
  • ClipRRect 위젯
  • AspectRatio 위젯
  • ListView 위젯
  • Font 변경
 

기본 구조 - Default

import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: MainPage(), ); } } class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Placeholder(), ); } }
 

Recipe App의 화면 구조

notion image
 

Layout 잡기

1. MainPage - AppBar

class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Placeholder(), ); } }
notion image

AppBar에 Icon 넣기

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: MainPage(), ); } } class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( actions: [ Icon(Icons.search), Icon(CupertinoIcons.heart)], ), body: Placeholder(), ); } }
notion image

AppBar 간격 주기

SizedBox(width: 16) → 기본값
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: MainPage(), ); } } class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( actions: [ Icon(Icons.search), SizedBox(width: 16), Icon(CupertinoIcons.heart), SizedBox(width: 16), ], ), body: Placeholder(), ); } }
notion image

AppBar 함수로 빼기

notion image
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: MainPage(), ); } } class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: Placeholder(), ); } AppBar _appBar() { return AppBar( actions: [ Icon(Icons.search), SizedBox(width: 16), Icon(CupertinoIcons.heart), SizedBox(width: 16), ], ); } }
 

MainPage - ListView

class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: ListView( children: [ Text("Recipes"), Container( child: Column( children: [ Icon(Icons.food_bank), Text("ALL"), ], ), ), ], ), ); }
notion image
 

ListView Class 만들기

문서
notion image
예제코드
ListView( padding: const EdgeInsets.all(8), children: <Widget>[ Container( height: 50, color: Colors.amber[600], child: const Center(child: Text('Entry A')), ), Container( height: 50, color: Colors.amber[500], child: const Center(child: Text('Entry B')), ), Container( height: 50, color: Colors.amber[100], child: const Center(child: Text('Entry C')), ), ], )
디자인 할 때는 순수하게 아이콘만 넣는게 좋다
여백까지 같이 줘버리면 다른 곳에서 재사용이 불가능하다
선택적 매개변수를 넣는 방법도 있으나 비추천 → 매개변수가 많아지고 복잡해짐
notion image
MenuItem
class MenuItem extends StatelessWidget { IconData mIcon; var mText; MenuItem(this.mIcon, this.mText); @override Widget build(BuildContext context) { return Container( child: Column( children: [ Icon(mIcon), Text(mText), ], ), ); } }
MainPage - ListView
class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: ListView( children: [ Text("Recipes"), Row( children: [ MenuItem(Icons.food_bank, "ALL"), MenuItem(Icons.emoji_food_beverage, "coffee"), MenuItem(Icons.fastfood, "burger"), MenuItem(Icons.local_pizza, "pizza"), ], ), ], ), ); }
notion image
MenuItem 사이 여백 추가 - SizedBox()
class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: ListView( children: [ Text("Recipes"), Row( children: [ MenuItem(Icons.food_bank, "ALL"), SizedBox(width: 25), MenuItem(Icons.emoji_food_beverage, "coffee"), SizedBox(width: 25), MenuItem(Icons.fastfood, "burger"), SizedBox(width: 25), MenuItem(Icons.local_pizza, "pizza"), ], ), ], ), ); }
notion image
 

Menu 위젯 만들기

notion image
Menu
class Menu extends StatelessWidget { @override Widget build(BuildContext context) { return Row( children: [ MenuItem(Icons.food_bank, "ALL"), SizedBox(width: 25), MenuItem(Icons.emoji_food_beverage, "coffee"), SizedBox(width: 25), MenuItem(Icons.fastfood, "burger"), SizedBox(width: 25), MenuItem(Icons.local_pizza, "pizza"), ], ); } }
MainPage - ListView
class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: ListView( children: [ Text("Recipes"), Menu(), ], ), ); }
 

Image 위젯 만들기

class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: ListView( children: [ Text("Recipes"), Menu(), Column( children: [ Image.asset("assets/burger.jpeg"), Text("title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), ], ), ], ), ); }
notion image
ListItem 위젯
class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( children: [ Image.asset("assets/$title.jpeg"), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), ], ); } }
MainPage - ListView
class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: ListView( children: [ Text("Recipes"), Menu(), ListItem("coffee"), ListItem("burger"), ListItem("pizza"), ], ), ); }
notion image
notion image
 

만든 위젯들 파일화 + MainPage 위젯 Class화

notion image

list_item

import 'package:flutter/material.dart'; class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( children: [ Image.asset("assets/$title.jpeg"), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), ], ); } }

m_title

import 'package:flutter/material.dart'; class MTitle extends StatelessWidget { @override Widget build(BuildContext context) { return Text("Recipes"); } }

menu

import 'package:flutter/material.dart'; import 'package:flutter_recipe_app_2/component/menu_item.dart'; class Menu extends StatelessWidget { @override Widget build(BuildContext context) { return Row( children: [ MenuItem(Icons.food_bank, "ALL"), SizedBox(width: 25), MenuItem(Icons.emoji_food_beverage, "coffee"), SizedBox(width: 25), MenuItem(Icons.fastfood, "burger"), SizedBox(width: 25), MenuItem(Icons.local_pizza, "pizza"), ], ); } }

menu_item

import 'package:flutter/material.dart'; class MenuItem extends StatelessWidget { IconData mIcon; var mText; MenuItem(this.mIcon, this.mText); @override Widget build(BuildContext context) { return Container( child: Column( children: [ Icon(mIcon), Text(mText), ], ), ); } }

main

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_recipe_app_2/component/list_item.dart'; import 'package:flutter_recipe_app_2/component/menu.dart'; import 'component/m_title.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: MainPage(), ); } } class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: ListView( children: [ MTitle(), Menu(), ListItem("coffee"), ListItem("burger"), ListItem("pizza"), ], ), ); } AppBar _appBar() { return AppBar( actions: [ Icon(Icons.search), SizedBox(width: 16), Icon(CupertinoIcons.heart), SizedBox(width: 16), ], ); } }
 

Design

ListView 왼 쪽에 마진

class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: ListView( children: [ MTitle(), Menu(), ListItem("coffee"), ListItem("burger"), ListItem("pizza"), ], ), ), ); }
상단의 아이콘과 위치가 잘 맞아야함
상단의 아이콘과 위치가 잘 맞아야함
 

heart 아이콘에 색깔주기

AppBar _appBar() { return AppBar( actions: [ Icon(Icons.search), SizedBox(width: 16), Icon(CupertinoIcons.heart, color: Colors.redAccent), SizedBox(width: 16), ], ); } }
notion image
 

MTitle(Recipes) Design

font 샘플링 할때는 부분 복사가 아닌 전체 복사 / 동일한 환경에서 샘플링
  • google_font 라이브러리 사용해서 font 바꾸기
import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; class MTitle extends StatelessWidget { @override Widget build(BuildContext context) { return Text( "Recipes", style: GoogleFonts.patuaOne(textStyle: TextStyle(fontSize: 30)), ); } }
notion image
 

Menu Item 디자인

  1. Container에 width, height 주기
import 'package:flutter/material.dart'; class MenuItem extends StatelessWidget { IconData mIcon; var mText; MenuItem(this.mIcon, this.mText); @override Widget build(BuildContext context) { return Container( width: 60, height: 80, child: Column( children: [ Icon(mIcon), Text(mText), ], ), ); } }
notion image
 
  1. Container 색깔, 모양주기
import 'package:flutter/material.dart'; class MenuItem extends StatelessWidget { IconData mIcon; var mText; MenuItem(this.mIcon, this.mText); @override Widget build(BuildContext context) { return Container( width: 60, height: 80, decoration: BoxDecoration( border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(30), ), child: Column( children: [ Icon(mIcon), Text(mText), ], ), ); } }
notion image
 
  1. 아이콘 정렬, 색깔, 아이콘 사이 여백
import 'package:flutter/material.dart'; class MenuItem extends StatelessWidget { IconData mIcon; var mText; MenuItem(this.mIcon, this.mText); @override Widget build(BuildContext context) { return Container( width: 60, height: 80, decoration: BoxDecoration( border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(30), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( mIcon, color: Colors.redAccent, size: 30, ), SizedBox(height: 5), Text(mText), ], ), ); } }
notion image
 

Mtitle과 ListItem 사이 여백주기

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_recipe_app_2/component/list_item.dart'; import 'package:flutter_recipe_app_2/component/m_title.dart'; import 'package:flutter_recipe_app_2/component/menu.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: MainPage(), ); } } class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), body: Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: ListView( children: [ MTitle(), SizedBox(height: 20), Menu(), SizedBox(height: 20), ListItem("coffee"), ListItem("burger"), ListItem("pizza"), ], ), ), ); } AppBar _appBar() { return AppBar( actions: [ Icon(Icons.search), SizedBox(width: 16), Icon(CupertinoIcons.heart, color: Colors.redAccent), SizedBox(width: 16), ], ); } }
notion image
 

이미지 비율 맞추기 + 자르기

  1. 이미지에 속성주기
import 'package:flutter/material.dart'; class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( children: [ Image.asset( "assets/$title.jpeg", fit: BoxFit.cover, ), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), ], ); } }
  1. 이미지 자르기
notion image
import 'package:flutter/material.dart'; class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( children: [ ClipRRect( child: Image.asset( "assets/$title.jpeg", fit: BoxFit.cover, ), borderRadius: BorderRadius.circular(20), ), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), ], ); } }
notion image
  1. 이미지 비율 맞추기
notion image
import 'package:flutter/material.dart'; class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( children: [ AspectRatio( aspectRatio: 2 / 1, child: ClipRRect( child: Image.asset( "assets/$title.jpeg", fit: BoxFit.cover, ), borderRadius: BorderRadius.circular(20), ), ), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), ], ); } }
notion image

ListItem 폰트 변경 + 글자 정렬 + ListItem 간 여백주기

  1. 글자 정렬
import 'package:flutter/material.dart'; class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: 2 / 1, child: ClipRRect( child: Image.asset( "assets/$title.jpeg", fit: BoxFit.cover, ), borderRadius: BorderRadius.circular(20), ), ), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), ], ); } }
notion image
  1. ListItem 간 여백주기
import 'package:flutter/material.dart'; class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: 2 / 1, child: ClipRRect( child: Image.asset( "assets/$title.jpeg", fit: BoxFit.cover, ), borderRadius: BorderRadius.circular(20), ), ), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), SizedBox(height: 20), ], ); } }
notion image
  1. 폰트, 폰트 색상 변경
import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: 2 / 1, child: ClipRRect( child: Image.asset( "assets/$title.jpeg", fit: BoxFit.cover, ), borderRadius: BorderRadius.circular(20), ), ), Text( "$title", style: GoogleFonts.patuaOne(textStyle: TextStyle(fontSize: 20)), ), Text( "Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back.", style: TextStyle(color: Colors.grey, fontSize: 12), ), SizedBox(height: 20), ], ); } }
notion image
notion image
Share article

parangdajavous