本文由 简悦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和密钥,用你的值替换现有的apiKey
和apiId
分配的字符串。不要复制结尾的空格和破折号,如下图所示。
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);
}
}
下面是对事情的分解。
-
getData
返回一个Future
(大写 "F"),因为API返回的数据类型是在未来确定的(小写 "f")。async
标志着这个方法是一个异步操作。 -
为了调试的目的,你要打印出传入的URL。
-
response
在await
完成之前没有一个值。response
和get()
来自HTTP包。get
从提供的url
中获取数据。 -
statusCode
为200意味着请求成功。 -
你
返回
嵌入在response.body
中的结果。 -
否则,你有一个错误--将
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;
}
}
在这段代码中,你。
- 创建一个新方法,
getRecipes()
,参数为query
,from
和to
。这些参数让你从完整的查询中获得特定的页面。from
从0开始,to
是通过将from
的索引加上你的页面大小来计算。你为这个方法使用Future<dynamic>
类型,因为你不知道它将返回哪种数据类型或何时完成。async
表示这个方法是异步运行的。 - 使用
final
来创建一个不变的变量。你使用await
来告诉应用程序等待,直到getData
返回其结果。仔细观察getData()
,注意你是用传入的变量(加上之前在Edam仪表盘中创建的ID)来创建API URL的。 返回
从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);
}
下面是这个的作用。
- 该方法是异步的,并返回一个
Future
。它需要一个查询
和配方数据的开始和结束位置,from
和to
分别代表这些。 - 你定义了
recipeJson
,它在完成后存储来自RecipeService().getRecipes()
的结果。它使用步骤1中的from
和to
字段。 - 变量
recipeMap
使用Dart的json.decode()
将字符串解码为Map<String, dynamic>
类型的地图。 - 你使用上一章创建的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);
},
),
);
}
下面是发生的情况。
- 该方法返回一个widget,并接受
recipeListContext
和一个配方hits
列表。 - 你使用
MediaQuery
来获得设备的屏幕尺寸。然后你设置一个固定的项目高度,并创建两列宽度为设备宽度一半的卡片。 - 你返回一个宽度和高度都很灵活的小部件。
GridView
与ListView
相似,但它允许一些有趣的行和列的组合。在这种情况下,你使用GridView.builder()
,因为你知道项目的数量,你将使用一个itemBuilder
。- 你使用
_scrollController
,在initState()
中创建,来检测滚动到离底部约70%的时候。 SliverGridDelegateWithFixedCrossAxisCount
委托有两列,并设置长宽比。- 你的网格项目的长度取决于
hits
列表中的项目数量。 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
},
);
}
下面是发生的事情。
- 你检查搜索词中至少有三个字符。你可以改变这个值,但你可能不会得到只有一个或两个字符的好结果。
FutureBuilder
决定了APIRecipeQuery
返回的Future
的当前状态。然后它建立一个小部件,在加载时显示异步数据。- 你将
getRecipeData
返回的Future
分配给future
。 builder
是必需的;它返回一个widget。- 你检查
connectionState
。如果状态是**完成,你可以用结果或错误来更新用户界面。 - 如果有一个错误,返回一个简单的
文本
元素,显示错误信息。 - 如果没有错误,处理查询结果并将
query.hit
添加到currentSearchList
。 - 如果不是在数据的末端,将
currentEndPosition
设置为当前位置。 - 使用
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);
}
}
一步一步地走完这个过程。
- 你检查
snapshot.connectionState
没有完成。 - 如果当前计数为0,显示一个进度指示器。
- 否则,只显示当前的列表。
注意: 如果你需要复习一下滚动的知识,请查看第5章 "可滚动的小部件"。
很好,是时候尝试一下这个应用程序了!
如果需要的话,进行一次热重载。在文本字段中输入鸡,然后按搜索图标。当应用程序从API提取数据时,你会看到圆形的进度条。
在应用程序收到数据后,你会看到一个包含不同类型鸡肉食谱的图片网格。
干得好! 你已经更新了你的应用程序来接收来自互联网的真实数据。试试不同的搜索查询,去向你的朋友展示你创造的东西。]
注意。如果你做了太多的查询,你可能会从Edamam网站得到一个错误。这是因为免费账户限制了你的查询次数。
关键点
- HTTP包是一套简单易用的方法,用于从互联网上检索数据。
- 内置的
json.decode
将JSON字符串转换为你可以在代码中使用的对象映射。 FutureBuilder
是一个从Future
中检索信息的小部件。GridView
对于显示数据列很有用。
从哪里开始?
你已经学会了如何从互联网上检索数据并将其解析为数据模型。如果你想了解更多关于HTTP包的信息并获得最新版本,请到pub.dev/packages/ht…。
在下一章中,你将了解到Chopper包,它将使处理来自互联网的数据更加容易。到那时为止!