Android应用篇 - 利用 APT 实现 Android 编译时注解

这篇文章来说说 APT。

目录:

  1. APT 概述
  2. 实现目标
  3. 项目框架
  4. 自定义注解模块
  5. 注解处理器模块
  6. API 模块
  7. 项目中的使用

1. APT 概述

在前面的《Java篇 - 深入了解注解》一文中已经讲过,可以在运行时利用反射机制运行处理注解。其实还可以在编译时处理注解,这就不得不说 Java 官方为我们提供的注解处理工具 APT (Annotation Processing Tool )。

APT 用来在编译时期扫描处理源代码中的注解信息,我们可以根据注解信息生成一些文件,比如 Java 文件。利用 APT 为我们生成的 Java 代码,实现冗余的代码功能,这样就减少手动的代码输入,提升了编码效率,而且使源代码看起来更清晰简洁。

从 Java5 开始,JDK 就自带了注解处理器 APT,不过从近几年开始 APT 才真正的流行起来,这要得益于 Android 上各种主流库都用了 APT 来实现,比如 Dagger、ButterKnife、AndroidAnnotation、EventBus 等。现在我们利用 APT 技术来实现自己的编译时注解。

2. 实现目标

在 Android 开发中我们经常要编写如下冗余的代码:

    Button button = (Button) findViewById(R.id.button1);
    Button button2 = (Button) findViewById(R.id.button2);

    button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {}
        });

    button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {}
    });

编写上面这些冗余代码不但浪费时间,而且一定程度上造成源代码冗余复杂。那么为了解决这种问题,我们可以利用注解和 APT工具生成一个代理类 (ProxyClass),让这个代理类帮助我们实现上面这些冗余的代码。首先我们通过自定义的注解来注解要处理的元素:

public class MainActivity extends AppCompatActivity {

    @ViewById(R.id.tv)
    TextView textView;
    @ViewById(R.id.btn)
    Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ProxyTool.bind(this);
    }

    @Click({R.id.tv, R.id.btn})
    public void myClick(View view) {
        //Click
    }
}

然后我们利用 APT 生成 MainActivity 的代理类 MainActivity$$Proxy,让该类帮我们实现冗余的功能:

public class MainActivity$$Proxy implements IProxy<MainActivity> {

  @Override
  public void inject(final MainActivity target, View root) {
    target.button = (Button) (root.findViewById(R.id.btn));
    target.textView = (TextView) (root.findViewById(R.id.tv));

    View.OnClickListener listener;

    listener = new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        target.myClick(view);
      }
    } ;

    (root.findViewById(R.id.btn)).setOnClickListener(listener);
    (root.findViewById(R.id.tv)).setOnClickListener(listener);
  }
}

下面我们就要来实现上面的功能。

3. 项目框架

我把这个的注解框架命名为 ProxyTool,把该框架分为四个模块,前三个为核心模块:

  • proxytool-api:框架 api 模块,供使用者调用,Android Library 类型模块。
  • proxytool-annotations:自定义注解模块,Java 类型模块。
  • proxytool-compiler:注解处理器模块,用于处理注解并生成文件,Java 类型模块。
  • proxytool-sample:示例 Demo 模块,Android 工程类型模块。


其中这四个模块的依赖关系如下:

  • proxytool-api 依赖 proxytool-annotations 模块。
  • proxytool-compiler 依赖 proxytool-annotations 模块。
  • proxytool-sample 模块依赖 proxytool-api 模块。

有人也许会问,为什么不可以将这写模块写在一起呢?

因为注解处理模块器 proxytool-compiler 只在我们编译过程中需要使用到,在 APP 运行阶段就不需要使用该模块了。所以在发布APP 时,我们就不必把注解处理器模块打包进来,以免造成程序臃肿,所以把 proxytool-compiler 模块单独拿出来。同时注解处理模块和 api 模块都需要使用到自定义注解模块,所以就需要把自定义注解模块单独拿出来。这样为何需要分成三个模块的原因也就一目了然了,其实 butterfnife 框架也是这样分的。

4. 自定义注解模块

