likes
comments
collection
share

flutter 极简的网络请求 - Retrofit 文档记录

作者站长头像
站长
· 阅读数 47

前言

对于Retrofit插件说实话之前是不太了解的,后来偶然发现了它,感觉还是比较惊艳的。主要工作流程就是注解、生成,通过定义简化通用请求方法的繁杂工作。(ps: json_serializablefreezed 和 最新的Riverpod也是类似的工作方式,但Riverpod理解要稍微复杂一些。)

优秀插件太多感觉都有点看不完了,但是一聊到能减少重()复()工()作()那高低肯定是要上车了。 (●'◡'●)

插件Git地址:retorfit.dart

一、Retrofit 文档记录

主要目的还是记录一下,方便后续使用的时候查看。

1、添加插件引用

dependencies:
  dio: any
  retrofit: '>=4.0.0 <5.0.0'
  logger: any  #for logging purpose
  json_annotation: ^4.8.1

dev_dependencies:
  retrofit_generator: '>=7.0.0 <8.0.0'   // required dart >=2.19
  build_runner: '>=2.3.0 <4.0.0'
  json_serializable: ^6.6.2

2、定义请求使用

import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';

part 'example.g.dart';

@RestApi(
    // 请求域名
    baseUrl: 'https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1/'
    // 数据解析方式,默认为json
    parser: Parser.JsonSerializable,
)
abstract class RestClient {
  // 标准的构建方式
  // dio: 传入发起网络请求的对象
  // baseUrl: 请求域名,优先级高于注解
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

  // 1、添加请求方式注解,接口地址
  // 2、定义返回值类型(可以是任意类型,也可以是定义好的model),请求方法名,请求参数(后面会提到)
  @GET('/tasks')
  Future<List<Task>> getTasks();
}

example.g.dart 是脚本生成的具体实现文件;

@RestApi(baseUrl:...) 添加注解及部分配置参数;

Future<List<Task>> getTasks(...) 定义请求的返回值、参数值、请求类型与接口地址;

这个文件请求方法配置,按规范书写就可以了。

3、执行编译脚本

# dart
dart pub run build_runner build

# flutter	
flutter pub run build_runner build

// 个人更建议使用 watch 命令
// 该命令监听输入,可以实时编译最新的代码,不用每次修改之后重复使用 build 了
flutter pub run build_runner watch

4、基本使用

import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
import 'package:retrofit_example/example.dart';

final logger = Logger();

void main(List<String> args) {
  final dio = Dio(); // Provide a dio instance
  dio.options.headers['Demo-Header'] = 'demo header'; // config your dio headers globally
  final client = RestClient(dio);

  client.getTasks().then((it) => logger.i(it));
}

5、更多的请求方式

  @GET('/tasks/{id}')
  Future<Task> getTask(@Path('id') String id);
  
  @GET('/demo')
  Future<String> queries(@Queries() Map<String, dynamic> queries);
  
  @GET('https://httpbin.org/get')
  Future<String> namedExample(
      @Query('apikey') String apiKey,
      @Query('scope') String scope,
      @Query('type') String type,
      @Query('from') int from);
  
  @PATCH('/tasks/{id}')
  Future<Task> updateTaskPart(
      @Path() String id, @Body() Map<String, dynamic> map);
  
  @PUT('/tasks/{id}')
  Future<Task> updateTask(@Path() String id, @Body() Task task);
  
  @DELETE('/tasks/{id}')
  Future<void> deleteTask(@Path() String id);
  
  @POST('/tasks')
  Future<Task> createTask(@Body() Task task);
  
  @POST('http://httpbin.org/post')
  Future<void> createNewTaskFromFile(@Part() File file);
  
  @POST('http://httpbin.org/post')
  @FormUrlEncoded()
  Future<String> postUrlEncodedFormData(@Field() String hello);

6、在方法中额外添加请求头

  @GET('/tasks')
  Future<Task> getTasks(@Header('Content-Type') String contentType);
  
  -- or -- 
  
  import 'package:dio/dio.dart' hide Headers;
  
  @GET('/tasks')
  @Headers(<String, dynamic>{
    'Content-Type': 'application/json',
    'Custom-Header': 'Your header',
  })
  Future<Task> getTasks();

官方后续文档就不在这里复述了,下面记录一下我自己的使用方式。

二、Retrofit 个人使用

如官方文档所述,Retrofit的使用本就十分简单,这里更多的是对请求使用的归纳。

