搞一个最基本的Dart版本的Retrofit

对于熟悉Retrofit的Android程序员来说, Retrofit确实是可读性极佳, 写起来也没有过多多余的重复代码要写, 对开发人员极是友好. 那我们有办法在dart上也搞一个吗?

于是我找了下pub.dev, 果然找到一个retrofit库. 但我个人对这个库不太满意, 因为它要用code generation来生成一些dart代码. 我一般不太喜欢用flutter的code generation, 而更倾向于自己用groovy或ruby写一个自己的脚本来完成这些工作. 另外, Android版本的Retrofit也没有这项生成代码的中间工作, 于是我就想更加简单地做到一个retrofit框架. 下面就是我的尝试与成功的过程

分解一: 注解 (Annotation)

Java中的Annotation用@interface来定义, Kotlin中用annotation class来定义. 但在Dart中, 任意一个普通的类, 都可以做注解. 所以, 下面的代码是完全合法的.

dart 复制代码
class Todo {
  final int id;
  final String name;
  const FishTodo(this.id, this.name); 
}

class Worker {
  @Todo(23, 'air fly')
  void work(bool isRepeat) {
    print('work : $isRepeat');
  }
}

分解二: 使用反射来读取Annotation中的值

dart中的反射多是在dart:mirror这个包里, 它里面有各种类, 如InstanceMirror, MethodMirror, ParameterMirror, .... , 分别对应了类, 方法, 参数等各种反射相关的类.

(你可以理解为java中的Class, Method, ... 这些反射的类.)

而这些mirror类有一个metadata成员, 这个成员就是我们的注解了. 所以我们若想得到上面的Todo注解的内容, 那就得这样:

dart 复制代码
  //用reflect(obj)得到这个obj的反射对象. 这个
  InstanceMirror mirror = reflect(Worker()); 
  
  //其实就是得到了这个对象的所有方法.  
  //  其中的key, 即Symbol, 可以暂时理解为方法名; 
  //  其中的value, 即MethodMirror, 则是每一个方法对应的反射
  Map<Symbol, MethodMirror> methods = mirror.type.instanceMembers;  //mirror.type是一个ClassMirror对象
  
  // 查看所有方法, 看哪个有Todo注解
  methods.forEach( (symbol, methodMirror) {
      try {
          List<InstanceMirror> annotationList = methodMirror.metadata;
          InstanceMirror annotation = annotationList.firstWhere ( (meta) => meta.reflectee is Todo)
          // 这个mirror的reflectee就是代表此反射的真正对象, 在这即为Todo注解的那个对象了
          Todo todo = annotation.reflectee;
          print('name = ${todo.anme}, id = ${todo.id}'); //=> name = air fly, id = 23
      } catch (err) { 
          // 上面的firstWhere没找到就会扔异常, 这里就catch住, 不做任何处理即可. 毕竟不是每个方法都有Todo注解的
      }
  }
  

上面代码中我用注释详细讲解了一些细节. 另外想说明的就是:

1). dart中的mirror = reflect(obj).type, 类似于我们的Class mirror = obj::class.java一样.

2). 而Map<Symbol, MethodMirror> methods = mirror.type.instanceMembers;, 则是类似 java中的Method[] methods = clazz.getMethods(), 用来得到各种方法

3). mirror.metadata则是表示它的元数据, 或者说表示它的Annotation了.

分解三: 动态代理

在java版本的Retrofit中, 我们定义了一个网络接口的interface, 只定义了方法. 那实际使用上我们却能得到这个interface的对象, 这其实是通过动态代理做到的. 具体的java版本做法, 可以看 从零开始实现一个 mini-Retrofit 框架这篇文章, 解释得很好. 主体代码就是:

java 复制代码
Proxy.newProxyInstance(classLoader, new Class[]{service},
    new InvocationHandler() {
        @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            final Annotation[] annotations = method.getAnnotations();
            Get annotation = annotations.first { it is GET }                
        	final Request request = new Request.Builder()
                .url(annotation.value())
                .get().build();
        	return okHttpClient.newCall(request);        }
    });

