July 12, 2021

Building a Food Recipe App From Scratch with Firebase and Spoonacular API

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.

Flutter gigs banner

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

Getting Started

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:

image

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/

Building the UI

UI Breakdown

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:

Bottom Navigation Bar

image

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;
   });
 }
}
  1. These are the screen widgets that will be displayed when the navigation icons are tapped

  2. _selectedIndex to keep track of currently selected bottom navigation item

  3. _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.

image

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();
  }
}

Let’s Build the Search Screen

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),
),

image

Search bar

  • A card-like widget to display the recipe with few details.

image

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',
        ),
     ],
    ),
  ),
),

image

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, ),
    );
  }
}

image

Save Recipe Screen

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',
                    ),
                  ),
                )
             ],
            ),
          ),
        ),
     ],
    );
  }
}

image

Where to go from here?

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!

LOOKING FOR WORK?

CHECK OUT TOPCODER FLUTTER FREELANCE GIGS

Click to show preference!
Click to show preference!