首先我们在自定义注解模块中定义两个注解类型,分别用于绑定 View 的 id 和注册 View 的点击事件:

/**
 *  Bind a field to the view for the specified ID
 */
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface ViewById  {
    int value();
}

/**
 * Bind a method to an android.view.View.OnClickListener on the view for each ID specified.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnClick {
   public int[] value();
}

ViewById 中的 value 用于接收注解该 View 的 id 值,OnClick 中的 value 数组用于接收一组 View 的 id 值,这些 view 会被注册点击响应事件。因为这些注册只在编译时有需要用到,程序运行时就不再需要了,所以我们把这些注解定义成编译时保留(RetentionPolicy.SOURCE) 即可。限于篇幅的考虑,下面的介绍中我只对 ViewById 注解做处理,OnClick 注解处理也是类似的。

5. 注解处理器模块

创建好自定义注解后,我们就需要利用 Java 提供的注解处理器根据自定义的注解来生成代理类了,该模块需要依赖其他三个模块:

compile 'com.squareup:javapoet:1.7.0'
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile project(':proxytool-annotations')
  • javapoet 是 square 公司出的一个帮助我们非常方便生成 Java 代码文件的第三方库,避免我们手动拼接字符串的麻烦。
  • auto-service 是 google 公司出的第三方库,主要用于注解处理器,可以自动帮我们生成 META-INF 配置信息。
  • 注解处理器要根据自定义注解进行解析,所以也需要依赖该模块。

让我们看一下注解处理器的 API,所有的注解处理器都必须继承 AbstractProcessor,如下所示:

/**
 * 注解处理器
 */
@AutoService(Processor.class)
public class ProxyToolProcessor extends AbstractProcessor {

    private Filer mFiler; //文件相关工具类
    private Elements mElementUtils; //元素相关的工具类
    private Messager mMessager; //日志相关的工具类

    /**
     * 处理器的初始化方法,可以获取相关的工具类
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }

    /**
     * 处理器的主方法,用于扫描处理注解,生成java文件
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
       ...

        return false;
    }

   /**
    *  指定哪些注解应该被注解处理器注册
    */
    @Override
    public Set<String> getSupportedOptions() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ViewById.class.getName());
        types.add(OnClick.class.getName());
        return  types;
    }

   /**
    *  用来指定你使用的 java 版本
    */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
  • @AutoService(Processor.class) 属于 auto-service 库,可以自动生成 META-INF/services/javax.annotation.processing.Processor 文件 (该文件是所有注解处理器都必须定义的),免去了我们手动配置的麻烦。
  • init(ProcessingEnvironment processingEnvironment) 在处理器初始化的调用,通过 processingEnv 参数我们可以拿到一些实用的工具类 Elements, Messager 和 Filer,我们在后面将会使用到它们。Elements,一个用来处理 Element 的工具类。Messager,一个用来输出日志信息的工具类。Filer、如这个类的名字所示,你可以使用这个类来创建文件。
  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 这是注解处理器的主方法,你可以在这个方法里面编码实现扫描,处理注解,生成 Java 文件。
  • getSupportedAnnotationTypes() 在这个方法里面你必须指定哪些注解应该被注解处理器注册。它的返回值是一个 String 集合,包含了你的注解处理器想要处理的注解类型的全限定名。
  • getSupportedSourceVersion() 用来指定你使用的 Java 版本,通常我们返回 SourceVersion.latestSupported() 即可。
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({
        "proxytool.ViewById",
        "proxytool.OnClick"
})
public class MyProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annoations,
            RoundEnvironment env) {
        return false;
    }
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }
}

但是考虑到兼容性问题,建议还是重写 getSupportedSourceVersion() 方法和 getSupportedAnnotationTypes() 方法。

在继续讲解处理器之前,我们必须先明白 Element 元素这个概念。在注解处理器中,我们扫描 Java 源文件,源代码中的每一部分都是 Element 的一个特定类型。换句话说:Element 代表程序中的元素,比如说 包,类,方法。在下面的例子中,我将添加注释来说明这个问题:

package com.example; //PackageElement

