[Flutter学徒] 11 - Flutter中的网络

638 阅读10分钟

本文由 简悦SimpRead 转码,原文地址 www.raywenderlich.com

本章将教你如何从互联网上检索数据并将其存储在模型类中,whi......。

从网络上加载数据并将其显示在用户界面中,是应用程序的一项非常常见的任务。在上一章中,你学会了如何将JSON数据序列化。现在,你将继续这个项目,学习如何从网络中检索JSON数据。

注意:你也可以通过打开本章的starter项目重新开始。如果你选择这样做,记得点击获取依赖项按钮或从终端执行flutter pub get

在本章结束时,你将知道如何。

  • 注册一个菜谱API服务。
  • 按名称触发对食谱的搜索。
  • 将API返回的数据转换为模型类。

闲话少说,现在是时候开始了!

注册菜谱API

对于你的远程内容,你将使用Edam Recipe API。在你的浏览器中打开这个链接。developer.edamam.com/

点击右上方的SIGN UP按钮,选择Recipe Search API选项。

该页面将显示多个订阅选择。点击开发者栏中的START NOW按钮,选择免费选项。

在弹出的注册信息窗口中,输入你的信息并点击注册。你很快就会收到一封确认邮件。

一旦你收到电子邮件并验证了你的账户,返回网站并登录。在菜单栏上,点击**现在获取API密钥!**按钮。

接下来,点击创建一个新的应用程序按钮。

选择服务页面,点击食谱搜索API链接。

将出现一个新应用程序页面。在应用程序的名称中输入raywenderlich.com Recipes,在描述中输入An app to display raywenderlich.com recipes - 或者使用你喜欢的任何数值。当你完成后,按创建应用程序按钮。

一旦网站生成了API密钥,你会看到一个屏幕,上面有你的应用ID应用密钥

你以后会需要你的API密钥和ID,所以把它们保存在方便的地方或保持浏览器标签打开。现在,查看API文档,它提供了关于API的重要信息,包括路径、参数和返回的数据。

访问API文档

在窗口的顶部,右键单击API开发者门户链接,并选择在新标签中打开链接

在新标签中,点击文档菜单,选择食谱搜索API

