Build a Recipe App with Flutter  🍳
Tags: flutter, api, beginners
Published: 2020-11-26 19:06:10

{% youtube VTR5HpRfS0A %}

Let’s make a recipe app with flutter, basically learning how to build full app with flutter, also learning how to use API in a flutter app, It is a good flutter project to add to cv.

If you like to learn via video then you can [watch the video](https://youtu.be/VTR5HpRfS0A)


📕 Things covered in this Series:

* Fetch data from an API with flutter
* How useState in a flutter app
* How to pass down data in a flutter widget
* Using Custom Font in Flutter App
* How to show a grid view and much more.
Prerequisites :

[Install Flutter](https://flutter.dev/docs/get-started/install) if not already.

Source code:

{% github theindianappguy/recipe_app %}

So let’s get started coding.

**Step 1:** Create a flutter project,
I am going to do it with Android studio you can user terminal/cmd or VS code there will be no difference.

**Step 2:** Code Cleanup
We will start by getting rid of all the comments in the main.dart and getting rid of MyHomePage() stateful widget

**Step 3:** Install the required package
http // for using api.
webview_flutter // for webview in mobile app.
url_launcher // for website to open recipe url in new tab.
to install just add then to pubspec.yaml file below cupertino_icons and then for vs code CTRL + S (save) , Android Studio click on Packages get at the top right hand corner.

**Step 4:** Create home.dart
inside views folder and add it to main.dart like this.

```
import 'package:flutter/material.dart';
import 'package:recipe_app/views/home.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Recipe',
      debugShowCheckedModeBanner: false,
      home: Home(),
    );
  }
}

```

**Step 5:** Let’s create the User Interface for our app now.
As a beginner to understand all the widgets (everything is widget in flutter image, text, container…..) check out flutter official Widget catalog : https://flutter.dev/docs/development/ui/widgets

Let’s discuss most important once i will recommend to watch the video to understand better.

### What is Scaffold?

It’s a wrapper which provides prebuild widgets like appbar, floating action button, drawer and much more so we don’t have to create all of them from scratch.

What is Stack?

We use stack to put one widget above another think of it like a sandwich but instead of brads we have some widgets, it can be text above image, may be full app above custom image/background and many more

What is Column?

Column are used to arrange widgets in Vertical Manner one below the other.

What is Row?

Row are used to arrange widget in Horizontal Manner one after the other.

What is Sizedbox?

Sizedbox are used to create space in our app that can be space between between the image and the text and text and text and in general with any widgets.

How to use Gradient Color background?

For Gradient color we use Box Decoration property if Container which have a Linear Gradient property.

Furthermore we have used Textedit Controller to keep track of what user is typing in the field. I have also used a custom font Overpass(i really like this one) so next step will be to add it as well.

This all sums up what we have built in home.dart file. ( did i miss anything? let me know in the comments)

```
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:recipe_app/models/recipe_model.dart';
import 'package:recipe_app/views/recipe_view.dart';
import 'package:url_launcher/url_launcher.dart';

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  List<RecipeModel> recipies = new List();
  String ingridients;
  bool _loading = false;
  String query = "";
  TextEditingController textEditingController = new TextEditingController();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          Container(
            height: MediaQuery.of(context).size.height,
            width: MediaQuery.of(context).size.width,
            decoration: BoxDecoration(
                gradient: LinearGradient(
                    colors: [
                  const Color(0xff213A50),
                  const Color(0xff071930)
                ],
                    begin: FractionalOffset.topRight,
                    end: FractionalOffset.bottomLeft)),
          ),
          SingleChildScrollView(
            child: Container(
              padding: EdgeInsets.symmetric(vertical: !kIsWeb ? Platform.isIOS? 60: 30 : 30, horizontal: 24),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Row(
                    mainAxisAlignment: kIsWeb
                        ? MainAxisAlignment.start
                        : MainAxisAlignment.center,
                    children: <Widget>[
                      Text(
                        "AppGuy",
                        style: TextStyle(
                            fontSize: 18,
                            color: Colors.white,
                            fontFamily: 'Overpass'),
                      ),
                      Text(
                        "Recipes",
                        style: TextStyle(
                            fontSize: 18,
                            color: Colors.blue,
                            fontFamily: 'Overpass'),
                      )
                    ],
                  ),
                  SizedBox(
                    height: 60,
                  ),
                  Text(
                    "What will you cook today?",
                    style: TextStyle(
                        fontSize: 20,
                        color: Colors.white,
                        fontWeight: FontWeight.w400,
                        fontFamily: 'Overpass'),
                  ),
                  Text(
                    "Just Enter Ingredients you have and we will show the best recipe for you",
                    style: TextStyle(
                        fontSize: 15,
                        color: Colors.white,
                        fontWeight: FontWeight.w300,
                        fontFamily: 'OverpassRegular'),
                  ),
                  SizedBox(
                    height: 40,
                  ),
                  Container(
                    child: Row(
                      children: <Widget>[
                        Expanded(
                          child: TextField(
                            controller: textEditingController,
                            style: TextStyle(
                                fontSize: 16,
                                color: Colors.white,
                                fontFamily: 'Overpass'),
                            decoration: InputDecoration(
                              hintText: "Enter Ingridients",
                              hintStyle: TextStyle(
                                  fontSize: 16,
                                  color: Colors.white.withOpacity(0.5),
                                  fontFamily: 'Overpass'),
                              enabledBorder: UnderlineInputBorder(
                                borderSide: BorderSide(color: Colors.white),
                              ),
                              focusedBorder: UnderlineInputBorder(
                                borderSide: BorderSide(color: Colors.white),
                              ),
                            ),
                          ),
                        ),
                        SizedBox(
                          width: 16,
                        ),
                        InkWell(
                            onTap: () async {
                              if (textEditingController.text.isNotEmpty) {
                                setState(() {
                                  _loading = true;
                                });
                                recipies = new List();
                                String url =
                                    "https://api.edamam.com/search?q=${textEditingController.text}&app_id=0f21d949&app_key=8bcdd93683d********5cb95e64ab26";
                                var response = await http.get(url);
                                print(" $response this is response");
                                Map<String, dynamic> jsonData =
                                    jsonDecode(response.body);
                                print("this is json Data $jsonData");
                                jsonData["hits"].forEach((element) {
                                  print(element.toString());
                                  RecipeModel recipeModel = new RecipeModel();
                                  recipeModel =
                                      RecipeModel.fromMap(element['recipe']);
                                  recipies.add(recipeModel);
                                  print(recipeModel.url);
                                });
                                setState(() {
                                  _loading = false;
                                });

                                print("doing it");
                              } else {
                                print("not doing it");
                              }
                            },
                            child: Container(
                              decoration: BoxDecoration(
                                borderRadius: BorderRadius.circular(8),
                                  gradient: LinearGradient(
                                      colors: [
                                    const Color(0xffA2834D),
                                    const Color(0xffBC9A5F)
                                  ],
                                      begin: FractionalOffset.topRight,
                                      end: FractionalOffset.bottomLeft)),
                              padding: EdgeInsets.all(8),
                              child: Row(
                                mainAxisSize: MainAxisSize.min,
                                children: <Widget>[
                                  Icon(
                                    Icons.search,
                                    size: 18,
                                      color: Colors.white
                                  ),
                                ],
                              ),
                            )),
                      ],
                    ),
                  ),
                  SizedBox(
                    height: 30,
                  ),
                  Container(
                    child: GridView(
                        gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
                            mainAxisSpacing: 10.0, maxCrossAxisExtent: 200.0),
                        shrinkWrap: true,
                        scrollDirection: Axis.vertical,
                        physics: ClampingScrollPhysics(),
                        children: List.generate(recipies.length, (index) {
                          return GridTile(
                              child: RecipieTile(
                            title: recipies[index].label,
                            imgUrl: recipies[index].image,
                            desc: recipies[index].source,
                            url: recipies[index].url,
                          ));
                        })),
                  ),
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

class RecipieTile extends StatefulWidget {
  final String title, desc, imgUrl, url;

  RecipieTile({this.title, this.desc, this.imgUrl, this.url});

  @override
  _RecipieTileState createState() => _RecipieTileState();
}

class _RecipieTileState extends State<RecipieTile> {
  _launchURL(String url) async {
    print(url);
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw 'Could not launch $url';
    }
  }

  @override
  Widget build(BuildContext context) {
    return Wrap(
      children: <Widget>[
        GestureDetector(
          onTap: () {
            if (kIsWeb) {
              _launchURL(widget.url);
            } else {
              print(widget.url + " this is what we are going to see");
              Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) => RecipeView(
                            postUrl: widget.url,
                          )));
            }
          },
          child: Container(
            margin: EdgeInsets.all(8),
            child: Stack(
              children: <Widget>[
                Image.network(
                  widget.imgUrl,
                  height: 200,
                  width: 200,
                  fit: BoxFit.cover,
                ),
                Container(
                  width: 200,
                  alignment: Alignment.bottomLeft,
                  decoration: BoxDecoration(
                      gradient: LinearGradient(
                          colors: [Colors.white30, Colors.white],
                          begin: FractionalOffset.centerRight,
                          end: FractionalOffset.centerLeft)),
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          widget.title,
                          style: TextStyle(
                              fontSize: 13,
                              color: Colors.black54,
                              fontFamily: 'Overpass'),
                        ),
                        Text(
                          widget.desc,
                          style: TextStyle(
                              fontSize: 10,
                              color: Colors.black54,
                              fontFamily: 'OverpassRegular'),
                        )
                      ],
                    ),
                  ),
                )
              ],
            ),
          ),
        ),
      ],
    );
  }
}

class GradientCard extends StatelessWidget {
  final Color topColor;
  final Color bottomColor;
  final String topColorCode;
  final String bottomColorCode;

  GradientCard(
      {this.topColor,
      this.bottomColor,
      this.topColorCode,
      this.bottomColorCode});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Wrap(
        children: <Widget>[
          Container(
            child: Stack(
              children: <Widget>[
                Container(
                  height: 160,
                  width: 180,
                  decoration: BoxDecoration(
                      gradient: LinearGradient(
                          colors: [topColor, bottomColor],
                          begin: FractionalOffset.topLeft,
                          end: FractionalOffset.bottomRight)),
                ),
                Container(
                  width: 180,
                  alignment: Alignment.bottomLeft,
                  decoration: BoxDecoration(
                      gradient: LinearGradient(
                          colors: [Colors.white30, Colors.white],
                          begin: FractionalOffset.centerRight,
                          end: FractionalOffset.centerLeft)),
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Column(
                      children: <Widget>[
                        Text(
                          topColorCode,
                          style: TextStyle(fontSize: 16, color: Colors.black54),
                        ),
                        Text(
                          bottomColorCode,
                          style: TextStyle(fontSize: 16, color: bottomColor),
                        )
                      ],
                    ),
                  ),
                )
              ],
            ),
          ),
        ],
      ),
    );
  }
}
```

**Step 6:** Add Custom Font to Flutter App:
First download the font .otf files from my github repo : here and add then to the assets/ fonts folder

then mention them in the pubspec.yaml file like this. check the full pubspec.yaml file here

```
 fonts:
    - family: Overpass
      fonts:
       - asset: assets/fonts/overpass_bold.otf
    - family: OverpassRegular
      fonts:
        - asset: assets/fonts/overpass_regular.otf
```

**Step 7:** Using Api in Flutter App
now we will add the Edamam Api to use their database of recipes and show when requested in out app

First Signup : https://developer.edamam.com/edamam-recipe-api
Click on Dashboard > Applications > View > Copy the application key.
**Step8:** Call the api and get data on search in the app so when app user submit something and click on search we will run this onTap:

code is already added in the top home.dart file so now if you will run it should work. Just make sure to update the Api Key

**Step 9:** Add Web View in the App.
it’s pretty simple to show web view the extra code is just the app bar, also i faced this issue that only https website were visible in webview so i made a custom function to redirect http sites to https. Here is the code just create a new file in views recipe_view.dart

```
import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class RecipeView extends StatefulWidget {
  final String postUrl;
  RecipeView({@required this.postUrl});

  @override
  _RecipeViewState createState() => _RecipeViewState();
}

class _RecipeViewState extends State<RecipeView> {

  final Completer<WebViewController> _controller =
      Completer<WebViewController>();

  String finalUrl ;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    finalUrl = widget.postUrl;
    if(widget.postUrl.contains('http://')){
      finalUrl = widget.postUrl.replaceAll("http://","https://");
      print(finalUrl + "this is final url");
    }

  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Column(
          children: <Widget>[
            Container(
              padding: EdgeInsets.only(top: Platform.isIOS? 60: 30, right: 24,left: 24,bottom: 16),
              width: MediaQuery.of(context).size.width,
              decoration: BoxDecoration(
                  gradient: LinearGradient(
                      colors: [
                        const Color(0xff213A50),
                        const Color(0xff071930)
                      ],
                      begin: FractionalOffset.topRight,
                      end: FractionalOffset.bottomLeft)),
              child:  Row(
                mainAxisAlignment: kIsWeb
                    ? MainAxisAlignment.start
                    : MainAxisAlignment.center,
                children: <Widget>[
                  Text(
                    "AppGuy",
                    style: TextStyle(
                        fontSize: 18,
                        color: Colors.white,
                        fontFamily: 'Overpass'),
                  ),
                  Text(
                    "Recipes",
                    style: TextStyle(
                        fontSize: 18,
                        color: Colors.blue,
                        fontFamily: 'Overpass'),
                  )
                ],
              ),
            ),
            Container(
              height: MediaQuery.of(context).size.height - (Platform.isIOS ? 104 : 30),
              width: MediaQuery.of(context).size.width,
              child: WebView(
                onPageFinished: (val){
                  print(val);
                },
                javascriptMode: JavascriptMode.unrestricted,
                initialUrl: finalUrl,
                onWebViewCreated: (WebViewController webViewController){
                  setState(() {
                    _controller.complete(webViewController);
                  });
                },
              ),
            ),
          ],
        ),
      )
    );
  }

```
view rawrecipe_app.dart hosted with ❤ by GitHub
Run the app and test if it is running fine any problems? Google it 😅 (it works for me),……. ohhh still not solved comment it down below all the FlutterNerd community will love to help.

**Step 10:** Running Flutter App On Web.
First make sure you are in beta channel to run flutter web apps step by step > here <

Then just run the app selecting chrome voila 🥳 it works it’s that simple no setup required.

If you are facing problem i will highly recommend to follow the video it’s better and i tried to explain each step visually.

🎓 More tutorials you may like

{% youtube FTju8w4zEno %}

1.Build a Fully Functioning Flutter Chat App with Firebase
In this one you learn to build a fully functioning chat app with one-2-one real time chat search user like Insta DM.

👉 https://flutternerd.com/build-a-fully-functioning-flutter-chat-app-with-firebase-part-1-4/

{% youtube EKdAU3l_0gA %}

2.Build a Wallpaper App with Flutter
In this You will learn how to build a Wallpaper app with Flutter this is a great project to add to portfolio

https://flutternerd.com/build-a-wallpaper-app-with-flutter/

{% youtube aaGcER1uUoE %}

3.Build a Flutter News App with NewsApi Org
In this project we will use NewsAPI.org and build a news app with it

https://flutternerd.com/news-app-with-newsapi-org-flutter/