1、创建请求体

创建文件api_client.dart,该文件主要是对请求方法的编写。

...
@RestApi()
abstract class ApiClient {
  factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;
  // 定义请求方法
  ...
 }

2、创建dio请求拦截

创建文件interceptor.dart,文件主要是对发起请求响应结果的通用处理,简化我们在使用过程中,重复的处理公共模块。


class NetInterceptor extends Interceptor {
  NetInterceptor();
    
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // 在这里可以配置请求头,设置公共参数
    final token = UserService.to.token.token;
    if (token.isNotEmpty) {
      options.headers['Authorization'] = token;
    }
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // 预处理请求返回结果,处理通用的错误信息
    // 包括不限于数据格式错误、用户登录失效、 需单独处理的额外约定错误码
    Map dataMap;
    if (response.data is Map) {
      dataMap = response.data;
    } else if (response.data is String) {
      dataMap = jsonDecode(response.data);
    } else {
      dataMap = {'code': 200, 'data': response.data, 'message': 'success'};
    }

    if (dataMap['code'] != 200) {
      if (dataMap['code'] == 402 || dataMap['code'] == 401) {
        // _ref.read(eventBusProvider).fire(AppNeedToLogin());
      }
      handler.reject(
        DioError(
          requestOptions: response.requestOptions,
          error: dataMap['message'],
        ),
        true,
      );
      return;
    }
    response.data = dataMap['result'];
    handler.next(response);
  }
}

3、独立请求参数类

创建文件params.dart,文件主要目的是存放请求类型参数(毕竟官方推荐使用具体类型作为结构参数,非Map),当然也可以直接使用我们请求结果的数据模型作为请求参数(但是不是所有的方法都合适),简单的参数还是不用写这,个人感觉还是太复杂了,不够简洁。

具体没啥说的,直接使用json_serializable就可以了,当然也可以手写,这里推荐一下 VS Code插件 - Dart Data Class Generator,直接定义好属性之后直接通过提示扩展对应的方法就好了。

import 'package:json_annotation/json_annotation.dart';
part 'params.g.dart';

@JsonSerializable()
class TokenParams {
  @JsonKey(name: 'client_id')
  final String clientId;

  TokenParams(this.clientId);
  factory TokenParams.fromJson(Map<String, Object?> json) =>
      _$TokenParamsFromJson(json);
  Map<String, dynamic> toJson() => _$TokenParamsToJson(this);
}

4、添加请求桥接(独立基础请求配置)

创建实际请求类repository.dart,简化实际使用


class NetRepository {
  /// 独立请求体 
  static ApiClient client = ApiClient(
    Dio(BaseOptions())
      ..interceptors.addAll([
        LogInterceptor(
          requestBody: true,
          responseBody: true,
        ),
        NetInterceptor(),
      ]),
    baseUrl: _devDomain.host,
  );
  
  /// 如果域名不一致可以独立创建,方便区分
  static ApiClient user...
  static ApiClient company...
  
}

final _devDomain = AppDomain(
  host: 'https://api.apiopen.top/api',
  pcHost: 'http://www.xxx.com ',
);

// 定义域名配置,用类的形式只是为了更好的管理和使用
// 当然这里也可以直接换成枚举、常量字符串等等,看个人编写习惯
class AppDomain {
  /// 接口域名
  final String host;

  /// 电脑端地址
  final String pcHost;

  /// final String host1;
  /// final String host2;
  /// ...

  AppDomain({
    required this.host,
    required this.pcHost,
  });
}

5、使用案例

这里是搬用上一篇 一站式刷新和加载 的使用场景,其他地方放使用类似。


  @override
  FutureOr fetchData(int page) async {
    try {
      final data = await NetRepository.client.videoList(page, 20);
      await Future.delayed(const Duration(seconds: 1));
      if (tag) {
        endLoad(data.list as List<VideoList>, maxCount: data.total);
      } else {
        tag = true;
        endLoad([], maxCount: data.total);
      }
    } catch (e) {
      formatError(e);
    }
  }

总结

如果使用这个请求库的话,可以极大的简化我们样板式代码的书写,还是值得推荐的。 一切的一切就是都是为了更简单,也算是为了尽量少写没用的代码而努力。

毕竟不想当将军的士兵不是好士兵,不想写代码的程序猿才是好猿。 ( ̄▽ ̄)"

附Demo地址: boomcx/template_getx

转载自:https://juejin.cn/post/7244358444349128763
评论
请登录