这个页面有大量关于你要使用的API的信息。在顶部,你会看到路径和一个可用于你要进行的`GET'请求的参数列表。

这个页面上的API信息比你的应用程序所需要的要多得多,所以你可能想把它收藏起来以备将来使用。

使用你的API密钥

对于你的下一步,你需要使用你新创建的API密钥。

注意。免费的开发者版本的API是有速率限制的。如果你经常使用API,你可能会收到一些带有错误的JSON响应和警告你有限制的电子邮件。

如果你关闭了你的浏览器,请再次登录。点击仪表板按钮,然后从菜单栏中选择应用程序。你会看到类似这样的东西。

点击查看按钮,查看你的ID和钥匙(s)。

保持这个页面打开,这样你就可以把这些值复制到你的代码中。你的第一步是导入一个方便的包来执行HTTP请求。

准备Pubspec文件

打开你的项目或本章的起始项目。为了在这个应用中使用http包,你需要把它添加到pubspec.yaml中,所以打开该文件并在json_annotation包后添加以下内容。

http: ^0.12.2

点击Pub get按钮来安装包,或者从终端标签运行flutter pub get

使用HTTP包

HTTP包只包含几个文件和方法,你将在本章中使用。REST协议有如下方法。

  • GET。检索数据。
  • POST: 保存新的数据。
  • PUT: 更新数据。
  • DELETE: 删除数据。

你将使用GET,特别是HTTP包中的函数get(),来从API中检索配方数据。这个函数使用API的URL和一个可选标题列表,从API服务中检索数据。在这种情况下,你将通过查询参数发送所有信息,你不需要发送头信息。

连接到菜谱服务

为了从配方API中获取数据,你将创建一个文件来管理连接。这个文件将包含你的API密钥、ID和URL。

在项目侧边栏,右击lib/network,创建一个新的Dart文件,并命名为recipe_service.dart。文件打开后,导入HTTP包。

import 'package:http/http.dart'

现在,添加你在调用API时要用到的常量。

const String apiKey = '<Your Key>';
const String apiId = '<your ID>';
const String apiUrl = 'https://api.edamam.com/search';

从你的Edam账户中复制API ID和密钥,用你的值替换现有的apiKeyapiId分配的字符串。不要复制结尾的空格和破折号,如下图所示。

apiUrl常量持有Edam搜索API的URL,来自配方API文档。

还是在recipe_service.dart中加入以下函数,从API中获取数据。

// 1
Future getData(String url) async {
  // 2
  print('Calling url: $url');
  // 3
  final response = await get(url);
  // 4
  if (response.statusCode == 200) {
    // 5
    return response.body;
  } else {
    // 6
    print(response.statusCode);
  }
}

下面是对事情的分解。

  1. getData返回一个Future(大写 "F"),因为API返回的数据类型是在未来确定的(小写 "f")。async标志着这个方法是一个异步操作。

  2. 为了调试的目的,你要打印出传入的URL。

  3. responseawait完成之前没有一个值。responseget()来自HTTP包。get从提供的url中获取数据。

  4. statusCode为200意味着请求成功。

  5. 返回嵌入在response.body中的结果。

  6. 否则,你有一个错误--将statusCode打印到控制台。

现在,在getData()后面添加这个服务类。

class RecipeService {
  // 1
  Future<dynamic> getRecipes(String query, int from, int to) async {
    // 2
    final recipeData = await getData(
        '$apiUrl?app_id=$apiId&app_key=$apiKey&q=$query&from=$from&to=$to');
    // 3
    return recipeData;
  }
}

在这段代码中,你。

  1. 创建一个新方法,getRecipes(),参数为queryfromto。这些参数让你从完整的查询中获得特定的页面。from从0开始,to是通过将from的索引加上你的页面大小来计算。你为这个方法使用Future<dynamic>类型,因为你不知道它将返回哪种数据类型或何时完成。async表示这个方法是异步运行的。
  2. 使用final来创建一个不变的变量。你使用await来告诉应用程序等待,直到getData返回其结果。仔细观察getData(),注意你是用传入的变量(加上之前在Edam仪表盘中创建的ID)来创建API URL的。
  3. 返回从API中获取的数据。

**注意:**这个方法并不处理错误。你将在第12章 "使用Chopper库 "中学习如何解决这些问题。

现在你已经写好了服务,现在是时候更新用户界面代码来使用它了。

构建用户界面

每一个好的菜谱集都是从一个菜谱卡开始的,所以你要先建立这个菜谱卡。

创建菜谱卡

文件ui/recipe_card.dart包含一些为你的食谱创建卡片的方法。现在打开它并添加以下导入。

import './network/recipe_model.dart';

现在,改变下面的一行 // TODO: 用新的类来代替

Widget recipeCard(APIRecipe recipe) {

这样就用 "APIRecipe "创建了一个卡片,但你会注意到一些红色的斜线,表示有错误。要纠正这些错误,请替换下面这一行 // TODO: 用菜谱中的图片替换

imageUrl: recipe.image,

并替换下面的行// TODO: 将配方中的标签替换为

recipe.label,

最后,打开recipe_list.dart,替换下面的一行// TODO: 替换为新的卡片方法

child: recipeCard(recipe),

没有红色的方块字了,现在你的肚子在咕咕叫了。是时候看一些菜谱了 :] 。

添加一个食谱列表

你的下一步是为你的用户创建一个方法来找到他们想尝试的卡片:一个食谱列表。

还是在recipe_list.dart中,在最后一次导入后,添加。

import '././network/recipe_service.dart'

替换。

List currentSearchList = [];

用。

List<APIHits> currentSearchList = [];

你已经接近运行应用程序了。挂在那里! 现在是使用配方服务的时候了。

检索配方数据

recipe_list.dart中,你需要创建一个方法来从RecipeService中获取数据。你将传入一个查询以及开始和结束的位置,API将返回解码后的JSON结果。

initState()之后添加这个新方法。

// 1
Future<APIRecipeQuery> getRecipeData(String query, int from, int to) async {
	  // 2
    final recipeJson = await RecipeService().getRecipes(query, from, to);
  	// 3
    final recipeMap = json.decode(recipeJson);
    // 4
    return APIRecipeQuery.fromJson(recipeMap);
}

下面是这个的作用。

  1. 该方法是异步的,并返回一个Future。它需要一个查询和配方数据的开始和结束位置,fromto分别代表这些。
  2. 你定义了recipeJson,它在完成后存储来自RecipeService().getRecipes()的结果。它使用步骤1中的fromto字段。
  3. 变量recipeMap使用Dart的json.decode()将字符串解码为Map<String, dynamic>类型的地图。
  4. 你使用上一章创建的JSON解析方法来创建一个APIRecipeQuery模型。

现在你已经创建了一个获取数据的方法,现在是时候将其投入使用了。在_buildRecipeLoader()之后,添加以下内容。

// 1
Widget _buildRecipeList(BuildContext recipeListContext, List<APIHits> hits) {
  // 2
  final size = MediaQuery.of(context).size;
  const itemHeight = 310;
  final itemWidth = size.width / 2;
  // 3
  return Flexible(
    // 4
    child: GridView.builder(
      // 5
      controller: _scrollController,
      // 6
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: (itemWidth / itemHeight),
      ),
      // 7
      itemCount: hits.length,
      // 8
      itemBuilder: (BuildContext context, int index) {
        return _buildRecipeCard(recipeListContext, hits, index);
      },
    ),
  );
}

下面是发生的情况。

  1. 该方法返回一个widget,并接受recipeListContext和一个配方hits列表。
  2. 你使用MediaQuery来获得设备的屏幕尺寸。然后你设置一个固定的项目高度,并创建两列宽度为设备宽度一半的卡片。
  3. 你返回一个宽度和高度都很灵活的小部件。
  4. GridViewListView相似,但它允许一些有趣的行和列的组合。在这种情况下,你使用GridView.builder(),因为你知道项目的数量,你将使用一个itemBuilder
  5. 你使用_scrollController,在initState()中创建,来检测滚动到离底部约70%的时候。
  6. SliverGridDelegateWithFixedCrossAxisCount委托有两列,并设置长宽比。
  7. 你的网格项目的长度取决于hits列表中的项目数量。
  8. itemBuilder现在使用_buildRecipeCard()为每个配方返回一张卡片。_buildRecipeCard()通过使用hits[index].recipe从hits列表中检索食谱。

很好,现在是时候做一点内务工作了。

删除示例代码

在上一章中,你向recipe_list.dart添加了代码,以显示一张卡片。现在你要显示一个卡片列表,你需要清理一些现有的代码来使用新的API。

_RecipeListState的顶部,删除这个变量声明。

APIRecipeQuery _currentRecipes1;

initState()中,删除对loadRecipes()的调用,找到并删除loadRecipes()的定义。

用下面的代码替换现有的_buildRecipeLoader()。暂时忽略代码中的任何警告斜线。

Widget _buildRecipeLoader(BuildContext context) {
  // 1
  if (searchTextController.text.length < 3) {
    return Container();
  }
  // 2
  return FutureBuilder<APIRecipeQuery>(
    // 3
    future: getRecipeData(searchTextController.text.trim(),
        currentStartPosition, currentEndPosition),
    // 4
    builder: (context, snapshot) {
      // 5
      if (snapshot.connectionState == ConnectionState.done) {
        // 6
        if (snapshot.hasError) {
          return Center(
            child: Text(snapshot.error.toString(),
                textAlign: TextAlign.center, textScaleFactor: 1.3),
          );
        }

        // 7
        loading = false;
        final query = snapshot.data;
        inErrorState = false;
        currentCount = query.count;
        hasMore = query.more;
        currentSearchList.addAll(query.hits);
        // 8
        if (query.to < currentEndPosition) {
          currentEndPosition = query.to;
        }
        // 9
        return _buildRecipeList(context, currentSearchList);
      }
      // TODO: Handle not done connection
    },
  );
}

下面是发生的事情。

  1. 你检查搜索词中至少有三个字符。你可以改变这个值,但你可能不会得到只有一个或两个字符的好结果。
  2. FutureBuilder决定了APIRecipeQuery返回的Future的当前状态。然后它建立一个小部件,在加载时显示异步数据。
  3. 你将getRecipeData返回的Future分配给future
  4. builder是必需的;它返回一个widget。
  5. 你检查connectionState。如果状态是**完成,你可以用结果或错误来更新用户界面。
  6. 如果有一个错误,返回一个简单的文本元素,显示错误信息。
  7. 如果没有错误,处理查询结果并将query.hit添加到currentSearchList
  8. 如果不是在数据的末端,将currentEndPosition设置为当前位置。
  9. 使用currentSearchList返回_buildRecipeList()

对于你的下一步,你将处理snapshot.connectionState不完整的情况。

// TODO: Handle not done connection替换为以下内容。

// 10
else {
  // 11
  if (currentCount == 0) {
    // Show a loading indicator while waiting for the recipes
    return const Center(child: CircularProgressIndicator());
  } else {
    // 12
    return _buildRecipeList(context, currentSearchList);
  }
}

一步一步地走完这个过程。

  1. 你检查snapshot.connectionState没有完成。
  2. 如果当前计数为0,显示一个进度指示器。
  3. 否则,只显示当前的列表。

注意: 如果你需要复习一下滚动的知识,请查看第5章 "可滚动的小部件"。

很好,是时候尝试一下这个应用程序了!

如果需要的话,进行一次热重载。在文本字段中输入,然后按搜索图标。当应用程序从API提取数据时,你会看到圆形的进度条。

在应用程序收到数据后,你会看到一个包含不同类型鸡肉食谱的图片网格。

干得好! 你已经更新了你的应用程序来接收来自互联网的真实数据。试试不同的搜索查询,去向你的朋友展示你创造的东西。]

注意。如果你做了太多的查询,你可能会从Edamam网站得到一个错误。这是因为免费账户限制了你的查询次数。

关键点

  • HTTP包是一套简单易用的方法,用于从互联网上检索数据。
  • 内置的json.decode将JSON字符串转换为你可以在代码中使用的对象映射。
  • FutureBuilder是一个从Future中检索信息的小部件。
  • GridView对于显示数据列很有用。

从哪里开始?

你已经学会了如何从互联网上检索数据并将其解析为数据模型。如果你想了解更多关于HTTP包的信息并获得最新版本,请到pub.dev/packages/ht…

在下一章中,你将了解到Chopper包,它将使处理来自互联网的数据更加容易。到那时为止!


www.deepl.com 翻译