Front-End/Flutter_Project_03_ToDo APP

7.Udemy 강의를 통한 Meals Project

sd4beatles 2025. 1. 21. 23:51

1. Before Start

- Drawer

Flutter에서 **Drawer(드로어)**는 화면의 왼쪽(또는 오른쪽)에서 슬라이드하여 나타나는 패널로, 보통 내비게이션 메뉴를 표시하는 데 사용됩니다. 드로어는 앱의 다양한 섹션이나 기능으로 쉽게 접근할 수 있도록 해줍니다.

- ListTile

- SwitchListTile

2. Prodjct

2.1 Main Drawer 정의(part 01) - DrawerHeader

Drawer는 Scaffold의 dreawer option으로 들어가게 된다. 물론, 그 방대한 Drawer Widget을 option에 집어넣게 된다면, 가독성을 저하시키는 원인이 되므로, 우리는 일단 이를 ./widgets/main_drawer.dart에 집어넣는다.

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: [
          //Drawer-Column-(1)DrawerHeader
          DrawerHeader(
            padding:const EdgeInsets.all(20)   , 
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [
                  Theme.of(context).colorScheme.primaryContainer,
                  Theme.of(context)
                  .colorScheme
                  .primaryContainer
                  .withAlpha((0.9*255).toInt()),]
                ,begin:Alignment.topLeft
                ,end:Alignment.bottomRight
                 )
            ) ,
            child: Row(
              children: [
                Icon(Icons.fastfood,size:48,color:Theme.of(context).colorScheme.primary,),
                const SizedBox(width:18),
                Text("Cooking up!",style:Theme.of(context).textTheme.titleLarge!.copyWith(
                  color:Theme.of(context).colorScheme.primary,
                )),

              //Drawer-Column-(2)DrawerHeader

              ],

            ),

            ),
        ],
      )
    );
  }

}

2.2 Main Drawer 정의(part 02) ListTitle

여러가지 정보를 알맞게 표현해주는 Widget이 있는데,이를 바로 ListTitle이라고 한다. 많은 옵션들이 있고, 이를 MainDrawer에 한번 써보도록 하자.

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: [
          //Drawer-Column-(1)DrawerHeader
          DrawerHeader(
            padding:const EdgeInsets.all(20)   , 
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [
                  Theme.of(context).colorScheme.primaryContainer,
                  Theme.of(context)
                  .colorScheme
                  .primaryContainer
                  .withAlpha((0.9*255).toInt()),]
                ,begin:Alignment.topLeft
                ,end:Alignment.bottomRight
                 )
            ) ,
            child: Row(
              children: [
                Icon(Icons.fastfood,size:48,color:Theme.of(context).colorScheme.primary,),
                const SizedBox(width:18),
                Text("Cooking up!",style:Theme.of(context).textTheme.titleLarge!.copyWith(
                  color:Theme.of(context).colorScheme.primary,
                ))

              ],
              ),

            ),

        //Drawer-Column-(2)DrawerHeader
            ListTile(
                leading: Icon(Icons.restaurant,size:26,color:Theme.of(context).colorScheme.onSurface,
                ),

                title:Text('Meals',
                        style:Theme.of(context).textTheme.titleSmall!.copyWith(
                        color:Theme.of(context).colorScheme.primary,
                        fontSize:24,
                        )
                      ),
                onTap: (){},
              ),
        //Drawer-Column-(3)
          ListTile(
                leading: Icon(Icons.settings,size:26,color:Theme.of(context).colorScheme.onSurface,
                ),

                title:Text('Filters',
                        style:Theme.of(context).textTheme.titleSmall!.copyWith(
                        color:Theme.of(context).colorScheme.primary,
                        fontSize:24,
                        )
                      ),
                onTap: (){},
              ),
        ],
      )
    );
  }

}

2.2 Main Drawer 정의(part 02) FiterScreen

마지막 main_drawer section에 올려놓은 것은 바로 'filter'항목이였다.그렇다면,이를 세분하 할 수 있는 예시를 통해서 filter를 항목을 조정해보자.

step 01) ./screens/filters.dart에 다가, FilterScreen StateFulWidget을 만들어 준다. 그리고 아래와 같이 항목을 만들어 준다.

import 'package:flutter/material.dart';



class FilterScreen extends StatefulWidget{
  @override
  State<StatefulWidget>createState(){
    return _FilterScreen();
  }


}


