Retrofit+Dio,让你的Flutter网络请求如Android般丝滑

如果你写过原生的Android应用,那么你一定会对大名鼎鼎的square/retrofit2不会感到丝毫陌生。作为OkHttp的封装框架,它的底层网络请求部分仍然使用OkHttp,保证安全稳定的同时,又对上层的使用做了极致的简化,做到了接口极大程度的解耦,使得网络请求变得优雅而简单,那么,作为目标全平台大一统的Flutter,是否也存在能够与retrofit2okhttp地位相匹敌的框架呢?

当然有!

  • Flutter的网络请求框架大致可分为两个阵营,由Flutter官方开发的http和由国内大神开发的dio
  • 这里就不得不提dio的作者即是Flutter中文网的发起人,他也在掘金拥有自己的账号
  • 作为国内Flutter领域内的先驱,他的很多文章都极具参考价值,还是希望大家多多关注,想必也会受益匪浅
  • 回归正题,早先我在写自己的小项目时有斟酌过二者到底谁更好用的问题,对于httpdio,它们的功能不可谓不强大,但我在原生Android的开发中对okhttpretrofit的使用早已烂熟于心,即使Flutter的这两个库确实很好用,但总觉得缺了些什么------类型标注的形式真的在很多时候省去了很多麻烦,而且标注式的写法,service接口的解耦也使得模块测试变得容易了许多,到底怎样才能实现像retrofit式的解耦呢?
  • 好吧,能打败retrofit的只有它自己,抱着这样的疑问不久,我就找到了相应的插件:
  • 遗憾的是其并非由square官方提供,但...至少能在Flutter中使用了不是?

使用介绍

前置准备

  • 作为一个顶层的框架,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

正式开始

GET

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
  • 生成代码后应如下所示:

  • 此时我们还需要一个dioclient来完成请求工作,为了更好使用,我又新建了一个类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的形式提交 ,和上传文件 的情况
  • POSTGET返回的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.dartmain.dart中编写配套的代码后,我们可以得到如下的结果:
  • 文件上传成功!

最后

  • 文章不长,但我想包含的内容已经足够使用,感谢您有耐心看到这里。Retrofit作为一个优秀的框架,它的思想不应被语言所限制,我期待它有朝一日能在Flutter中获得与Java中一样的地位~~
  • 再次感谢您的观看.
相关推荐
江上清风山间明月1 天前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能1 天前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人1 天前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen1 天前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang2 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang2 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1232 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-2 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11192 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力2 天前
Flutter应用开发:对象存储管理图片
flutter