使用 Flutter 构建食谱应用 🍳 使用 Flutter 构建食谱应用

2025-06-07

使用 Flutter 构建食谱应用

使用 Flutter 构建食谱应用

让我们用 Flutter 制作一个食谱应用程序,基本上学习如何用 Flutter 构建完整的应用程序,还要学习如何在 Flutter 应用程序中使用 API,这是一个可以添加到 cv 中的很好的 Flutter 项目。

如果您喜欢通过视频学习,那么您可以观看视频

📕 本系列涵盖的内容:

  • 使用 Flutter 从 API 获取数据
  • Flutter 应用中如何使用 useState
  • 如何在 Flutter 小部件中传递数据
  • 在 Flutter 应用中使用自定义字体
  • 如何显示网格视图等等。先决条件:

如果尚未安装Flutter,请安装。

源代码:

GitHub 徽标 theindianappguy / Recipe_app

Flutter 食谱应用

Flutter 食谱应用

使用 Flutter 构建食谱应用

完整免费课程:https://youtu.be/VTR5HpRfS0A

在今天的课程中,我们将学习#flutter的基础知识,并创建一个真实的应用程序。我们将学习如何仅使用Flutter构建一个食谱应用程序。

📕 本视频涵盖的内容:

• 使用 Flutter 从 API 获取数据

• 如何在 Flutter 中 useState

• 如何在 Flutter 中通过 Widget 传递数据

• 如何显示网格视图

如果你是 Flutter 的初学者,我强烈建议你继续学习,因为你将学习如何将所有小部分组合在一起,以理解如何从 API 获取数据、如何更新状态等内容。




那么让我们开始编码吧。

步骤 1:创建一个 Flutter 项目,
我将使用 Android Studio 来完成,您可以使用终端/cmd 或 VS 代码,不会有任何区别。

步骤 2:代码清理
我们将首先删除 main.dart 中的所有注释,并删除 MyHomePage() 状态小部件

步骤 3:安装所需的包
http // 以使用 api。webview_flutter
// 用于移动应用中的 webview。url_launcher
// 用于网站在新选项卡中打开食谱 url。
要安装,只需将其添加到 cupertino_icons 下方的 pubspec.yaml 文件中,然后对于 vs 代码 CTRL + S(保存),Android Studio 单击右上角的“获取包”。

步骤 4:在 views 文件夹中创建 home.dart
并将其添加到 main.dart,如下所示。

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

Enter fullscreen mode Exit fullscreen mode

步骤5:现在让我们为应用程序创建用户界面。
作为初学者,为了了解所有小部件(Flutter 中的所有内容,包括图像、文本、容器……),请查看 Flutter 官方小部件目录:https://flutter.dev/docs/development/ui/widgets

让我们讨论最重要的事情,我建议观看视频以更好地理解。

什么是 Scaffold?

它是一个包装器,提供预构建的小部件,如应用栏、浮动操作按钮、抽屉等等,因此我们不必从头开始创建它们。

什么是 Stack?

我们使用堆栈将一个小部件放在另一个小部件上方,把它想象成一个三明治,但我们有一些小部件而不是图钉,它可以是图像上方的文本,可能是自定义图像/背景上方的完整应用程序等等

什么是 Column?

列用于以垂直方式将小部件一个接一个地排列。

Row 是什么?

行用于以水平方式一个接一个地排列小部件。

什么是 Sizedbox?

Sizedbox 用于在我们的应用程序中创建空间,可以是图像和文本之间、文本和文本之间的空间,也可以是任何小部件之间的空间。

如何使用渐变色背景?

对于渐变色,如果容器具有线性渐变属性,我们使用盒子装饰属性。

此外,我们还使用了 Textedit Controller 来跟踪用户在字段中的输入内容。我还使用了自定义字体 Overpass(我非常喜欢这个),所以下一步也要添加它。

以上就是我们在 home.dart 文件中构建的内容。(我是否遗漏了什么?请在评论中告诉我)

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),
                        )
                      ],
                    ),
                  ),
                )
              ],
            ),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

步骤 6:将自定义字体添加到 Flutter 应用:
首先从我的 github repo 下载字体 .otf 文件:这里,然后添加到 assets/fonts 文件夹

然后在 pubspec.yaml 文件中像这样提及它们。请在此处查看完整的 pubspec.yaml 文件

 fonts:
    - family: Overpass
      fonts:
       - asset: assets/fonts/overpass_bold.otf
    - family: OverpassRegular
      fonts:
        - asset: assets/fonts/overpass_regular.otf
Enter fullscreen mode Exit fullscreen mode

步骤 7:在 Flutter 应用程序中使用 Api,
现在我们将添加 Edamam Api 以使用其食谱数据库,并在应用程序中请求时显示

首次注册:https://developer.edamam.com/edamam-recipe-api
点击“仪表盘”>“应用程序”>“查看”>“复制应用程序密钥”。
步骤 8:调用 API 并获取应用中的搜索数据。因此,当应用用户提交内容并点击“搜索”时,我们将在 onTap 上运行此代码:

代码已添加到顶部 home.dart 文件中,因此现在运行应该可以正常工作。只需确保更新 Api 密钥即可。

步骤9:在应用中添加Web视图。
显示Web视图非常简单,额外的代码只是应用栏。此外,我还遇到了一个问题,Web视图中只能显示https网站,所以我创建了一个自定义函数,将http网站重定向到https网站。代码如下:在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);
                  });
                },
              ),
            ),
          ],
        ),
      )
    );
  }

Enter fullscreen mode Exit fullscreen mode

查看 rawrecipe_app.dart,托管于 GitHub,❤
运行应用测试是否正常运行,有什么问题吗?谷歌一下😅(对我来说没问题),……哦,还是没解决,请在下方评论,FlutterNerd 社区的所有人都很乐意提供帮助。

第十步:在 Web 上运行 Flutter 应用。
首先,请确保您处于 Beta 频道,以便逐步运行 Flutter Web 应用 > 此处 <

然后只需运行选择 chrome voila 的应用程序即可🥳它就可以工作了,就是这么简单,无需设置。

如果您遇到问题,我强烈建议您观看视频,这样更好,而且我尝试以直观的方式解释每个步骤。

🎓 您可能喜欢的更多教程

1. 使用 Firebase 构建功能齐全的 Flutter 聊天应用程序
在本课程中,您将学习如何构建一个功能齐全的聊天应用程序,该应用程序具有一对一实时聊天搜索用户功能,例如 Insta DM。

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

2.使用 Flutter 构建壁纸应用程序
在此您将学习如何使用 Flutter 构建壁纸应用程序这是一个可以添加到作品集的很棒的项目

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

3.使用 NewsApi Org 构建 Flutter 新闻应用程序
在这个项目中,我们将使用 NewsAPI.org 并使用它构建一个新闻应用程序

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

文章来源:https://dev.to/theindianappguy/build-a-recipe-app-with-flutter-4iko
PREV
适用于小型个人网站的快速简易 .htaccess .htaccess 是什么?.htaccess(以及共享主机)的局限性和缺点 常见用例速查表 调试和其他一般技巧 其他资源
NEXT
如何学习——大多数学校🎓不教什么是学习?