public class Foo { // TypeElement

    private int a; // VariableElement
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {
    }
}
  • ExecuteableElement:可以表示一个普通方法、构造方法、初始化方法 (静态和实例)。
  • PackageElement:代表一个包名。
  • TypeElement:代表一个类、接口。
  • VariableElement:代表一个字段、枚举常量、方法或构造方法的参数、本地变量、或异常参数等。
  • Element:上述所有元素的父接口,代表源码中的每一个元素。

在注解处理器世界中,整个 Java 代码被结构化了。我们需要像解析 XML 文件一样去解析整个源代码。Element 就像 XML 解析器中的 DOM 元素,你可以通过如下两个方法获取该元素的子元素和父元素:

Element getEnclosingElement();  //获取父元素
List<? extends Element> getEnclosedElements(); //获取子元素

比如你有如下的一个类:

public class Foo {

    private int a; // VariableElement
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement
}

成员变量 a 通过 getEnclosingElement() 方法返回的是的父元素是类 Foo,类 Foo 通过 getEnclosedElements() 返回的子元素就包括成员变量 a、成员变量 other 以及构造方法 Foo()。关于这两个方法更多的解释,可以参见官方文档。

  • 5.1 收集注解信息

注解处理器中,最核心的方法就是 process(),在这里你可以扫描和处理注解,并生成 Java 文件。首先我们扫描所有被@ViewById:

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

   //处理被ViewById注解的元素
    for (Element element : roundEnv.getElementsAnnotatedWith(ViewById.class)) {
        if (!isValid(ViewById.class, "fields", element)) {
             return true;
        }
        parseViewById(element);
    }
  ...

注解的元素:

通 roundEnvironment.getElementsAnnotatedWith(ViewById.class) 方法返回一个被 @ViewById 注解的 Element 类型的元素列表。注意这里是 Element 列表,而不是类列表,Element 可以包括类、方法、变量等。

  • 5.2 匹配准则

接下来我们需要对这个元素做进一步的检查,保证被注解的元素是符合规范的。如果使用者不按规范随意注解元素的话,程序是无法正常运行的。所以我们需要执行 isValid() 方法用于检测被注解元素的合法性:

private boolean isValid(Class<? extends Annotation> annotationClass, String targetThing, Element element) {
        boolean isVaild = true;

        //获取变量的所在的父元素,肯能是类、接口、枚举
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
        //父元素的全限定名
        String qualifiedName = enclosingElement.getQualifiedName().toString();

        // 所在的类不能是private或static修饰
        Set<Modifier> modifiers = element.getModifiers();
        if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
            error(element, "@%s %s must not be private or static. (%s.%s)",
                    annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            isVaild = false;
        }

        // 父元素必须是类,而不能是接口或枚举
        if (enclosingElement.getKind() != ElementKind.CLASS) {
            error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
                    annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            isVaild = false;
        }

        //不能在Android框架层注解
        if (qualifiedName.startsWith("android.")) {
            error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
                    annotationClass.getSimpleName(), qualifiedName);
            return false;
        }
        //不能在java框架层注解
        if (qualifiedName.startsWith("java.")) {
            error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
                    annotationClass.getSimpleName(), qualifiedName);
            return false;
        }

        return isVaild;
    }

    private void error(Element e, String msg, Object... args) {
        mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
    }

在 isValid() 方法中,它检查被注解的元素是否符合规则:

  • 被注解元素的父元素必须是个类,而不能是接口、枚举。
  • 被注解元素的父元素必须是非 private 和非 static 修饰。
  • 被注解的元素只能注解非框架层元素。

这里只是简单的列出几个是否符合注解规范的条件,更严格的判断条件还需要大家来完善。

  • 5.3 错误处理

不知道大家有没有发现,在 error() 方法中利用了 Messager (init() 方法中获取到的工具类) 来处理错误信息。Messager 为注解处理器提供了一种报告错误消息,警告信息和其他消息的方式。它不是注解处理器开发者的日志工具,Messager 是用来给那些使用了你的注解处理器的第三方开发者显示信息的。

