本文由 简悦SimpRead 转码,原文地址 www.raywenderlich.com
本章将教你如何使用Chopper包来检索互联网上的数据。与Dart中内置的网络方法不同,这个包可以轻松完成REST API通信所需的所有必要任务。
在上一章中,你学习了在Flutter中使用HTTP包进行联网。现在,你将继续之前的项目,学习如何使用Chopper包来访问Edam Recipe API。
注意: 你也可以通过打开本章的starter项目重新开始。如果你选择这样做,记得点击Get dependencies按钮或从终端执行
flutter pub get
。你还需要添加你的API密钥和ID。
在本章结束时,你将知道。
- 如何设置Chopper并使用它从服务器API获取数据。
- 如何使用转换器和拦截器来装饰请求和操作响应。
- 如何记录请求。
为什么是Chopper?
正如你在上一章学到的,HTTP包很容易用来处理网络调用,但它也是相当基本的。Chopper可以做得更多。比如说。
- 它生成代码以简化网络代码的开发。
- 它允许你以模块化的方式组织这些代码,所以它更容易改变和推理。
注意: 如果你来自移动开发的Android方面,你可能对Retrofit库很熟悉,它是类似的。如果你有iOS背景,AlamoFire是一个非常类似的库。
准备使用Chopper
要使用Chopper,你需要将该包添加到pubspec.yaml。为了记录网络调用,你还需要logging包。
打开pubspec.yaml,将http包的一行替换为。
chopper: ^3.0.6
logging: ^0.11.4
你还需要chopper_generator,这是一个以part文件的形式为你生成模板代码的包。在dev_dependencies部分,在json_serializable之后,添加这个。
chopper_generator: ^3.0.6
接下来,点击Pub get或在终端运行`flutter pub get'来获取新的软件包。
现在,新的软件包已经准备好使用了......系好你的安全带! :]
处理配方结果
在这种情况下,创建一个通用的响应类是很好的做法,它将持有一个成功的响应或一个错误。虽然这些类不是必须的,但它们使处理服务器返回的响应变得更容易。
右击lib/network,创建一个名为model_response.dart的新Dart文件。在其中添加以下类。
// 1
abstract class Result<T> {
}
// 2
class Success<T> extends Result<T> {
final T value;
Success(this.value);
}
// 3
class Error<T> extends Result<T> {
final Exception exception;
Error(this.exception);
}
在这里,你已经:
- 创建了一个
abstract class
。它是一个具有泛型T
的请求结果的模板。 - 创建了
Success
类来扩展Result
并在响应成功时保存一个值。例如,这可以保存JSON数据。 - 创建了
Error
类来扩展Result
并保存一个异常。这将模拟在HTTP调用过程中发生的错误,比如你使用了错误的凭证或试图在没有授权的情况下获取数据。
注意:要复习Dart中抽象类的知识,请查看我们的Dart学徒书www.raywenderlich.com/books/dart-…。
你将使用这些类来为使用Chopper通过HTTP获取的数据建模。
准备配方服务
打开recipe_service.dart,删除现有的RecipeService
类和getData
方法。用以下内容替换http
包的导入。
import 'package:chopper/chopper.dart';
import 'recipe_model.dart';
import 'model_response.dart';
这就增加了Chopper包和你的模型。
将现有的apiUrl
常量替换为。
const String apiUrl = 'https://api.edamam.com';
这让你可以调用除/search
之外的其他API。
现在是设置Chopper的时候了!
设置Chopper客户端
你的下一步是创建一个定义你的API调用的类,并设置Chopper客户端来为你做工作。还是在recipe_service.dart中,添加以下内容。
// 1
@ChopperApi()
// 2
abstract class RecipeService extends ChopperService {
// 3
@Get(path: 'search')
// 4
Future<Response<Result<APIRecipeQuery>>> queryRecipes(
// 5
@Query('q') String query, @Query('from') int from, @Query('to') int to);
}
这里有相当多的东西需要理解。要把它分解开来。
@ChopperApi()
告诉Chopper生成器建立一个part文件。这个生成的文件将具有与此文件相同的名称,但在其上添加chopper。在这种情况下,它将是recipe_service.chopper.dart。这样的文件将容纳模板代码。- "RecipeService "是一个 "抽象 "类,因为你只需要定义方法的签名。生成器脚本将接受这些定义并生成所有需要的代码。
@Get
是一个注解,它告诉生成器这是一个GET请求,有一个名为search
的path
,你之前从apiUrl
中删除了这个路径。你还可以使用其他的HTTP方法,如@Post
、@Put
和@Delete
,但在本章中你不会用到它们。- 你定义一个函数,使用先前创建的
APIRecipeQuery
返回一个Response
的Future
。你在上面创建的抽象的Result
将持有一个值或一个错误。 queryRecipes()
使用Chopper@Query
注解来接受一个query
字符串和from
和to
整数。这个方法没有一个主体。生成器脚本将用所有的参数创建这个函数的主体。
注意,到目前为止,你定义了一个通用接口来进行网络调用。没有实际的代码来执行诸如将API密钥添加到请求中或将响应转化为数据对象的任务。这是转换器和拦截器的工作!
转换请求和响应
为了使用返回的API数据,你需要一个转换器来转换请求和响应。为了将转换器附加到Chopper客户端,你需要一个拦截器。你可以把拦截器看作是每次你发送请求或接收响应时都会运行的函数--一种钩子,你可以在传递这些数据之前给它附加一些功能,比如转换或装饰数据。
右键点击lib/network,创建一个名为model_converter.dart的新文件,添加以下内容。
import 'dart:convert';
import 'package:chopper/chopper.dart';
import 'model_response.dart';
import 'recipe_model.dart';
这就增加了内置的Dart convert包,它可以将数据转换为JSON,再加上Chopper包和你的模型文件。
接下来,通过添加创建ModelConverter
。
// 1
class ModelConverter implements Converter {
// 2
@override
Request convertRequest(Request request) {
// 3
final req = applyHeader(
request,
contentTypeKey,
jsonHeaders,
override: false,
);
// 4
return encodeJson(req);
}
Request encodeJson(Request request) {}
Response decodeJson<BodyType, InnerType>(Response response) {}
@override
Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {}
}
下面是你用这段代码要做的事情。
- 使用
ModelConverter
来实现ChopperConverter
抽象类。 - 覆盖
convertRequest()
,它接收一个请求并返回一个新的请求。 - 使用
jsonHeaders
在请求中添加一个头,说明你的请求类型为application/json。这些常量是Chopper的一部分。 - 调用
encodeJson()
将请求转换为JSON编码的请求,这是服务器API的要求。
剩下的代码由占位符组成,你将在下一节中加入这些代码。
编码和解码JSON
为了便于将来扩展你的应用程序,你将把编码和解码分开。如果你以后需要分别使用它们,这给了你灵活性。
每当你进行网络调用时,你要确保在发送请求之前对其进行encode,并将响应字符串decode到你的模型类中,你将用它来在用户界面中显示数据。
编码JSON
要将请求编码为JSON格式,请将现有的encodeJson()
替换为。
Request encodeJson(Request request) {
// 1
final contentType = request.headers[contentTypeKey];
// 2
if (contentType != null && contentType.contains(jsonHeaders)) {
// 3
return request.copyWith(body: json.encode(request.body));
}
return request;
}
在这段代码中,你。
- 提取请求头。
- 确认
contentType
是application/json
类型。 - 制作一个带有JSON编码的请求的副本。
基本上,这个方法接收一个Request
实例,并返回一个经过装饰的副本,准备发送到服务器。解码呢?很高兴你这么问。]
解码JSON
现在,是时候添加解码JSON的功能了。服务器的响应通常是一个字符串,所以你必须解析JSON字符串并将其转换为APIRecipeQuery
模型类。
将decodeJson()
替换为。
Response decodeJson<BodyType, InnerType>(Response response) {
final contentType = response.headers[contentTypeKey];
var body = response.body;
// 1
if (contentType != null && contentType.contains(jsonHeaders)) {
body = utf8.decode(response.bodyBytes);
}
try {
// 2
final mapData = json.decode(body);
// 3
if (mapData['status'] != null) {
return response.copyWith<BodyType>(
body: Error(Exception(mapData['status'])) as BodyType);
}
// 4
final recipeQuery = APIRecipeQuery.fromJson(mapData);
// 5
return response.copyWith<BodyType>(
body: Success(recipeQuery) as BodyType);
} catch (e) {
// 6
chopperLogger.warning(e);
return response.copyWith<BodyType>(body: Error(e) as BodyType);
}
}
这里有很多需要思考的问题。要把它分解开来,你。
-
检查你是否在处理JSON,并将
response
解码为一个名为body
的字符串。 -
使用JSON解码,将该字符串转换为地图表示。
-
当出现错误时,服务器会返回一个名为
status
的字段。在这里,你检查地图是否包含这样一个字段。如果是,你就返回一个嵌入了Error
实例的响应。 -
使用
APIRecipeQuery.fromJson()
将地图转换成模型类。 -
返回一个成功的响应,包裹着
recipeQuery
。 -
如果你得到任何其他类型的错误,用`Error'的通用实例来包装响应。
你仍然需要覆盖一个方法:convertResponse
。这个方法将给定的响应改为你想要的。
将现有的convertResponse()
替换为。
@override
Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {
// 1
return decodeJson<BodyType, InnerType>(response);
}
- 这只是调用了你之前定义的
decodeJson
。
现在是时候在适当的地方使用转换器并添加一些拦截器了。
使用拦截器
如前所述,拦截器可以拦截请求、响应或两者。在请求拦截器中,你可以添加头信息或处理认证。在响应拦截器中,你可以操作响应并将其转化为另一种类型,你很快就会看到。你将从装饰请求开始。
自动包括你的ID和密钥
为了请求任何配方,API需要你的app_id
和app_key
。你可以使用一个拦截器将这些字段添加到每个调用中,而不是手动添加到每个查询中。
打开recipe_service.dart,在 "RecipeService "类声明后添加以下内容。
Request _addQuery(Request req) {
// 1
final params = Map<String, dynamic>.from(req.parameters);
// 2
params['app_id'] = apiId;
params['app_key'] = apiKey;
// 3
return req.copyWith(parameters: params);
}
这是一个请求拦截器,将API密钥和ID添加到查询参数中。下面是代码的作用。
- 创建一个
Map
,其中包含来自现有Request
参数的键值对。 - 将
app_id
和app_key
参数添加到地图中。 - 返回一个新的
Request
副本,其中包含地图中的参数。
这个方法的好处是,一旦你把它挂起来,你所有的调用都会使用它。虽然你现在只有一个调用,但如果你增加更多的调用,它们将自动包括这些键。而如果你想在每个调用中添加一个新的参数,你只需改变这个方法即可。你开始看到Chopper的优势了吗?]
你有拦截器来装饰请求,你有一个转换器来将响应转化为模型类。接下来,你要把它们用起来了!
拦截器和转换器的连接
在recipe_service.dart的顶部添加以下导入语句。
import 'model_converter.dart';
现在,将这个新方法添加到RecipeService
中。确保不要把它添加到_addQuery()
。不要担心那些红色的斜线,它们在警告你缺少模板代码,因为你还没有生成它。
static RecipeService create() {
// 1
final client = ChopperClient(
// 2
baseUrl: apiUrl,
// 3
interceptors: [_addQuery, HttpLoggingInterceptor()],
// 4
converter: ModelConverter(),
// 5
errorConverter: const JsonConverter(),
// 6
services: [
_$RecipeService(),
],
);
// 7
return _$RecipeService(client);
}
在这段代码中,你
- 创建一个
ChopperClient
实例。 - 使用`apiUrl'常量传递一个基本的URL。
- 传入两个拦截器。
_addQuery()
将你的密钥和ID添加到查询中。HttpLoggingInterceptor
是Chopper的一部分,记录所有调用。当你在开发时,它很方便地看到应用程序和服务器之间的流量。 - 将
converter
设置为ModelConverter
的一个实例。 - 使用内置的
JsonConverter
来解码任何错误。 - 定义当你运行生成器脚本时创建的服务。
- 返回生成的服务的一个实例。
这一切都准备好了,你已经准备好生成模板代码了!
生成Chopper文件
你的下一步是生成recipe_service.chopper.dart,它与part
一起工作。记得在第10章 "用JSON进行序列化 "中,part
将包括指定的文件并使其成为一个大文件的一部分。
导入你要生成的文件。打开recipe_service.dart,在import
语句后面加上这个。
part 'recipe_service.chopper.dart';
忽略红色的方块字。它们会在你生成文件后消失。
注意:在文件创建之前导入文件可能看起来很奇怪,但是如果生成器脚本不知道哪里有声明的注释,它就会失败。
现在,在Android Studio中打开Terminal。默认情况下,它将在你的项目文件夹中。执行。
flutter pub run build_runner build --delete-conflicting-outputs
注意:使用
--delete-conflicting-outputs
会在生成新文件之前删除所有已生成的文件。
在执行过程中,你会看到类似这样的东西。
一旦它完成,你会看到新的recipe_service.chopper.dart在lib/network。在它出现之前,你可能需要刷新network文件夹。
注意:如果你没有看到这个文件或者Android Studio没有检测到它的存在,请重新启动Android Studio。
打开它并检查一下。你会看到的第一件事是一个注释,说明不要手工修改该文件。
再往下看,你会看到一个叫做_$RecipeService
的类。在这下面,你会注意到queryRecipes()
已经被重写,以建立参数和请求。它使用客户端来发送请求。
这可能看起来不多,但当你添加不同路径和参数的不同调用时,你会开始欣赏像Chopper中包含的代码生成器的帮助 :]
现在你已经将RecipeService
改为使用Chopper,是时候进行最后的修饰了。设置日志并使用新的方法来获取数据。
记录请求和响应
打开in.dart,添加以下导入。
import 'package:logging/logging.dart';
这是来自于你之前添加到pubspec.yaml的logging包。
在 "main() "之后,添加。
void _setupLogging() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((rec) {
print('${rec.level.name}: ${rec.time}: ${rec.message}');
});
}
这将初始化logging包,并允许Chopper记录请求和响应。设置级别为Level.ALL
,这样你就可以看到每一条日志语句。
注意:你可以尝试将
ALL
改为WARNING
、SEVERE
或其他级别,看看会发生什么。
在main()
中,添加以下内容作为第一个语句。
_setupLogging();
记录现在都设置好了。现在是使用基于Chopper的新功能的时候了 :]
使用Chopper客户端
打开ui/recipes/recipe_list.dart。你会看到一些由于你所做的改变而产生的错误。
如果你看到下面的import
,请删除它,因为它已经在其他类中被导入。
import 'dart:convert';
现在,添加以下导入。
import 'package:chopper/chopper.dart';
import '../../network/model_response.dart';
找到getRecipeData()
的声明并删除它。
在_buildRecipeLoader()
中,替换下面这行// TODO: 改变为新的响应
,从。
return FutureBuilder<APIRecipeQuery>(
改为:
return FutureBuilder<Response<Result<APIRecipeQuery>>>(
这使用了新的响应类型,包装了API调用的结果。
现在,将下面的future
替换为// TODO: change with new RecipeService
。
future: RecipeService.create().queryRecipes(
searchTextController.text.trim(),
currentStartPosition,
currentEndPosition),
future
现在创建了一个新的RecipeService
实例,并调用其方法queryRecipes()
,以执行查询。
最后,替换下面的一行 // TODO: 用新的快照改变
从:
final query = snapshot.data;
到:
// 1
final result = snapshot.data.body;
// 2
if (result is Error) {
// Hit an error
inErrorState = true;
return _buildRecipeList(context, currentSearchList);
}
// 3
final query = (result as Success).value;
下面是你在上面的代码中所做的工作。
snapshot.data
现在是一个Response
,而不再是一个字符串。body
字段是你上面定义的成功
或错误
。将body
的值提取到result
中。- 如果
result
是一个错误,返回当前的配方列表。 - 由于
result
通过了错误检查,将其作为Success
并将其值提取到query
中。
运行应用程序并输入一个搜索值,如chicken。按下搜索图标,验证你是否看到用户界面中显示的菜谱。
现在,看看Android Studio的Run窗口,你会看到很多与你的网络调用有关的INFO信息。这是一个很好的方法,可以看到你的请求和响应的情况,并找出导致任何问题的原因。
你成功了! 你现在可以使用Chopper来调用服务器的API并检索配方了。
关键点
Chopper包提供了从互联网上检索数据的简单方法。
- 你可以给每个网络请求添加头信息。
- 拦截器可以拦截请求和响应并改变这些值。
- 转换器可以修改请求和响应。
- 很容易设置全局日志。
何去何从?
如果你想了解更多关于Chopper包的信息,请到pub.dev/packages/ch…。关于日志库的更多信息,请访问pub.dev/packages/lo…。
在下一章中,你将了解到状态管理这个重要的话题。到那时为止!