원하는 화면

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 라이브러리

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