其中非常重要的是 Kind.ERROR 级别信息,因为这种消息类型是用来表明我们的注解处理器在处理过程中出错了。有可能是第三方开发者误使用了我们的 @ViewById 注解 (比如,使用 @ViewById 注解了一个接口中的变量)。这个概念与传统的 Java 应用程序有一点区别,传统的 Java 应用程序出现了错误,你可以抛出一个异常。如果你在 process() 中抛出了一个异常,那 JVM 就会崩溃。注解处理器的使用者将会得到一个从 javac 给出的非常难懂的异常错误信息,因为它包含了注解处理器的堆栈信息。因此注解处理器提供了 Messager 类,它能打印漂亮的错误信息,而且你可以链接到引起这个错误的元素上。回到 process 中:

 @Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
   //处理被ViewById注解的元素
   for (Element element : roundEnvironment.getElementsAnnotatedWith(ViewById.class)) {
        if (!isValid(ViewById.class, "fields", element)) {
            return true;
        }
        parseViewById(element);
    }

    ...
}

为了能够获取 Messager 显示的信息,非常重要的是注解处理器必须不崩溃地完成运行。这就是我们在调用 error() 后,跳出isValid(),执行 return true 的原因。如果我们在这里没有返回的话,process() 就会继续运行,因为 messager.printMessage( Diagnostic.Kind.ERROR) 并不会终止进程。

  • 5.4 数据模型

一旦 isValid() 方法检查通过,那么就表示该注解元素是可以使用的,因此我们继续执行 parseViewById() 方法,把这些元素封装成 model,供后面生成 Java 文件时使用。

private Map<String, ProxyClass> mProxyClassMap = new HashMap<>();

/**
 * 处理ViewById注解
 *
 * @param element
 */
private void parseViewById(Element element) {
    ProxyClass proxyClass = getProxyClass(element);
    //把被注解的view对象封装成一个model,放入代理类的集合中
    FieldViewBinding bindView = new FieldViewBinding(element);
    proxyClass.add(bindView);
}

/**
 * 生成或获取注解元素所对应的ProxyClass类
 */
private ProxyClass getProxyClass(Element element) {
    //被注解的变量所在的类
    TypeElement classElement = (TypeElement) element.getEnclosingElement();
    String qualifiedName = classElement.getQualifiedName().toString();
    ProxyClass proxyClass = mProxyClassMap.get(qualifiedName);
    if (proxyClass == null) {
        //生成每个宿主类所对应的代理类,后面用于生产java文件
        proxyClass = new ProxyClass(classElement, mElementUtils);
        mProxyClassMap.put(qualifiedName, proxyClass);
    }
    return proxyClass;
}
  • parseViewById(Element element):在该方法中,首先需要通过 getProxyClass() 方法获取一个 ProxyClass 类型的对象。ProxyClass 代表了该注解元素所对应的类元素,这里我们利用面向对象的思想进行了封装。然后我们把被注解的元素也封装成一个 FieldViewBinding 类型的 model,并放入到 ProxyClass 中。
  • getProxyClass(Element element):该方法主要是生成或获取注解元素所对应的类。你可以在 getProxyClass() 方法中看到,利用 getEnclosingElement() 方法获取了该注解元素的父元素,也就是该注解元素的所在的类。我们把每一个类元素TypeElement 都封装成了 ProxyClass,并保存在 HashMap 中。

上面这两个方法的作用,主要是把注解的元素和注解元素所在的类都封装成了 model,并存储起来,供我们后面生成 Java 源码时使用,下面表示了两个 model 类:

public class FieldViewBinding {

    /** 注解元素*/
    private VariableElement mElement;

    /** 资源id*/
    private int mResId;

    /** 变量名*/
    private String mVariableName;

    /**变量类型*/
    private TypeMirror mTypeMirror;

    public FieldViewBinding(Element element) {

        mElement = (VariableElement) element;
        ViewById viewById = element.getAnnotation(ViewById.class);
        //资源id
        mResId = viewById.value();
        //变量名
        mVariableName = element.getSimpleName().toString();
        //变量类型
        mTypeMirror = element.asType();
    }