class _FilterScreen extends State<FilterScreen>{
  var _glutenFreeFilterSet=false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      //Scaffold-appBar
      appBar:AppBar(
        title:const Text("Your filter"),
      ),
      //Scaffold-body
      body:Column(
        children: [
          //value:boolean, onChanged:activation funciton if true
          //title: message appears
          //activeColor: choose the color if switch is activated
          //SwitchListTile(01)
          SwitchListTile(
            value:_glutenFreeFilterSet , 
            onChanged: (bool isChecked){
              setState((){
                _glutenFreeFilterSet=isChecked;
              });
            },
            //SwitchListTile-title
            title:Text(
              'Glutten-free',
              style:Theme.of(context).textTheme.titleLarge!.copyWith(
                color:Theme.of(context).colorScheme.onSurface,)),
            //SwitchListTile-subtitle
            subtitle:Text(
              'Only include glute-free meals.',
              style:Theme.of(context).textTheme.labelMedium!.copyWith(
                color:Theme.of(context).colorScheme.onSurface,
              )
            ),
            //active color-color if switch is activated
            activeColor: Theme.of(context).colorScheme.tertiary,
            contentPadding: const EdgeInsets.only(left:34,right:22),    

            ),


        ],
      )
    );

  }

}

step 02) 우리가 더 추가적인 SwitchListTile을 집어넣기 전에, 이 filter.dart가 과연 제대로 작동한지 보고 싶다. 이를 위해선, ./screens/tabs.dart에 가서 아래와 같이 코드를 추가한다.

// ./screens/tabs.dart

  //new function
  void _setScreen(String identifier){
    if(identifier=='filters'){
      Navigator.of(context).push(
          MaterialPageRoute(builder: (ctx)=>const FilterScreen())
      );

    }else{
      Navigator.of(context).pop();
    }

@override build function 부분 위에다가 윗 코드를 정의한다. 그리고 이를 동일한 file안에 있는, Scaffold-drawer option안의 우리가 지정한 MainDrawer widget class의 옵션으로 넣어야 한다. 하지만, MainDrawer는 현재 function을 받아줄 매개변수가 없다. 이를 추가하기 위해, 다시
./widget/main_drawer.dart 로 들어가서 function을 받아줄 매개변수를 정의한다.

// ./widget/main_drawer.dart

ListTile(
            leading: Icon(
              Icons.settings,
              size: 26,
              color: Theme.of(context).colorScheme.onSurface,
            ),
            title: Text(
              'Filters',
              style: Theme.of(context).textTheme.titleSmall!.copyWith(
                    color: Theme.of(context).colorScheme.onSurface,
                    fontSize: 24,
                  ),
            ),
            onTap: () {
              onSelectScreen('filters');
            },
          ),

./widget/main_drawer.dart

import 'package:flutter/material.dart';

class MainDrawer extends StatelessWidget {
  const MainDrawer({super.key, required this.onSelectScreen});

  final void Function(String identifier) onSelectScreen;

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: [
          DrawerHeader(
            padding: const EdgeInsets.all(20),
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [
                  Theme.of(context).colorScheme.primaryContainer,
                  Theme.of(context)
                      .colorScheme
                      .primaryContainer
                      .withAlpha((0.8*255).toInt()),
                ],
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
              ),
            ),
            child: Row(
              children: [
                Icon(
                  Icons.fastfood,
                  size: 48,
                  color: Theme.of(context).colorScheme.primary,
                ),
                const SizedBox(width: 18),
                Text(
                  'Cooking Up!',
                  style: Theme.of(context).textTheme.titleLarge!.copyWith(
                        color: Theme.of(context).colorScheme.primary,
                      ),
                ),
              ],
            ),
          ),
          ListTile(
            leading: Icon(
              Icons.restaurant,
              size: 26,
              color: Theme.of(context).colorScheme.onSurface,
            ),
            title: Text(
              'Meals',
              style: Theme.of(context).textTheme.titleSmall!.copyWith(
                    color: Theme.of(context).colorScheme.onSurface,
                    fontSize: 24,
                  ),
            ),
            onTap: () {
              onSelectScreen('meals');
            },
          ),
          ListTile(
            leading: Icon(
              Icons.settings,
              size: 26,
              color: Theme.of(context).colorScheme.onSurface,
            ),
            title: Text(
              'Filters',
              style: Theme.of(context).textTheme.titleSmall!.copyWith(
                    color: Theme.of(context).colorScheme.onSurface,
                    fontSize: 24,
                  ),
            ),
            onTap: () {
              onSelectScreen('filters');
            },
          ),
        ],
      ),
    );
  }
}

./screens/tabs.dart 는 아래와 같다.

