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中一样的地位~~
  • 再次感谢您的观看.
相关推荐
比格丽巴格丽抱11 小时前
flutter项目苹果编译运行打包上线
flutter·ios
SoaringHeart11 小时前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter
AiFlutter15 小时前
Flutter通过 Coap发送组播
flutter
嘟嘟叽2 天前
初学 flutter 环境变量配置
flutter
iFlyCai2 天前
深入理解Flutter生命周期函数之StatefulWidget(一)
flutter·生命周期·dart·statefulwidget
sunly_2 天前
Flutter:photo_view图片预览功能
android·javascript·flutter
Summer不秃2 天前
Flutter中sqflite的使用案例
flutter
sunly_2 天前
Flutter:TweenAnimationBuilder自定义隐式动画
flutter
AiFlutter2 天前
Flutter-Web首次加载时添加动画
前端·flutter
Allen Su3 天前
【Flutter 问题系列第 84 篇】如何清除指定网络图片的缓存
flutter·缓存·如何清除指定网络图片的缓存·网络图片缓存