Flutter Project – Meals App
Every day, we prepare meals for breakfast, lunch, dinner, or snacks. Remember when you wanted to prepare a meal and forgot the recipe or ingredients to add? It would be helpful if there were an app from which you could find recipes for various cuisines, and it would be better if it could tell how much time it may take to prepare the recipe.
About Flutter Meals App
In this Flutter Meals App project, we will create an app in Flutter that provides recipes for various meals based on categories like Italian, French, and Summer. The app can also indicate the expected time to prepare the recipe, whether it is easy or difficult to make, and whether it is affordable or pricey.
Let’s dive into it!
Prerequisites For Meals App Using Flutter
Before starting work on building the app, you should have the below-mentioned required software on your computer:
(i) Flutter – Refer to the link for installing Flutter, depending on your operating system.
(ii) Android Studio – You can download Android Studio. This is necessary as it will run the app in the Android emulator.
(iii) Visual Studio Code—Although this is not necessary, you can also build apps in Android Studio. In our case, we have used VS Code as it is a good code editor.
Now that the setup is ready let’s get started!
Download Flutter Meals App project.
Please download the source code of the Flutter Meals App Project: Flutter Meals App Project Code.
Creating New Flutter Meals app Project
First, let’s create a new project in Flutter Meals app by doing the following steps:-
1. Go to the directory where you want to save the project using:-
cd $Project-directory-path
2. Then create a new project using the below command:-
flutter create meals_app
Changes in pubsec.yaml file
To build the application, we would need the following packages:-
google_fonts: This package gives access to all the google_fonts created by Google. You can install this by the following command:-
flutter pub add google_fonts
transparent_image: This package in Flutter helps to create a transparent image. This is useful while loading images from the web. It can be installed using the following command:-
flutter pub add transparent_image
You can see that the installed packages would reflect changes in the pubsec.yaml file, as seen in the image below.
Steps to Build Flutter Meals App
1. Building Main.dart file and setting Theme for App
Let’s start building the main layout of the app. Here, we are importing the google_fonts package installed in our app while making changes in pubsec.yaml file. We are also importing ‘categories_screen’, which we will create later.
We are storing ThemeData in the theme variable, where we set the ‘useMaterial3’ property to true and defined the color scheme. We have also defined the text theme using GoogleFonts and the latoTextTheme.
In the build function, we are returning the MaterialApp widget to use the app’s material design. Inside, we are returning the theme we created, and in the ‘home’ property, we are returning the TabsScreen widget, which we will create later.
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import './screens/categories_screen.dart';
final theme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: const Color.fromARGB(255, 131, 57, 0),
),
textTheme: GoogleFonts.latoTextTheme(),
);
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: theme,
home: const TabsScreen(),
);
}
}2. Creating Models Used in the App
In this app, we have defined two classes or models for storing data for each Category and Meal.
A. Category
This class is used to store the property and function for each category. We have specified that each class will have an id, title and color. The user will specify all these properties when instantiating an object of this class.
import 'package:flutter/material.dart';
class Category {
Category({required this.id, required this.title, this.color = Colors.orange});
final String id;
final String title;
final Color color;
}B. Meal
The Meal class(model) specifies a meal’s blueprint, which will contain its properties and functions. Here we have defined that each meal will have an id, list of categories in which it falls, title, imageUrl for its image, list of ingredients, list of steps, duration to make it, complexity, whether it is simple or hard to prepare, its affordability, whether it is glutenFree or not, whether it is lactoseFree or not, whether it is a vegan meal or not and also whether it is a vegetarian dish or not.
enum Complexity {
simple,
challenging,
hard,
}
enum Affordability {
affordable,
pricey,
luxurious,
}
class Meal {
const Meal({
required this.id,
required this.categories,
required this.title,
required this.imageUrl,
required this.ingredients,
required this.steps,
required this.duration,
required this.complexity,
required this.affordability,
required this.isGlutenFree,
required this.isLactoseFree,
required this.isVegan,
required this.isVegetarian,
});
final String id;
final List<String> categories;
final String title;
final String imageUrl;
final List<String> ingredients;
final List<String> steps;
final int duration;
final Complexity complexity;
final Affordability affordability;
final bool isGlutenFree;
final bool isLactoseFree;
final bool isVegan;
final bool isVegetarian;
}3. Data Used in the App
In our project, we created sample data for categories and meals using the classes we created above. We will use this data to display categories on the categories screen and various meals on the meals screen. You can customize the data according to your requirements.
4. Creating Tabs Screen
This screen will be shown when a user opens the app. It is a stateful widget, as the content on the screen needs to be rendered again if some states change.
Here we have defined variables like:-
a. selectedPageIndex—On this screen, there is a BottomNeavigationBar that can be used to navigate between the Categories screen and the Favorite Meals screen. This variable is used to select which screen the user lands on. By default, its value is 0, which points to the Categories Screen.
b. favoriteMeals—This is a list of Meals. It stores the meals that the user has selected as favourites.
c. _selectedFilters—This is used to store the filters selected by the user. We will learn about this later.
We have also defined a few functions for user interaction. These are:-
a. _showToggleInfo—This function shows the SnackBar message when a user adds or removes a meal from favorites.
b. toggleMealFavoriteStatus—This function is executed when the user tries to add or remove a meal from favoriteMeals. First, it checks whether the meal is already in favoriteMeals. If it is, it removes that meal from favoriteMeals and shows a message using _showToggleInfo that the meal is removed. Otherwise, it adds the meal to favoriteMeals and shows a message that the meal has been added.
c. _selectPage—This function sets the selectedPageIndex variable when the user selects a screen from the bottom navigation bar.
d. _setScreen – This function is passed to the Drawer widget which we will create. It is used to push the FiltersScreen using Navigator.of(context).push. We also receive data which is stored in variable result, when the screen pops off. Finally we also set the _selectedFilters to the values that are selected at Filters screen.
In the build function, we store the filtered meals based on the _selectedFilters in the variable availableMeals. In the variable activePage, we store the CategoryScreen widget, which we will create after the next section. We then check if the selectedPageIndex is 1 and change the activePage variable to MealsScreen, where the meals shown are the favourite meals selected by the user.
Finally, we are returning the Scaffold widget to create the main layout of the page. In Scaffold, we are showing the appBar, a drawer that we will create in the next section, through which users can access the filters to be implemented on the meals. In the body argument, we are returning the widget we stored in the activePage variable.
Finally, at the bottom, we show the BottomNavigationBar, through which the user can access the Categories screen and Favorite Meals screen. In the onTap argument, we pass the _selectPage function we defined above.
import 'package:flutter/material.dart';
import 'package:meals_app/data/dummy_data.dart';
import 'package:meals_app/screens/categories_screen.dart';
import 'package:meals_app/screens/filters_screen.dart';
import 'package:meals_app/screens/meals_Screen.dart';
import '../models/meal.dart';
import '../widgets/main_drawer.dart';
final kInitialFilters = {
Filter.glutenFree: false,
Filter.lactoseFree: false,
Filter.vegetarian: false,
Filter.vegan: false
};
class TabsScreen extends StatefulWidget {
const TabsScreen({super.key});
@override
State<TabsScreen> createState() {
return _TabsScreenState();
}
}
class _TabsScreenState extends State<TabsScreen> {
var selectedPageIndex = 0;
final List<Meal> favoriteMeals = [];
Map<Filter, bool> _selectedFilters = kInitialFilters;
void _showToggleInfo(String message) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message),
));
}
void toggleMealFavoriteStatus(Meal meal) {
final isFavorite = favoriteMeals.contains(meal);
if (isFavorite) {
setState(() {
favoriteMeals.remove(meal);
});
_showToggleInfo('Meal is removed from Favorites');
} else {
setState(() {
favoriteMeals.add(meal);
});
_showToggleInfo('Meal is added to Favorites');
}
}
void _selectPage(index) {
setState(() {
selectedPageIndex = index;
});
}
void _setScreen(String identifier) async {
Navigator.of(context).pop();
if (identifier == 'filters') {
final result = await Navigator.of(context).push<Map<Filter, bool>>(
MaterialPageRoute(
builder: (context) => FiltersScreen(
currentFilters: _selectedFilters,
),
),
);
setState(() {
_selectedFilters = result ?? kInitialFilters;
});
}
}
@override
Widget build(BuildContext context) {
final availableMeals = dummyMeals.where((meal) {
if (_selectedFilters[Filter.glutenFree]! && !meal.isGlutenFree) {
return false;
}
if (_selectedFilters[Filter.lactoseFree]! && !meal.isLactoseFree) {
return false;
}
if (_selectedFilters[Filter.vegetarian]! && !meal.isVegetarian) {
return false;
}
if (_selectedFilters[Filter.vegan]! && !meal.isVegan) {
return false;
}
return true;
}).toList();
Widget activePage = CategoriesScreen(
onToggleFavorite: toggleMealFavoriteStatus,
availableMeals: availableMeals,
);
var activePageTitle = 'Categories (By DataFlair)';
if (selectedPageIndex == 1) {
activePage = MealsScreen(
onToggleFavorite: toggleMealFavoriteStatus, meals: favoriteMeals);
activePageTitle = 'Your Favorites';
}
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: 'Favorites')
]),
);
}
}5. Creating Drawer for Tabs Screen
The code below creates the Drawer we use in the Tabs screen. Here, we accept the onSelectScreen through the constructor function we created in the Tabs screen, which has the name _setScreen.
In the build function, we return the Drawer widget, inside which we show a Column widget. Inside the Column widget, we show DrawerHeader, where we have also added styling using BoxDecoration. In its child argument, we show an Icon of fast and Text horizontally using the Row widget. Below the DrawerHeader, we are showing a ListTile where we set the title as ‘Meals’ through which the user can navigate to the Categories screen using the onSelectScreen function. Below it is another ListTile, which allows the user to navigate to the Filters screen.
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.withOpacity(0.8)
], 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: const Icon(Icons.restaurant),
title: Text(
'Meals',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
fontSize: 24),
),
onTap: () {
onSelectScreen('meals');
},
),
ListTile(
leading: const Icon(Icons.settings),
title: Text(
'Filters',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
fontSize: 24),
),
onTap: () {
onSelectScreen('filters');
},
)
],
),
);
}
}6. Creating Categories Screen
Let’s start building the CategoriesScreen, which will be used to show the different categories for meals. It is shown through the Tabs Screen which we created above.
Here, we are accepting the onToggleFavorite function and availableMeals through the constructor function.
It is a Stateless widget, as the content on the page doesn’t need to render again. We have defined the _selectCategory function, which will be executed when the user selects a category. When the user selects a category, it will show the MealsScreen, which is a list of meals for that category, which we will create in the next section.
In the build function, we return the Scaffold widget, which will create the app’s basic design structure. Inside it, we have defined the appBar, and in the ‘body’ parameter, we are returning the GridView widget. In the GridView, we have defined some padding, in the ‘gridDelegate’ parameter we are returning SliverGridDelegateWithFixedCrossAxisCount with crossAxisCount as 2 and defined crossAxisSpacing, mainAxisSpacing and childAspectRatio. In the children property, we return a list where each category is mapped to a CategoryGridItem(we will create it in the helper widgets section).
import 'package:flutter/material.dart';
import 'package:meals_app/screens/meals_screen.dart';
import '../data/dummy_data.dart';
import '../widgets/category_grid_item.dart';
import '../models/category.dart';
import '../models/meal.dart';
class CategoriesScreen extends StatelessWidget {
const CategoriesScreen(
{super.key,
required this.onToggleFavorite,
required this.availableMeals});
final void Function(Meal meal) onToggleFavorite;
final List<Meal> availableMeals;
void _selectCategory(BuildContext context, Category category) {
final filteredMeals = availableMeals
.where((meal) => meal.categories.contains(category.id))
.toList();
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => MealsScreen(
title: category.title,
meals: filteredMeals,
onToggleFavorite: onToggleFavorite,
),
),
);
}
@override
Widget build(BuildContext context) {
return GridView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
childAspectRatio: 3 / 2),
children: availableCategories
.map((category) => CategoryGridItem(
category: category,
onSelectCategory: () {
_selectCategory(context, category);
},
))
.toList(),
);
}
}7. Creating Meals Screen
As the name suggests, it will show the list of meals corresponding to a specific category. Here, we are importing the Meal model we created above and the meal_item widget, which we will make in the helper widgets section.
Here, we accept the title, the category name, the list of meals, and the onToggleFavorite function through the constructor function.
In the build function, we store the widget for a list of meals using ListView.builder in the content variable. We then check if the list of meals is empty; we replace the content with another widget, which shows that there is nothing here, and try different categories using the Text widget. The Text widgets are arranged using the Column and Center widgets.
Finally, in the build, we are returning the Scaffold widget with an appBar and the content variable in the body parameter.
import 'package:flutter/material.dart';
import '../models/meal.dart';
import '../widgets/meal_item.dart';
class MealsScreen extends StatelessWidget {
const MealsScreen(
{super.key,
this.title,
required this.meals,
required this.onToggleFavorite});
final String? title;
final List<Meal> meals;
final void Function(Meal meal) onToggleFavorite;
@override
Widget build(BuildContext context) {
Widget content = ListView.builder(
itemCount: meals.length,
itemBuilder: (context, index) {
return MealItem(
meal: meals[index],
onToggleFavorite: onToggleFavorite,);
},
);
if (meals.isEmpty) {
content = Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Uh oh.....Nothing here!',
style: Theme.of(context).textTheme.headlineLarge!.copyWith(
color: Theme.of(context).colorScheme.onBackground)),
const SizedBox(
height: 16,
),
Text(
'You can try some other category!',
style: Theme.of(context)
.textTheme
.bodyLarge!
.copyWith(color: Theme.of(context).colorScheme.onBackground),
)
],
),
);
}
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: content);
}
}8. Creating Meal Details Screen
This screen opens when the user selects a particular meal. It shows the details of that meal. Here, we are accepting the meal about which to show and the onToggleFavorite function through the constructor function.
Finally, in the build function, we are returning a Scaffold widget, inside which we have defined the appBar, which shows the title of the meal.
In the body parameter, we are returning the content using the Column widget inside the ‘SingleChildScrollView’ widget to make the content scrollable. In the column widget, we are returning Image on top, using Image.network. Below it we are showing the ingredients, where each ingredient is shown using the Text widget by looping over the list of ingredients. And the the bottom, we are showing the steps to prepare the meal. It is also shown in the same way as ingredients, where each step is shown using Text widget by looping over list of steps. Each content shown on the screen is separated using SizedBox widget.
import 'package:flutter/material.dart';
import '../models/meal.dart';
class MealDetailsScreen extends StatelessWidget {
const MealDetailsScreen(
{super.key, required this.meal, required this.onToggleFavorite});
final Meal meal;
final void Function(Meal meal) onToggleFavorite;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(meal.title),
actions: [
IconButton(
onPressed: () {
onToggleFavorite(meal);
},
icon: const Icon(Icons.star))
],
),
body: SingleChildScrollView(
child: Column(
children: [
Image.network(
meal.imageUrl,
height: 300,
width: double.infinity,
fit: BoxFit.cover,
),
const SizedBox(
height: 16,
),
Text(
'Ingredients',
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(color: Theme.of(context).colorScheme.primary),
),
const SizedBox(
height: 14,
),
for (final ingredient in meal.ingredients)
Text(
ingredient,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
const SizedBox(
height: 16,
),
Text(
'Steps',
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(color: Theme.of(context).colorScheme.primary),
),
const SizedBox(
height: 14,
),
for (final step in meal.steps)
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Text(
step,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
)
],
),
),
);
}
}9. Creating Filters Screen
This is the screen that opens when the user selects ‘Filter’ on the MainDrawer. Here we have created an enum on top of the file which is Filter, which takes one of the following values:- glutenFree, lactoseFree, vegetarian, vegan.
It is a StatfulWidget as the state needs to be rendered again when the user modifies various filter options. Here we are accepting the currentFilters through the constructor function. We have also created variables like _glutenFreeFilterState, _lactoseFreeFilterState, _vegetarianFilterState, and _veganFilterState which are all set to false and represent the state of different filter options.
We are also overriding the initState where we are setting the state of different filter options equal to the currentFilters as selected by the user.
Finally, in the build function, we are returning the Scaffold widget, inside which we have defined the appBar. In the body argument, we are returning WillPopScope widget, which is used so that when this screen closes, it can return the state of filters.
In the child argument, we are returning the Column widget, within which we are showing SwitchListTile for each filter. In each of the SwitchListTile, there is title and subtitle defined and the value is the state of the filter and when user clicks on the switch button, it toggles the state of the filter.
import 'package:flutter/material.dart';
enum Filter { glutenFree, lactoseFree, vegetarian, vegan }
class FiltersScreen extends StatefulWidget {
const FiltersScreen({super.key, required this.currentFilters});
final Map<Filter, bool> currentFilters;
@override
State<FiltersScreen> createState() {
return _FiltersScreenState();
}
}
class _FiltersScreenState extends State<FiltersScreen> {
var _glutenFreeFilterState = false;
var _lactoseFreeFilterState = false;
var _vegetarianFilterState = false;
var _veganFilterState = false;
@override
void initState() {
super.initState();
_glutenFreeFilterState = widget.currentFilters[Filter.glutenFree]!;
_lactoseFreeFilterState = widget.currentFilters[Filter.lactoseFree]!;
_vegetarianFilterState = widget.currentFilters[Filter.vegetarian]!;
_veganFilterState = widget.currentFilters[Filter.vegan]!;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Your Filters'),
),
body: WillPopScope(
onWillPop: () async {
Navigator.of(context).pop({
Filter.glutenFree: _glutenFreeFilterState,
Filter.lactoseFree: _lactoseFreeFilterState,
Filter.vegetarian: _vegetarianFilterState,
Filter.vegan: _veganFilterState
});
return false;
},
child: Column(
children: [
SwitchListTile(
value: _glutenFreeFilterState,
onChanged: (isChecked) {
setState(() {
_glutenFreeFilterState = isChecked;
});
},
title: Text(
'Gluten-Free',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
subtitle: Text(
'Only include gluten-free meals.',
style: Theme.of(context).textTheme.labelMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
activeColor: Theme.of(context).colorScheme.tertiary,
contentPadding: const EdgeInsets.only(left: 34, right: 22),
),
SwitchListTile(
value: _lactoseFreeFilterState,
onChanged: (isChecked) {
setState(() {
_lactoseFreeFilterState = isChecked;
});
},
title: Text(
'Lactose-Free',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
subtitle: Text(
'Only include lactose-free meals.',
style: Theme.of(context).textTheme.labelMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
activeColor: Theme.of(context).colorScheme.tertiary,
contentPadding: const EdgeInsets.only(left: 34, right: 22),
),
SwitchListTile(
value: _vegetarianFilterState,
onChanged: (isChecked) {
setState(() {
_vegetarianFilterState = isChecked;
});
},
title: Text(
'Vegetarian',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
subtitle: Text(
'Only include vegetarian meals.',
style: Theme.of(context).textTheme.labelMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
activeColor: Theme.of(context).colorScheme.tertiary,
contentPadding: const EdgeInsets.only(left: 34, right: 22),
),
SwitchListTile(
value: _veganFilterState,
onChanged: (isChecked) {
setState(() {
_veganFilterState = isChecked;
});
},
title: Text(
'Vegan',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
subtitle: Text(
'Only include vegan meals.',
style: Theme.of(context).textTheme.labelMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground),
),
activeColor: Theme.of(context).colorScheme.tertiary,
contentPadding: const EdgeInsets.only(left: 34, right: 22),
)
],
),
),
);
}
}10. Building Helper Widgets
A. Category Grid Item
This widget is used to show each category in the Categories Screen. Here, we are accepting the category to show and the onSelectCategory function, which we created in the Categories Screen, through the constructor function.
In the build function, we are returning the InkWell widget, which is similar to a button but has additional design properties, as it shows a splash effect when the user taps on it. Here, we have set the splashColor using the ThemeData we created and the borderRadius to give a nice look. In the child argument, we are returning a Container widget with padding and decoration through which we have a LinearGradient effect. Inside it, we are showing the category using the Text widget. On tapping, it executes the onSelectCategory function, which we accepted through the constructor function.
import 'package:flutter/material.dart';
import '../models/category.dart';
class CategoryGridItem extends StatelessWidget {
const CategoryGridItem(
{required this.category, required this.onChoosingCategory, super.key});
final Category category;
final void Function() onChoosingCategory;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onChoosingCategory,
splashColor: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
gradient: LinearGradient(colors: [
category.color.withOpacity(0.5),
category.color.withOpacity(0.9)
], begin: Alignment.topLeft, end: Alignment.bottomRight),
borderRadius: BorderRadius.circular(16),
),
child: Text(
category.title,
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(color: Theme.of(context).colorScheme.onBackground),
),
),
);
}
}B. Meal Item
This widget is used to show each meal in the MealsScreen. Here, we import the transparent_image package we installed while making changes in the pubsec.yaml file. We are also importing the meal model and the meal_details_screen, which we created above, and the meal_item_labeler widget, which we will create next.
It is a Stateless Widget where we accept the meal object about which to show. For the logic of the app, we have defined a few functions:-
a. complexityText—It is a getter function that converts the complexity enum of the meal class so that the first letter is UpperCase and the rest is lower.
b. affordabilityText—It is also a getter function that converts the affordability enum of the meal class so that the first letter is UpperCase and the rest is lower.
c. onSelectMeal—This function is executed when a user selects a meal. It opens the MealDetailsScreen for that meal using Navigator.push, which navigates to the new screen.
In the build function, we are returning a Card widget, where we have set the margin, elevation, shape, and ClipBehavior to give it a nice look. In the child, we are returning the InkWell widget, which executes the onSelectMeal function on tap. In the child parameter, we are returning the Stack widget. The stack widget places the children’s widgets on top of each other.
Here in the children property of Stack, we are showing images using FadeInImage widget which shows the image in fade-in manner and as a placeholder we are using the transparent image. Above it, we are showing the content using a Column widget inside a Positioned widget, to give space from all sides. In the children argument of the Column widget, we are showing the Title of the meal and below it, with the help of Row widget, with its mainAxisAlignment as centre, we are showing duration, complexity, and affordability using the help of MealItemLabeler which we will create next.
import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';
import '../models/meal.dart';
import './meal_item_labeler.dart';
import '../screens/meal_details_screen.dart';
class MealItem extends StatelessWidget {
const MealItem({super.key, required this.meal});
final Meal meal;
String get complexityText {
return meal.complexity.name[0].toUpperCase() +
meal.complexity.name.substring(1);
}
String get affordabilityText {
return meal.affordability.name[0].toUpperCase() +
meal.affordability.name.substring(1);
}
void onSelectMeal(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) {
return MealDetailsScreen(
meal: meal,
);
},
),
);
}
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(8),
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () {
onSelectMeal(context);
},
child: Stack(
children: [
FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image: NetworkImage(meal.imageUrl),
fit: BoxFit.cover,
height: 200,
width: double.infinity,
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 40),
color: Colors.black54,
child: Column(
children: [
Text(
meal.title,
maxLines: 2,
textAlign: TextAlign.center,
softWrap: true,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white),
),
const SizedBox(
height: 16,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MealItemLabeler(
icon: Icons.schedule,
label: '${meal.duration} min'),
const SizedBox(
width: 12,
),
MealItemLabeler(
icon: Icons.work, label: complexityText),
const SizedBox(
width: 12,
),
MealItemLabeler(
icon: Icons.schedule, label: affordabilityText),
],
)
],
),
))
],
),
),
);
}
}C. Meal Item Labeler
This is a custom widget that we are using in the Meal Item widget, as seen above. It shows information about duration or affordability.
Here, we are accepting two arguments, an icon and a label, through the constructor function.
Finally, with the help of the build function, we return the icon alongside the label with the help of the Row widget, where there is a gap between the icon and the label. This widget is used at several places in Meal Item, making our code less redundant.
import 'package:flutter/material.dart';
class MealItemLabeler extends StatelessWidget {
const MealItemLabeler({super.key, required this.icon, required this.label});
final IconData icon;
final String label;
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(
icon,
size: 17,
color: Colors.white,
),
const SizedBox(
width: 6,
),
Text(
label,
style: const TextStyle(color: Colors.white),
)
],
);
}
}Flutter Meals App Output
Conclusion
Now, you can look up recipes for various cuisines using your meals_app. In this Flutter meals app project, we learned a lot of widgets and used various packages. We installed the google_fonts package to access multiple Google fonts, which we used in setting up our textTheme and used in different places in our app. We also installed transparent_image package, which we use in the Meal Item widget as a placeholder for the image. We also got to implement widgets like InkWell, Stack, Navigator, GridView, SingleChildScrollView, etc.
I hope you enjoyed working on this project!
Thank you for reading! Keep Learning Flutter!
If you are Happy with DataFlair, do not forget to make us happy with your positive feedback on Google