    public VariableElement getElement() { return mElement;}
    public int getResId() { return mResId;}
    public String getVariableName() {return mVariableName;}
    public TypeMirror getTypeMirror() {return mTypeMirror;}
}
public class ProxyClass {

    /**类元素 */
    public TypeElement mTypeElement;

    /**元素相关的辅助类*/
    private Elements mElementUtils;

    /** FieldViewBinding类型的集合*/
    private Set<FieldViewBinding> bindViews = new HashSet<>();

    public ProxyClass(TypeElement mTypeElement, Elements mElementUtils) {
        this.mTypeElement = mTypeElement;
        this.mElementUtils = mElementUtils;
    }

    public void add(FieldViewBinding bindView) {
        bindViews.add(bindView);
    }


    /**
     * 用于生成代理类
     */
    public JavaFile generateProxy() {
        ...
    }
}

ProxyClass 类中的 generateProxy() 方法是用于生成每个类所对应的代理类,比如类 MainActivity 就会生成类MainActivity$$Proxy,该方法生成的详细过程会后面再讲。

  • 5.5 文件的生成

既然我们已经收集到了注解元素和注解元素所在的类,那么我们就需要为每个类生成一个全新的代理类,在代理类中执行那些冗余的代码操作,继续回到 process() 方法中:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

    ...

    //为每个宿主类生成所对应的代理类
    for (ProxyClass proxyClass_ : mProxyClassMap.values()) {
        try {
            proxyClass_.generateProxy().writeTo(mFiler);
        } catch (IOException e) {
            error(null, e.getMessage());
        }
    }
    mProxyClassMap.clear();

    return true;
}

现在既然已经收集到了每个注解元素所对应的类,那么我们就需要为每个类生成所对应的代理类,遍历所有的类元素集合mProxyClassMap 通过 ProxyClass 的 generateProxy() 方法来生成 Java 源码,ProxyClass##generateProxy():

//proxytool.IProxy
public static final ClassName IPROXY = ClassName.get("proxytool.api", "IProxy");
//android.view.View
public static final ClassName VIEW = ClassName.get("android.view", "View");
//生成代理类的后缀名
public static final String SUFFIX = "$$Proxy";

/**
 * 用于生成代理类
 */
public JavaFile generateProxy() {

    //生成public void inject(final T target, View root)方法
    MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
            .addModifiers(Modifier.PUBLIC)
            .addAnnotation(Override.class)
            .addParameter(TypeName.get(mTypeElement.asType()), "target", Modifier.FINAL)
            .addParameter(VIEW, "root");

    //在inject方法中,添加我们的findViewById逻辑
    for (FieldViewBinding model : bindViews) {
        // find views
        injectMethodBuilder.addStatement("target.$N = ($T)(root.findViewById($L))", model.getVariableName(),
                ClassName.get(model.getTypeMirror()), model.getResId());
    }


    // 添加以$$Proxy为后缀的类
    TypeSpec finderClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + SUFFIX)
            .addModifiers(Modifier.PUBLIC)
            //添加父接口
            .addSuperinterface(ParameterizedTypeName.get(IPROXY, TypeName.get(mTypeElement.asType())))
            //把inject方法添加到该类中
            .addMethod(injectMethodBuilder.build())
            .build();

    //添加包名
    String packageName = mElementUtils.getPackageOf(mTypeElement).getQualifiedName().toString();

    //生成Java文件
    return JavaFile.builder(packageName, finderClass).build();
}

上面我们使用了 javapoet 来帮助我们生成 Java 源码,免去手动拼接字符串的麻烦。生成过程很简单,看注释就明白了。

不知道大家发现了没有循环结束后我们还执行了 mProxyClassMap.clear(),原因就在于 process() 可以被多次调用,因为新生成的 Java 文件很可能包括 @ViewById 注解,所以 process() 方法会多次执行直到没有生成该注解为止。所以我们应该清空之前的数据,避免生成重复的代理类。

6. API 模块

既然已经生成了代理类,那么我还需要提供 API 供使用者访问该代理类,供在 Activity、Fragment、View 中如下使用:

