如果你写过原生的
Android
应用,那么你一定会对大名鼎鼎的square/retrofit2
不会感到丝毫陌生。作为OkHttp
的封装框架,它的底层网络请求部分仍然使用OkHttp
,保证安全稳定的同时,又对上层的使用做了极致的简化,做到了接口极大程度的解耦,使得网络请求变得优雅而简单,那么,作为目标全平台大一统的Flutter
,是否也存在能够与retrofit2
或okhttp
地位相匹敌的框架呢?
当然有!
Flutter
的网络请求框架大致可分为两个阵营,由Flutter
官方开发的http
和由国内大神开发的dio
- 这里就不得不提
dio
的作者即是Flutter
中文网的发起人,他也在掘金拥有自己的账号 - 作为国内
Flutter
领域内的先驱,他的很多文章都极具参考价值,还是希望大家多多关注,想必也会受益匪浅 - 回归正题,早先我在写自己的小项目时有斟酌过二者到底谁更好用的问题,对于
http
和dio
,它们的功能不可谓不强大,但我在原生Android
的开发中对okhttp
和retrofit
的使用早已烂熟于心,即使Flutter
的这两个库确实很好用,但总觉得缺了些什么------类型标注的形式真的在很多时候省去了很多麻烦,而且标注式的写法,service
接口的解耦也使得模块测试变得容易了许多,到底怎样才能实现像retrofit
式的解耦呢? - 好吧,能打败
retrofit
的只有它自己,抱着这样的疑问不久,我就找到了相应的插件: - 遗憾的是其并非由
square
官方提供,但...至少能在Flutter
中使用了不是?
使用介绍
- 插件指路: retrofit | Dart package
前置准备
- 作为一个顶层的框架,
retrofit for Flutter
本身并没有包含能提供进行网络请求部分的client
,官方示例的代码用例使用的是dio
来进行实现,所以要使用它,我们至少得在pubspec.yaml
中导入以下的包:
yaml
dependencies:
retrofit: any
logger: any #for logging purpose
json_annotation: any
dev_dependencies:
retrofit_generator: any
build_runner: any
json_serializable: any
- 具体版本和时间有关,这篇文章写于
2024年3月
,故如下所示
yaml
dependencies:
retrofit: ^4.1.0
json_annotation: ^4.8.1
dio: ^5.4.0
logger: ^2.0.2+1
dev_dependencies:
retrofit_generator: ^8.1.0
build_runner: ^2.4.8
json_serializable: ^6.7.1
Dart
没有像Java
那样的类型反射机制,所以对于标注的代码或是对于json
的解析,Dart
一直需要使用build_runner
在运行前生成代码,这也是其一直被诟病的地方,希望以后能被解决吧....- 但...身为人类我们总不能根据后端发来的
json
一个个手敲字段,太浪费时间且不现实。而AndroidStudio
中也有相应的根据json
生成Dart
实体类的插件,但经过本人测试后发现均不太好用 - 好在发现了一个支持多语言生成实体类的网站,成功解决了我的诸多问题:QuickType.io
- 如果你之前并未接触过
retrofit
,或者对retrofit
的标注已经有些陌生,建议参考retrofit for Flutter
的页面或retrofit
的官网(例子中均为Java
代码,注意转换): Retrofit
正式开始
- 本文测试接口来自: Postman-Echo
GET
- postman-echo.com/get?page=1&...
- 我们先使用最简单的接口,看看它返回的
json
结果如何
json
{
"args": {
"page": "1",
"count": "2"
},
"headers": {
"x-forwarded-proto": "https",
"x-forwarded-port": "443",
"host": "postman-echo.com",
"x-amzn-trace-id": "Root=1-6604493c-13898e086cb2109071952f8b",
"user-agent": "PostmanRuntime/7.37.0",
"accept": "*/*",
"postman-token": "22209334-b7e4-45f2-a2c0-fde405af2715",
"accept-encoding": "gzip, deflate, br",
"cookie": "sails.sid=s%3AHYip1Q_7uKBIFcoyQvq0DxxReXIk5ptH.p1JLzOK1Tjd6RtitSVB4VtPQQYFwGLVyAzkc5z0Oduo"
},
"url": "https://postman-echo.com/get?page=1&count=2"
}
- 将代码贴到
QuickType
中,得到如下所示的实体类:
dart
import 'package:json_annotation/json_annotation.dart';
import 'dart:convert';
part 'request.g.dart';
@JsonSerializable()
class Request {
@JsonKey(name: "args")
Args? args;
@JsonKey(name: "headers")
Headers? headers;
@JsonKey(name: "url")
String? url;
Request({
this.args,
this.headers,
this.url,
});
factory Request.fromJson(Map<String, dynamic> json) => _$RequestFromJson(json);
Map<String, dynamic> toJson() => _$RequestToJson(this);
}
@JsonSerializable()
class Args {
@JsonKey(name: "page")
String? page;
@JsonKey(name: "count")
String? count;
Args({
this.page,
this.count,
});
factory Args.fromJson(Map<String, dynamic> json) => _$ArgsFromJson(json);
Map<String, dynamic> toJson() => _$ArgsToJson(this);
}
@JsonSerializable()
class Headers {
@JsonKey(name: "x-forwarded-proto")
String? xForwardedProto;
@JsonKey(name: "x-forwarded-port")
String? xForwardedPort;
@JsonKey(name: "host")
String? host;
@JsonKey(name: "x-amzn-trace-id")
String? xAmznTraceId;
@JsonKey(name: "user-agent")
String? userAgent;
@JsonKey(name: "accept")
String? accept;
@JsonKey(name: "postman-token")
String? postmanToken;
@JsonKey(name: "accept-encoding")
String? acceptEncoding;
@JsonKey(name: "cookie")
String? cookie;
Headers({
this.xForwardedProto,
this.xForwardedPort,
this.host,
this.xAmznTraceId,
this.userAgent,
this.accept,
this.postmanToken,
this.acceptEncoding,
this.cookie,
});
factory Headers.fromJson(Map<String, dynamic> json) => _$HeadersFromJson(json);
Map<String, dynamic> toJson() => _$HeadersToJson(this);
}
- 我们还需要把代码贴到项目中,这里我新建了一个名为
request.dart
的文件,注意,这里part: 'request.g.dart'
的.g.dart
的前的名字需要与你建立的实体类名字相同 ,这里我就用了一样的request
,否则在build
的过程中会因文件名识别不同而报错 - 接下来即是编写请求接口:
dart
import 'package:blog_test/request.dart';
import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';
part 'service.g.dart';
@RestApi(baseUrl: 'https://postman-echo.com/')
abstract class Service {
factory Service(Dio dio, {String baseUrl}) = _Service;
@GET('/get')
Future<Request> getTask(
@Query('page') String page,
@Query('count') String count,
);
}
- 这里的
Query
注解即对应跟在相应api
后的参数(/get?page=xxx&count=xxx
), - 这里在编写时和实体类编写时一样,编译器会报错,因为
build_runner
没有运行,相应的代码便没有生成,确保自己除了没生成的代码后没有别的问题,就可以在控制台生成代码了(两行效果一样)
bash
# dart
dart pub run build_runner build
# flutter
flutter pub run build_runner build
- 生成代码后应如下所示:
- 此时我们还需要一个
dio
的client
来完成请求工作,为了更好使用,我又新建了一个类network.dart
dart
import 'package:blog_test/request.dart';
import 'package:blog_test/service.dart';
import 'package:dio/dio.dart';
class Network{
static final Dio _dio = Dio()..interceptors.add(LogInterceptor(responseBody: true));
static final Service _service = Service(_dio);
static Future<Request> getTask(String page,String count) => _service.getTask(page, count);
}
- 这里我在创建
dio
的同时还为其添加了拦截器LogInterceptor
,为的是能够在运行中发起请求时查看打印的日志,便于调试。此时我们可以在network
的基础上再新建一层repository
对接收到的原始数据进行进一步的封装工作,或是直接调用network
来获取原始数据,后续工作因人而异,故不做赘述 - 这样网络请求的基础部分已经完成,我们在
main.dart
中绘制一个简单的页面,看看结果如何:
dart
import 'dart:convert';
import 'package:blog_test/network.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget{
String result = "";
@override
State<StatefulWidget> createState() => MainScreenState();
}
class MainScreenState extends State<MainScreen>{
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Retrofit Test"),
centerTitle: true,
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SingleChildScrollView(
child: Column(
children: [
ElevatedButton(
onPressed: (){
Network.getTask("1", "50").then((value){
setState(() {
widget.result = jsonEncode(value.toJson());
});
});
},
child: Text('发送请求')
),
Text(widget.result)
],
),
),
);
}
}
- 可以看到,请求成功了
- 关于
GET
请求大多数的情况都是Query
注解,故只提供这一个例子已足够
POST
POST
最常用的使用场景有三种: 以表单数据的形式提交 ,以Body的形式提交 ,和上传文件 的情况POST
和GET
返回的json
略有不同,我们还需要新建一个request_post.dart
类来接收,其它与GET
情况一致,记得使用build_runner
生成代码
dart
import 'package:json_annotation/json_annotation.dart';
import 'dart:convert';
part 'request_post.g.dart';
@JsonSerializable()
class RequestPost {
@JsonKey(name: "args")
dynamic args;
@JsonKey(name: "data")
dynamic data;
@JsonKey(name: "files")
dynamic files;
@JsonKey(name: "form")
dynamic form;
@JsonKey(name: "headers")
Headers? headers;
@JsonKey(name: "json")
dynamic json;
@JsonKey(name: "url")
String? url;
RequestPost({
this.args,
this.data,
this.files,
this.form,
this.headers,
this.json,
this.url,
});
factory RequestPost.fromJson(Map<String, dynamic> json) => _$RequestPostFromJson(json);
Map<String, dynamic> toJson() => _$RequestPostToJson(this);
}
@JsonSerializable()
class Headers {
@JsonKey(name: "x-forwarded-proto")
String? xForwardedProto;
@JsonKey(name: "x-forwarded-port")
String? xForwardedPort;
@JsonKey(name: "host")
String? host;
@JsonKey(name: "x-amzn-trace-id")
String? xAmznTraceId;
@JsonKey(name: "content-length")
String? contentLength;
@JsonKey(name: "user-agent")
String? userAgent;
@JsonKey(name: "accept")
String? accept;
@JsonKey(name: "postman-token")
String? postmanToken;
@JsonKey(name: "accept-encoding")
String? acceptEncoding;
@JsonKey(name: "content-type")
String? contentType;
@JsonKey(name: "cookie")
String? cookie;
Headers({
this.xForwardedProto,
this.xForwardedPort,
this.host,
this.xAmznTraceId,
this.contentLength,
this.userAgent,
this.accept,
this.postmanToken,
this.acceptEncoding,
this.contentType,
this.cookie,
});
factory Headers.fromJson(Map<String, dynamic> json) => _$HeadersFromJson(json);
Map<String, dynamic> toJson() => _$HeadersToJson(this);
}
单一表单数据提交
- 在接口类
service.dart
中编写以下代码:
dart
import 'package:blog_test/request.dart';
import 'package:blog_test/request_post.dart';
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
part 'service.g.dart';
@RestApi(baseUrl: 'https://postman-echo.com/')
abstract class Service {
factory Service(Dio dio, {String baseUrl}) = _Service;
@GET('/get')
Future<Request> getTask(
@Query('page') String page,
@Query('count') String count,
);
@FormUrlEncoded()
@POST('/post')
Future<RequestPost> postFormData(
@Field('id') String id,
@Field('password') String password,
);
}
- 其中
@FromUrlEncoded
注解表示该数据以application/x-www-form-urlencoded
的形式提交,@Field
即我们需要上传的字段,二者一般需要互相搭配才能正常使用 network
层也要加入相应的代码,代码重复故不作赘述- 如果上述代码编写正常,应该能看到这样的结果:
以Body的形式提交
- 新建一个
login_post_body.dart
,写入以下代码以便测试:
dart
import 'package:json_annotation/json_annotation.dart';
part 'login_post_body.g.dart';
@JsonSerializable()
class LoginPostBody{
String id;
String password;
LoginPostBody({required this.id,required this.password});
factory LoginPostBody.fromJson(Map<String, dynamic> json) => _$LoginPostBodyFromJson(json);
Map<String, dynamic> toJson() => _$LoginPostBodyToJson(this);
}
- 作为需要上传的
body
,需要保证持有toJson
方法以便进行json
的序列化,因为前面需要用build_runner
,所以我其它部分的写法便和QuickType
中生成的代码保持一致 - 接口部分:
dart
import 'package:blog_test/login_post_body.dart';
import 'package:blog_test/request.dart';
import 'package:blog_test/request_post.dart';
import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';
part 'service.g.dart';
@RestApi(baseUrl: 'https://postman-echo.com/')
abstract class Service {
factory Service(Dio dio, {String baseUrl}) = _Service;
@GET('/get')
Future<Request> getTask(
@Query('page') String page,
@Query('count') String count,
);
@FormUrlEncoded()
@POST('/post')
Future<RequestPost> postFormData(
@Field('id') String id,
@Field('password') String password,
);
@POST('/post')
Future<RequestPost> postBody(
@Body() LoginPostBody body,
);
}
- 运行,返回结果如下所示:
- 可以看到,此时我们上传的数据不在
form
,而在data
字段中,表明以body
形式上传的数据并不以表单数据的形式存在
文件上传
- 直接访问手机储存中的文件还需要额外导入
path_provider
包,以获得手机中软件的临时存储路径 - 我把需要上传的文件放入了项目根目录下的
resources
文件夹中以便访问 - 当然别忘了在
pubspec.yaml
中声明并执行pub get
以同步文件 - 接下来便是把在
assets
中的文件读入并写入手机的临时存储路径中,并把成功读取的文件进行返回.我把这些操作放入了名为asset_solver.dart
中
dart
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/services.dart' show rootBundle;
import 'package:path_provider/path_provider.dart';
class AssetSolver{
static Future<File> getFile() async {
var byteData = await rootBundle.load('resources/Flutter.jpeg');
var directory = await getTemporaryDirectory();
var file = File('${directory.path}/Flutter.jpeg');
file.writeAsBytesSync(byteData.buffer.asUint8List());
return file;
}
}
- 接口部分如下:
dart
import 'dart:io';
import 'dart:typed_data';
import 'package:blog_test/login_post_body.dart';
import 'package:blog_test/request.dart';
import 'package:blog_test/request_post.dart';
import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';
part 'service.g.dart';
@RestApi(baseUrl: 'https://postman-echo.com/')
abstract class Service {
factory Service(Dio dio, {String baseUrl}) = _Service;
@GET('/get')
Future<Request> getTask(
@Query('page') String page,
@Query('count') String count,
);
@FormUrlEncoded()
@POST('/post')
Future<RequestPost> postFormData(
@Field('id') String id,
@Field('password') String password,
);
@POST('/post')
Future<RequestPost> postBody(
@Body() LoginPostBody body,
);
@MultiPart()
@POST('/post')
Future<RequestPost> postFile(
@Part() File file,
);
}
- 这里的
@MultiPart
注解即对应multipart/form-data
,我们也可以使用这种形式进行表单和文件的混合上传,具体形式读者自行测试即可明白 - 一切准备就绪,在
network.dart
和main.dart
中编写配套的代码后,我们可以得到如下的结果: - 文件上传成功!
最后
- 文章不长,但我想包含的内容已经足够使用,感谢您有耐心看到这里。
Retrofit
作为一个优秀的框架,它的思想不应被语言所限制,我期待它有朝一日能在Flutter
中获得与Java
中一样的地位~~ - 再次感谢您的观看.