import 'package:flutter/material.dart';
import 'package:meals/screens/categories.dart';
import 'package:meals/screens/filters.dart';
import 'package:meals/screens/meals.dart';
import 'package:meals/models/meal.dart';
import 'package:meals/widget/main_drawer.dart';
import 'package:meals/widget/meal_item.dart';

//하나의 route에서 다른 route로 이동을 해야하므로, 우리는
//StatelessWidget이 아닌 StatefulWidget를 사용한다. 
class TabsScreen extends StatefulWidget{
  const TabsScreen({super.key});

  @override
  State<TabsScreen>createState(){
    return _TabsScreen();
  }

}


class _TabsScreen extends State<TabsScreen>{

  int _selectedPageIndex=0;
  final List<Meal>_favoriteMeals=[];
  /*
  그렇다면, snackBar라는 option을 통해서 우리가 전달하고자 하는 메시지를
  인자로 주고, 그걸 snackBar에 보여주는 것을 목표로 한다. 

  1) _favoriteMeals에 meal을 추가할때, 
      "Marked as a favorite"메세지 보내기

  2) _favoriteMeals에서 meal을 삭제할때,
     "Meal is no longer a favorite"메시지 보내기


  */
  void _showInfoMessage(String message){
    ScaffoldMessenger.of(context).clearSnackBars();
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        ),
    );
  }

  void _toggleMealFavoriteStatus(Meal meal){
    //parameter로 받은 meal이 현재 _favoriteMeal에 있는지 체크
    final isExisting=_favoriteMeals.contains(meal);

    if(isExisting){
       setState(() {
          _favoriteMeals.remove(meal);
       });
       _showInfoMessage("Meal is no longer a favorite");

    }else{
      setState(() {
         _favoriteMeals.add(meal);
      });
      _showInfoMessage("Marked as a favorite");

    }    
  }





  var activePageTitle="Categories";

  void _selectPage(int index){
    setState((){
      _selectedPageIndex=index;

    });
  }

  //new function
  void _setScreen(String identifier){
    if(identifier=='filters'){
      Navigator.of(context).push(
          MaterialPageRoute(builder: (ctx)=>const FilterScreen())
      );

    }else{
      Navigator.of(context).pop();
    }

  }

  @override
  Widget build(BuildContext context){
    Widget activePage=CategoriesScreen(ontoggleFavorite:_toggleMealFavoriteStatus,);

    if(_selectedPageIndex==1){
      activePage=MealsScreen( 
      meals:_favoriteMeals,
      ontoggleFavorite:_toggleMealFavoriteStatus,);
      activePageTitle="Your favorite";
    }



    return Scaffold(
      appBar:AppBar(
        title:Text(activePageTitle),
      ),
      drawer: MainDrawer(
        onSelectScreen:_setScreen,
      ),
      body:activePage,
      bottomNavigationBar:BottomNavigationBar(
        onTap:_selectPage,
        currentIndex: _selectedPageIndex,
        items:const [
          BottomNavigationBarItem(icon:Icon(Icons.set_meal),label:"Categories"),
          BottomNavigationBarItem(icon:Icon(Icons.star),label:"Favorite"),

        ]
       ),
    );
  }

}

2.3 Navigator Pop (다른 방법)

Filter 안에 drawer가 없는 경우 (변경 전)

이번에는 drawer를 Filter안에다가 넣는 방법도 알아보도록하자 .

step 01) User가 'Your filter'를 진입을 했으면, 다시 TabScreen화면으로 전환시킨다.

step 02) TabScreen으로 전환된 뒤에, Meals를 입력한다면 MealScreen으로 전환.



class FilterScreen extends StatefulWidget{
  const FilterScreen({super.key});

  @override
  State<StatefulWidget>createState(){
    return _FilterScreen();
  }


}


class _FilterScreen extends State<FilterScreen>{
  var _glutenFreeFilterSet=false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      //Scaffold-appBar
      appBar:AppBar(
        title:const Text("Your filter"),
      ),
      drawer:MainDrawer(onSelectScreen: (identifier){
        //현재 filter context를 지워버리고. 
        Navigator.of(context).pop();
        //만약 User가 Meals를 선택한다면, tabScreen으로 돌아가게 한다. 
        if(identifier=="meals"){
          Navigator
          .of(context)
          .push(MaterialPageRoute(builder: (ctx)=>const TabsScreen()));
        }
      },)
      ,//Scaffold-body

 

 

 

Filter에다가 drawer를 쓰지 않고 싶다면, 위의 drawer를 지워버리면 됨.