//Activity
ProxyTool.bind(this);

//Fragment
ProxyTool.bind(this, view);

//View
ProxyTool.bind(this);

在 ProxyTool 的 bind() 方法中我们需要为需要为不同的目标 (比如 Activity、Fragment 和 View 等) 提供重载的注入方法,这些方法最终都调用 createBinding() 方法:

public class ProxyTool {

   //Activity
    @UiThread
    public static void bind(@NonNull Activity target) {
        View sourceView = target.getWindow().getDecorView();
        createBinding(target, sourceView);
    }

   //View
    @UiThread
    public static void bind(@NonNull View target) {
        createBinding(target, target);
    }
    //Fragment
    @UiThread
    public static void bind(@NonNull Object target, @NonNull View source) {
        createBinding(target, source);
    }

    public static final String SUFFIX = "$$Proxy";

    public static void createBinding(@NonNull Object target, @NonNull View root) {

        try {
            //生成类名+后缀名的代理类,并执行注入操作
            Class<?> targetClass = target.getClass();
            Class<?> proxyClass = Class.forName(targetClass.getName() + SUFFIX);
            IProxy proxy = (IProxy) proxyClass.newInstance();
            proxy.inject(target, root);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

我们重载了三个 bind() 方法用于接收的不同目标 (Activity、Fragment 和 View 等),target 参数表示注解元素所在的类,root 参数表示要查找的 View 的,因为 Activity 和 View 本身即是 target 也是 root,所以只要一个参数即可,而 Fragment 中类和 View是分离的,所以需要两个参数。

所有 bind() 方法最终调用了createBinding() 方法,在该方法中我们通过 targetClass.getName() + SUFFIX,拼接成代理类的全限定名,然后生成代理类的实例,并执行代理类的 inject() 方法,该方法中执行的就是 findViewById() 的操作。

这里我们还需要注意一点,所有生成的代理类都默认实现 IProxy 接口:

public interface IProxy<T> {
    /**

     * @param target 所在的类
     * @param root 查找 View 的地方
     */
    public void inject(final T target, View root);
}

该接口定义了 inject() 方法,代理类中需要在该方法中实现具体的注入逻辑。代理类的生成和 inject() 方法的实现,都是在注解处理器模块中进行处理,具体的生成过程都在文件的生成章节中,这里就不再讲。最终访问 Activity、Fragment、View 都是通过它们的代理类来访问。

7. 项目中的使用

上面三个核心模块都已经介绍完了,现在让我们在具体的项目中使用吧。

首先在整个工程的 build.gradle 中添加如下:

dependencies {
     ...
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}

然后在自己 module 的 build.gradle 中添加插件和依赖,如下所示:

apply plugin: 'com.neenbedankt.android-apt'

...

dependencies {
    ...
    compile project(':proxytool-api')
    apt project(':proxytool-compiler')
}

然后我们在项目中使用该框架的注解和 API:

public class MainActivity extends AppCompatActivity {

    @ViewById(R.id.btnOne)
    Button btnOne;
    @ViewById(R.id.btnTwo)
    Button btnTwo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ProxyTool.bind(this);
    }

    @OnClick({R.id.btnOne,R.id.btnTwo})
    public void myClick(View view){
        btnOne.setText("111111");
        btnTwo.setText("222222");
    }
}

执行 Make Project 操作后,在 build/generated/source/apt/debug/ 下就会生成所对应的代理类 MainActivity$$Proxy:

public class MainActivity$$Proxy implements IProxy<MainActivity> {
  @Override
  public void inject(final MainActivity target, View root) {
    target.btnTwo = (Button)(root.findViewById(2131427414));
    target.btnOne = (Button)(root.findViewById(2131427413));
    View.OnClickListener listener;
    listener = new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        target.myClick(view);
      }
    } ;
    (root.findViewById(2131427413)).setOnClickListener(listener);
    (root.findViewById(2131427414)).setOnClickListener(listener);
  }
}


 

发布了126 篇原创文章 · 获赞 215 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/u014294681/article/details/88757868