레시피 앱 구조 살펴보기(스크롤)

Stupefyee's avatar
Dec 23, 2024
레시피 앱 구조 살펴보기(스크롤)

0. 페이지를 구현하기에 앞서

  • 페이지 내의 요소들을 모조리 한 곳에 몰아서 적으면 유지보수와 가독성이 떨어짐
    • void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: RecipePage(), // 메소드안에 페이지를 구현해 놓고 갈아끼우는 방식 ); } } class RecipePage extends StatelessWidget { const RecipePage({super.key}); @override Widget build(BuildContext context) { return const Scaffold(); } }

1. 뼈대 생성하기

1. 앱 제작시 뼈대를 먼저 만들기

  • 컴포넌트 제작 후 조정 시 무너질 가능성 증가
  • 또한 위젯마다 정렬방식이 달라 무너질 경우도 있음. 아래의 예시의 차이>> Column과 ListView
// 페이지 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: Column( // Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로 children: [ Text("Recipes"), Row( children: [ // Container >> html의 div와 비슷한 역할 Container( width: 60, height: 80, color: Colors.red, ), ], ), Placeholder(), Placeholder(), Placeholder(), ], ), ), ); } // 길어지니까 메소드 추출을 이용해 빼내기 AppBar _appbar() { return AppBar( leading: Text("리딩 영역"), title: Text("타이틀 영역"), // appbar의 액션 영역에 아이콘 넣기 actions: [ Icon(Icons.search), // 돋보기 아이콘 SizedBox(width: 16), // 여백용 Icon(CupertinoIcons.heart, color: Colors.redAccent), // 하트 아이콘 (CupertinoIcons >> 자동으로 들어가 있는 아이콘 외부 라이브러리) SizedBox(width: 16), ], ); }
Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류
Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류
// 페이지 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( // Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로 children: [ Text("Recipes"), Row( children: [ // Container >> html의 div와 비슷한 역할 Container( width: 60, height: 80, color: Colors.red, ), ], ), Placeholder(), Placeholder(), Placeholder(), ], ), ), ); } // 길어지니까 메소드 추출을 이용해 빼내기 AppBar _appbar() { return AppBar( leading: Text("리딩 영역"), title: Text("타이틀 영역"), // appbar의 액션 영역에 아이콘 넣기 actions: [ Icon(Icons.search), // 돋보기 아이콘 SizedBox(width: 16), // 여백용 Icon(CupertinoIcons.heart, color: Colors.redAccent), // 하트 아이콘 (CupertinoIcons >> 자동으로 들어가 있는 아이콘 외부 라이브러리) SizedBox(width: 16), ], ); } }
ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로
ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로

2. 앱 바 생성

1. 앱바의 기본 구조

notion image

2. 앱 바 구성

