[TIL] flutter - Recipe App

김영광·2025년 12월 16일

모닝스터디 2시간 반에... 실력별 분반 1시간 반에... 오늘은 공부할 시간이 얼마 없었다..
강의에 있던 간단한 레시피 앱과 영화 예매 앱을 만들어보려 했지만, 영화 예매 앱은 내일로 "당당히" 미루겠다.

🔎 페이지별로 분리한, 위젯별로 분리한... 분리한..

1️⃣ Main, EntryOfApp

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        appBarTheme: AppBarThemeData(
          actionsPadding: EdgeInsets.only(right: 20),
          backgroundColor: Colors.grey[300],
        ),
        fontFamily: 'NotoSansKR',
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.pinkAccent),
      ),
      home: Recipepage(),
    );
  }
}

compose에서 공부하던 기억을 떠올려 page를 구분했다.
덕분에 main 함수에는 전역적인 Theme와 Font 등 시스템적인 부분만 관리할 수 있게 되었다.
가독성을 챙길 수 있고, 명시성 또한 생긴다.

2️⃣ page, RecipePage

class Recipepage extends StatelessWidget {
  const Recipepage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          Icon(Icons.search, color: Colors.black54),
          SizedBox(width: 12),
          Icon(Icons.favorite_border_outlined, color: Colors.redAccent),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          spacing: 10,
          children: [
            RecipeTitle(title: "Recipes"),
            RecipeMenu(),
            RecipeList(
              itemList: [
                RecipeData(imageUri: "assets/coffee.jpg", menu: "coffee"),
                RecipeData(imageUri: "assets/burger.jpg", menu: "burger"),
                RecipeData(imageUri: "assets/pizza.jpg", menu: "pizza"),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

이번 앱은 페이지가 하나로 구성되어 있는 간단한 앱이다.
그래서 Page간의 Route 이동은 없고, 해당 Page가 전부이다.
모든 Widget은 추후 재사용하기 위해 컴포넌트화를 목표로 했다.

또, 새로 알게된 사실은 appBar에 요구되는 파라미터는 PreferredSizeWidget? 타입이다.
PrefererredSizeWdiget class는 extends와 implements 둘 다 사용할 수 있는 abstract class이다.

3️⃣ RecipeTitle

class RecipeTitle extends StatelessWidget {
  const RecipeTitle({super.key, required this.title});

  final String title;

  
  Widget build(BuildContext context) {
    return SizedBox(
      child: Text(
        title,
        maxLines: 1,
        style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
      ),
    );
  }
}

Title을 Constructor parameter로 받는 간단한 Widget이다.

4️⃣ RecipeMenu

class RecipeMenu extends StatelessWidget {
  const RecipeMenu({super.key});

  
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        RecipeMenuItem(icon: Icons.food_bank, text: "ALL"),
        RecipeMenuItem(icon: Icons.coffee_rounded, text: "Coffee"),
        RecipeMenuItem(icon: Icons.fastfood, text: "Burger"),
        RecipeMenuItem(icon: Icons.local_pizza_rounded, text: "Pizza"),
      ],
    );
  }
}

class RecipeMenuItem extends StatelessWidget {
  const RecipeMenuItem({super.key, required this.icon, required this.text});

  final IconData icon;
  final String text;
  
  Widget build(BuildContext context) {
    return Container(
      width: 70,
      height: 90,
      decoration: BoxDecoration(
        border: Border.all(color: Colors.black),
        borderRadius: BorderRadius.all(Radius.circular(36)),
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [Icon(icon), Text(text)],
      ),
    );
  }
}

ALL, burger 등 중상단 메뉴버튼이다.
지금은 단순한 Container지만, 나중에는 ImageButton으로 만들어봐야겠다.

5️⃣ RecipeList

class RecipeList extends StatelessWidget {
  const RecipeList({super.key, required this.itemList});

  final List<RecipeData> itemList;

  
  Widget build(BuildContext context) {
    return Expanded(
      child: ListView.separated(
        itemCount: itemList.length,
        itemBuilder: (context, index) =>
            RecipeListItem(recipeData: itemList[index]),
        separatorBuilder: (context, index) =>
            Divider(indent: 20, endIndent: 20),
      ),
    );
  }
}

class RecipeListItem extends StatelessWidget {
  const RecipeListItem({super.key, required this.recipeData});

  final RecipeData recipeData;

  
  Widget build(BuildContext context) {
    return Column(
      spacing: 5,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        AspectRatio(
          aspectRatio: 2 / 1,
          child: ClipRRect(
            borderRadius: BorderRadiusGeometry.all(Radius.circular(8)),
            child: Image.asset(recipeData.imageUri, fit: BoxFit.cover),
          ),
        ),
        Text(recipeData.menu, style: TextStyle(fontSize: 20)),
        Text(
          "Have you ever made your own Made ${recipeData.menu}? Once you've tried a homemade Made ${recipeData.menu}, you'll never go back.",
        ),
      ],
    );
  }
}

class RecipeData {
  RecipeData({required this.imageUri, required this.menu});

  final String imageUri;
  final String menu;
}

DataClass처럼 RecipeData class를 정의해 사용했다.
해당 패턴은 Json 파일을 불러올 때도 자주 사용되니, 조금 더 공부하며 적응해봐야겠다.

🔎 결과 및 추후 방향성



아직 Layout이 어떻게 잡히는지 미숙하다고 생각된다.
조금 더 여러 앱들을 만들며 Widget, 특히 Conatainer와 Row, Column과 친해지는 시간을 가져야겠다.

팀의 학습 방향으로는 튜터님의 조언을 받고 Class Constructor, Named Parameter을 위주로 공부해야겠다.

profile
주니어 개발자

0개의 댓글