This is a four-part series in which I will guide you through building a food app from scratch in Flutter. After all, who doesn’t like food?
The series will be divided in the following fashion:
Building the UI
Connecting the Spoonacular API to search recipe screen
Understanding state management using favorite list screen
Saving your recipes to Firestore
This is the first part of the series in which I will break down the UI of the app.
We’re going to use an API later on for obtaining data. Since we are focusing on the user interface at this stage of the project, dummy data is provided to continue with the implementation.
What you will learn in this series
Building UI in Flutter
State management using stateNotifierProvider and freezed
To fetch data using API
To save/fetch data from firebase
Prerequisites:
The latest version of either Android Studio or VS Code installed along with Flutter and Dart plugins/extensions.Basic concepts of Dart and the latest version of Flutter (null-safety).
I will be using Android Studio for this tutorial. If you haven’t already installed your IDE please follow the instructions below:
For macOS: https://flutter.dev/docs/get-started/install/macos
For Windows: https://flutter.dev/docs/get-started/install/windows
Go ahead and add the following command in the terminal to create your project and open the file on your IDE.
flutter create food_recipe_app
Open Android Studio and set up your emulator using the following instructions:
https://developer.android.com/studio/run/emulator
Hit the run button. You must be able to see the counter screen as below:
In the main/lib file replace the entire code with:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.orangeAccent,
),
home: MainPage(),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({
Key key
}): super(key: key);
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State < MainPage > {
@override
Widget build(BuildContext context) {
return Container();
}
}
Download any five food images of your choice, make a folder named images and add the images to this folder. In pubspec.yaml file register the images folder.
assets:
- images/
The UI consists of three screens:
Screen for searching recipes using Spoonacular API
A favorite page for fav list of recipes
Screen for saving recipes to Firestore.
Let’s start by adding the bottom navigation bar which consists of the following pages:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class MainPage extends StatefulWidget {
const MainPage({
Key ? key
}): super(key: key);
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State < MainPage > {
// 1
final List < Widget > _pages = <Widget>[
SearchPopular(),
FavListPage(),
SaveRecipe()
];
//2
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
body: Center(
child: _pages.elementAt(_selectedIndex), //New
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: _onItemTapped,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.restaurant, color: Colors.orangeAccent),
label: '',
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite_border, color: Colors.orangeAccent),
label: '',
),
BottomNavigationBarItem(
icon: Icon(Icons.save, color: Colors.orangeAccent),
label: '',
)
],
),
),
);
}
//3
void _onItemTapped(int index) {
print(index);
setState(() {
_selectedIndex = index;
});
}
}
These are the screen widgets that will be displayed when the navigation icons are tapped
_selectedIndex to keep track of currently selected bottom navigation item
_onItemTapped method is used to update the state of _selectedIndex when items of the bottom navigation bar are tapped.
Keep a clean architecture, make a directory called views, and add three files for each of the above pages.
For the time being, add this to each of the files with their respective names. The widgets contain only a container. We will work on each page as we progress.
1
2
3
4
5
6
7
8
9
10
class FavPageList extends StatelessWidget {
const FavPageList({
Key ? key
}): super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
}
This page consists of
A search bar for searching recipes
List of card-like widgets for displaying the recipe details
The UI for the search bar involves a text field with properties such as suffix search icon, rounded border, and hint text to display the search here text.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
TextField(
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
borderSide: BorderSide(
width: 2,
color: Colors.orangeAccent,
),
),
enabledBorder: const OutlineInputBorder(
borderRadius: const BorderRadius.all(
const Radius.circular(10.0),
),
borderSide: const BorderSide(
color: Colors.orangeAccent, width: 2.0),
),
suffixIcon: Icon(
Icons.search,
color: Colors.black,
),
filled: true,
hintStyle: new TextStyle(color: Colors.grey[800]),
hintText: "Search here",
fillColor: Colors.white70),
),
Search bar
A card-like widget to display the recipe with few details.
Let’s create a common label for our card widget to label the details as seen above. The label contains positioned properties to position the label at any desired position.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Label extends StatelessWidget {
final double ? left;
final double ? right;
final double ? top;
final double ? bottom;
final String label;
const Label({
Key ? key,
this.left,
this.right,
this.top,
this.bottom,
required this.label,
}): super(key: key);
@override
Widget build(BuildContext context) {
return Positioned(
left: left,
bottom: bottom,
right: right,
top: top,
child: Container(
height: 30,
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.all(
Radius.circular(
8,
),
),
),
child: Center(
child: Text(
label,
style: TextStyle(
color: Colors.white,
),
),
),
),
);
}
}
We will display the recipe name, rating and time required using the above label widget and add it to our container widget to shape a beautiful recipe card as seen above.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Container(
child: Padding(
padding: EdgeInsets.only(bottom: 16),
child: Stack(
children: [
Container(
height: 250,
width: double.infinity,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.asset(
'images/food2.jpeg',
fit: BoxFit.cover,
),
),
),
Label(
left: 10,
top: 10,
label: 'Chicken Soup',
),
Label(
left: 10,
bottom: 10,
label: 'Rating : 5 Stars',
),
Label(
right: 10,
bottom: 10,
label: 'Time Required : 20 minutes',
),
],
),
),
),
Favorite Recipes Screen
This page will consist of saved recipes that are liked by us.
For the UI, I will use page view for a little fancy-looking UI (because why not?).
A viewport of 0.8 to show a glimpse of the previous and next page.
The code is fairly simple as below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import 'package:flutter/material.dart';
class FavListPage extends StatelessWidget {
const FavListPage({
Key ? key
}): super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Favorite List',
),
),
body: PageView(
controller: PageController(
initialPage: 1,
viewportFraction: 0.8,
),
children: [
_ImageContainer(
image: 'images/pasta.jpeg',
),
_ImageContainer(
image: 'images/cream.jpeg',
),
_ImageContainer(
image: 'images/food3.jpeg',
),
],
),
);
}
}
class _ImageContainer extends StatelessWidget {
final String image;
const _ImageContainer({
Key ? key,
required this.image
}): super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Image.asset(image, fit: BoxFit.fill, ),
);
}
}
These are the text fields I will be adding to this screen:
Recipe title
The procedure
Time required
A camera icon for:
Capturing the completed recipe
I created a common text field with custom properties and class properties for label and MaxLine of textField.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class TextField extends StatelessWidget {
final String label;
final int maxLine;
const TextField({
Key ? key,
required this.label,
required this.maxLine,
}): super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: TextFormField(
maxLines: maxLine,
decoration: InputDecoration(
labelText: label, filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(25.0),
borderSide: BorderSide(),
),
//fillColor: Colors.green
),
style: TextStyle(
fontFamily: "Poppins",
),
),
);
}
}
I will be using a stack widget to stack text fields above a background image.
The class will look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class SaveRecipe extends StatelessWidget {
const SaveRecipe({
Key ? key
}): super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
children: [
Container(
height: double.infinity,
child: Image.asset(
'images/food3.jpeg',
),
),
Scaffold(
appBar: AppBar(
title: Text('Save recipe'),
),
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent,
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.camera_alt,
color: Colors.white,
),
TextField(
label: 'Recipe name',
maxLine: 1,
),
SizedBox(
height: 16,
),
TextField(
label: 'Time required',
maxLine: 1,
),
SizedBox(
height: 16,
),
TextField(
label: 'Description',
maxLine: 10,
),
SizedBox(
height: 16,
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(30.0),
),
primary: Colors.orangeAccent,
padding:
EdgeInsets.symmetric(horizontal: 50, vertical: 20),
textStyle: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
onPressed: () {},
child: Text(
'Save my recipe',
),
),
)
],
),
),
),
],
);
}
}
You can download the entire code from here https://github.com/huma11farheen/recipe_app_with_firebase
There you have the UI for the recipe app. In the next article, you will learn about connecting API to the search screen.
That’s all for this article, stay tuned for the next one. Happy coding!
CHECK OUT TOPCODER FLUTTER FREELANCE GIGS