好了, 问题回到Dart, Dart中有这样的动态代理吗? 即表面上你是在调用某一个接口, 但其实是在调用InnvocationHandler这个代理类?

: 不好意思, 没有. Dart没有这样的动态代理类.

但是, 但是, Dart有动态代理的功能, 这个功能类似Ruby的动态代理功能. 熟悉Ruby的同学就知道, Ruby元编程里一个重要方法就是method_missing, 即你的类明明没有这个方法, 但你要是调用这个不存在的方法, 其实就是导致这个类的method_missing方法被调用.

Ruby的源码为:

ruby 复制代码
class User 
    def method_missing(name, *args) 
        ....
    end

Dart中也是类似的机制, 只不过这个特殊方法不叫method_missing, 而是叫noSuchMethod方法. 我们来看个例子, 我们的Face类明明没有foo方法, 但我可以让调用foo方法不报错:

dart 复制代码
class Face {
  @override
  dynamic noSuchMethod(Invocation invocation) {
    print('called: $invocation');
    return 23;
  }
}

我们现在这样调用:

dart 复制代码
main() {
    dynamic obj = Face();
    final result = obj.foo();
    print("result = $result"); //=> result = 23
}

这个其实就是我们的动态代理, 即我们可以让一些不存在的方法, 或是一些抽象方法, 在被调用时, 走到noSuchMethod里来, 然后我们在这里面调用Dio来做网络请求.

完结: 真正做一个Retrofit出来

先来看一下基本框架哦.

dart 复制代码
// 定义一个annotation类
class Get {
  final String url;
  const Get(this.url);
}

class UserService {
  @Get('https://www.somesite.com/api/Gut')
  String getUser();

  @override
  dynamic noSuchMethod(Invocation ivc) {
      ... //关键在这里
  }
}

// 这样我们就可以使用它了: 
void main() {
  UserService http = UserService();
  final resp = http.getUser();
  print('resp = $resp');
}

填充那个关键的动态代理方法

关键都在这个noSuchMethod方法里, 我们要用它得到方法mirror, 以及方法所带的annotation, 并调用Dio来网络请求

dart 复制代码
  @override
  dynamic noSuchMethod(Invocation ivc) {
    final objMirror = reflect(this);
    // ivc.memberName即我们所调用的方法名
    final methodMirror = objMirror.type.instanceMembers[ivc.memberName]!; 
    // 找到Get这个annotation注解对象
    final annoMirror = methodMirror.metadata.firstWhere( (meta) meta is Get ); 
    String url = annoMirror.reflectee.url;
    
    Future resp = dio.get(url);
    return resp; // 返回dio得到的response
  }

这样一来, dart平台的Retrofit就做完了 (基本版本). 要想完善, 自然带得有Put, Delete, Post, Query, PathArgument, ...等各种功能的填充了. 这里主要是入个门.

后记

最后说一句, 这里说的Dart平台的Retrofit是有深意的. 真的它只能用于纯Dart平台, 是的, 没法用于Flutter.

原因就是Flutter为了怕开发乱搞, 禁止了dart:mirror的使用, 所以上面的办法都没法用于Flutter平台. 你要是想用, 就会报错:

这也怕也是为什么pub.dev上的retrofit库并没有使用反射, 而是使用了code generation的原因吧.

至于Flutter上如何仿制一个类似的Retrofit(不使用code generation的情况下), 我还在研究. 有结果了再和大家汇报.

相关推荐
Neo Evolution4 小时前
Flutter与移动开发的未来:谷歌的技术愿景与实现路径
android·人工智能·学习·ios·前端框架·webview·着色器
coooliang4 小时前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang4 小时前
Flutter项目中设置安卓启动页
android·flutter
JIngles1234 小时前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-7 小时前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying111916 小时前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力18 小时前
Flutter应用开发:对象存储管理图片
flutter
没头脑的ht1 天前
ios App的启动过程和启动优化
ios
敲代码的鱼哇1 天前
设备唯一ID获取,支持安卓/iOS/鸿蒙Next(uni-device-id)UTS插件
android·ios·uniapp·harmonyos
江上清风山间明月1 天前
Flutter最简单的路由管理方式Navigator
android·flutter·ios·路由·页面管理·navigator