AppBar _appbar() { return AppBar( leading: Text("리딩 영역"), title: Text("타이틀 영역"), // appbar의 액션 영역에 아이콘 넣기 actions: [ Icon(Icons.search), // 돋보기 아이콘 SizedBox(width: 16), // 여백용 Icon(CupertinoIcons.heart, color: Colors.redAccent), // 하트 아이콘 (CupertinoIcons >> 자동으로 들어가 있는 아이콘 외부 라이브러리) SizedBox(width: 16), ], ); } }

3. 내부 카테고리 위젯 생성(appbar 생략)

1. 내부에서 하나 생성

// 페이지 class RecipePage extends StatelessWidget { const RecipePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: _appBar(), // AppBar 메서드 호출 body: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), // 좌우 여백 20 child: ListView( // ListView: 스크롤 가능 + 기본 정렬은 위에서 아래로 children: [ Text("Recipes"), // 제목 텍스트 Row( children: [ // 컨테이너를 통해 하나 생성 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: 3), // 간격 Text( "${mText}", // 텍스트 style: TextStyle(color: Colors.black87), // 텍스트 스타일 ), ], ), ), ], ), Placeholder(), // 플레이스홀더 1 Placeholder(), // 플레이스홀더 2 Placeholder(), // 플레이스홀더 3 ], ), ), ); } }
💡

Container?

  • html의 div와 비슷한 역할
  • 색상을 주거나 테두리 선을 줄 수있음
  • 자식이 없는 경우 최대한 큰 영역을 가지려함
  • 자식이 있는 경우 자식의 크게에 맞는 영역을 가짐

SizedBox와의 차이?

SizedBox는 주로 마진을 줄 때 사용함

2. 반복되는 부분 위젯화하여 반복 줄이기

// 페이지 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( // Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로 children: [ Text("Recipes"), Row( children: [ 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"), ], ), Placeholder(), Placeholder(), Placeholder(), ], ), ), ); } class MenuItem extends StatelessWidget { // 필드 생성 final mIcon; final mText; // 생성자 >> 헷갈리지 않게 필드명이 뜨도록 {}안에 넣기 + 반드시 받도록 required 붙이기 MenuItem({required this.mIcon, required this.mText}); @override Widget build(BuildContext context) { // Container >> html의 div와 비슷한 역할 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: 3), // "${}" 이렇게 하면 null이 나오거나 문자열이 너무 길면 빈칸 처리 Text("${mText}", style: TextStyle(color: Colors.black87)) ], ), ); } }

3. 카테고리들 하나로 뭉치기

// 페이지 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( // Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로 children: [ Title(), Menu(), ], ), ), ); } } class Menu extends StatelessWidget { const Menu({ super.key, }); @override Widget build(BuildContext context) { return Row( children: [ 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"), ], ); } } class MenuItem extends StatelessWidget { final mIcon; final mText; MenuItem({required this.mIcon, required 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: 3), Text("${mText}", style: TextStyle(color: Colors.black87)) ], ), ); } }

4. 내부 리스트 위젯 생성

💡

3번과 방향(Column인가 Row인가)의 차이, 구성방식은 같음

1. 코드 (위와 중복되는 위젯 구성 부분은 생략)

// 페이지 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), children: [ Title(), Menu(), // 동일하게 내부에서 하나만 생성후 위젯화 한 다음 생성자 파라미터만 넣어 반복 생성 RecipeItem(myText: "Burger"), RecipeItem(myText: "Coffee"), RecipeItem(myText: "Pizza"), ], ), ), ); } } class RecipeItem extends StatelessWidget { // 클래스 속성 final myText; // 텍스트를 받아오는 변수 // 생성자 RecipeItem({required this.myText}); // 생성자에서 myText 값을 필수로 전달받음 @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, // 왼쪽 정렬 children: [ SizedBox(height: 30), // 위쪽 간격 추가 AspectRatio( aspectRatio: 2 / 1, // 가로:세로 비율 설정 (2:1) child: ClipRRect( borderRadius: BorderRadius.circular(20), // 둥근 모서리 설정 child: Image.asset( "assets/${myText}.jpeg", // assets 폴더에서 이미지 파일을 불러옴 fit: BoxFit.cover, // 이미지가 영역을 꽉 채우도록 설정 ), ), ), SizedBox(height: 10), // 이미지와 텍스트 사이의 간격 Text( "Made ${myText}", // 제목 텍스트 style: TextStyle(fontSize: 20), // 텍스트 크기 설정 ), Text( // 설명 텍스트 "Have you ever made your own ${myText}? Once you've tried a homemade ${myText}, you'll never go back.", style: TextStyle(color: Colors.grey, fontSize: 12), // 회색 텍스트, 작은 글자 크기 ), ], ); } }

5. 최종 코드와 화면

1. 최종 코드

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()), // 앱 전체 글꼴 설정 ); } } // 페이지 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( // Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로 children: [ Title(), Menu(), RecipeItem(myText: "Burger"), RecipeItem(myText: "Coffee"), RecipeItem(myText: "Pizza"), ], ), ), ); } // 길어지니까 메소드 추출을 이용해 빼내기 AppBar _appbar() { return AppBar( leading: Text("리딩 영역"), title: Text("타이틀 영역"), // appbar의 액션 영역에 아이콘 넣기 actions: [ Icon(Icons.search), // 돋보기 아이콘 SizedBox(width: 16), // 여백용 Icon(CupertinoIcons.heart, color: Colors.redAccent), // 하트 아이콘 (CupertinoIcons >> 자동으로 들어가 있는 아이콘 외부 라이브러리) SizedBox(width: 16), ], ); } } class RecipeItem extends StatelessWidget { final myText; RecipeItem({required this.myText}); @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/${myText}.jpeg", fit: BoxFit.cover, ), ), ), SizedBox(height: 10), Text("Made ${myText}", style: TextStyle(fontSize: 20)), Text( "Have you ever made your own ${myText}? Once you've tried a homemade ${myText}, you'll never go back.", style: TextStyle(color: Colors.grey, fontSize: 12), ), ], ); } } class Menu extends StatelessWidget { const Menu({ super.key, }); @override Widget build(BuildContext context) { return Row( children: [ // 컴포넌트 생성 반복 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"), ], ); } } 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), ), ); } } class MenuItem extends StatelessWidget { // 필드 생성 final mIcon; final mText; // 생성자 >> 헷갈리지 않게 필드명이 뜨도록 {}안에 넣기 + 반드시 받도록 required 붙이기 MenuItem({required this.mIcon, required this.mText}); @override Widget build(BuildContext context) { // Container >> html의 div와 비슷한 역할 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: 3), // "${}" 이렇게 하면 null이 나오거나 문자열이 너무 길면 빈칸 처리 Text("${mText}", style: TextStyle(color: Colors.black87)) ], ), ); } }

2. 화면

notion image

6. 추가 정보 정리

1. 메소드 빼내기

💡
컨트롤 + 알트 + M
선택된 영역이 메소드로 빠짐
선택된 영역이 메소드로 빠짐

2. 라이브러리 추가하기 (폰트 라이브러리 추가)

  1. 검색 후 인스톨에 명령어 찾기
    1. notion image
  1. 터미널에 입력하고 실행하면
    1. notion image
  1. pubspec.yaml 파일의 의존성 파트에 추가됨
    1. notion image
  1. 라이브러리 추가전에 새로운 프로젝트를 파서 예제코드를 긁어서 테스트 하는 샘플링 과정을 거치는 것이 좋음
    1. 저장하면 자동으로 정렬하는 기능
      저장하면 자동으로 정렬하는 기능

3. 반복되는 컴포넌트 위젯화 하기

만드는 경로(단축키가 잘 안먹음)
만드는 경로(단축키가 잘 안먹음)
생성할 위젯이름
생성할 위젯이름
class MenuItem extends StatelessWidget { const MenuItem({ super.key, }); @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(Icons.food_bank, color: Colors.redAccent, size: 30), SizedBox(height: 3), Text("ALL", style: TextStyle(color: Colors.black87)) ], ), ); } }
생성된 컴포넌트

4. 이미지를 넣을때

💡
이미지 넣을때 BoxFit과 aspect는 쌍으로 와야 이쁘게 들어감
AspectRatio( aspectRatio: 2 / 1, // 가로:세로 비율을 2:1로 설정 (가로가 세로의 두 배 크기) child: ClipRRect( borderRadius: BorderRadius.circular(20), // 모서리를 둥글게 (반지름 20) child: Image.asset( "assets/${myText}.jpeg", // assets 폴더에서 ${myText} 값에 해당하는 이미지 로드 fit: BoxFit.cover, // 이미지를 영역에 꽉 채우되, 비율을 유지하며 넘치는 부분은 잘림 ), ), ),
Share article

stupefyee