背景
APT 全称 Annotation Processing Tool,主要是用来处理注解的。我们可以使用注解标记代码中的某些逻辑,比如类,方法,域等,然后在编译的javac过程中就会调用到注解处理器进行注解的处理。一般可以进行辅助代码生成或者是一些问题检测。
最近在项目中使用到了MultiType(https://github.com/drakeet/MultiType) 这个框架,实现一个拥有多种类型item的列表还是挺好用的,不过就是需要写比较多的binder类。
github的demo可以看到每次添加一个类型的ViewHolder,都需要创建多一个Binder类:
class PostViewBinder : ItemViewBinder<Post, PostViewBinder.ViewHolder>() {
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
return ViewHolder(inflater.inflate(R.layout.item_post, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, item: Post) {
holder.setData(item)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val cover: ImageView = itemView.findViewById(R.id.cover)
private val title: TextView = itemView.findViewById(R.id.title)
fun setData(post: Post) {
cover.setImageResource(post.coverResId)
title.text = post.title
}
}
}
这个Binder类主要用来生厂ViewHolder的,功能比较简单并且单一,所以这里我们可以使用APT来进行代码生成,做一个代码生成demo。
实现
接下来就开始实现这个工厂类的生成demo,顺便学习一下APT的基本使用。
api工程
创建一个java工程,主要做最顶层的依赖,以及一些通用的接口。
api工程的build.gradle
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api('me.drakeet.multitype:multitype:3.4.0')
}
sourceCompatibility = "7"
targetCompatibility = "7"
这里依赖了mutitype
另外还有一个IViewHolder的接口,主要每一个ViewHolder都需要实现该接口
public interface IViewHolder<T> {
void onBindData(T t);
}
annotation工程
主要是声明注解类,也是一个java library
build.gradle
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
创建一个ViewHolderBinder的注解接口,用于返回ViewHolder对应的xml
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
//@Documented
//@Inherited
public @interface ViewHolderBinder {
int xml() default 0;
}
这个注解接口上面还有几个注解声明:
1、Target
代表着我们的注解可以使用在哪些地方,主要取值有以下:
public enum ElementType {
TYPE, // 对类,接口
FIELD, // 域
METHOD, // 方法
PARAMETER, // 参数声明
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类型
PACKAGE, // 包声明
TYPE_PARAMETER, // 可以使用在type的声明
TYPE_USE; // 可以使用在所有Type的使用地方,比如泛型
private ElementType() {
}
}
2、Retention
主要代表这个注解的生命周期限制,主要有以下选择
public enum RetentionPolicy {
SOURCE, // 存在源码中,编译为class之后就没有了
CLASS, // 存在class中,运行的时候就没有了
RUNTIME; // 直到运行的时候一直都存在
private RetentionPolicy() {
}
}
3、Documented
表明这个注解应该被javadoc工具记录,默认情况下javadoc是不包括注解的,但如果声明了的话则会包含在其中。
4、Inherited
声明被注解标注的类具有自动继承属性。也就是说我们使用Inherited来声明我们定义的ViewHolderBinder注解,然后使用ViewHolderBinder注解来修饰一个类A,而类A有一个子类B,那么B就会自己继承我们的ViewHolderBinder的修饰,不需要手动处理。
这里我们主要把注解用于ViewHolder上,所以Target使用的是Type,而只是在Javac的编译过程中有效,也就是源码到class的过程,所以就指定为Source即可。
compiler工程
compiler工程是主要作用的工程,也是一个java工程
build.gralde
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
api 'com.squareup:javapoet:1.7.0'
api project(":annotation")
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
这里我们需要依赖annotation工程,需要处理其声明的注解类;另外还依赖了auto-service,这个可以帮我们自动注册Processor(注意gradle版本>=5.0 build plugin >= 3.4 的时候就需要annotationProcessor auto-service, 不然的话就跑不起来,我也是踩了个大坑);另外还有javapoet工具,辅助我们生产java类。
在这个工程下创建一个AnnotationProcessor类
@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor
注意要添加上@AutoService的注解。
如果不添加该注解的话则需要我们再当前工程目录src/main下创建resources\META-INF\services\目录,然后在该目录下创建文件javax.annotation.processing.Processor,在文件中写上我们的AnnotationProcessor的类路径:com.example.compiler.AnnotationProcessor,这样子才算注册成功。
接下来看一下我们AnnotationProcessor的主要逻辑:
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mMessager = processingEnvironment.getMessager();
mMessager.printMessage(Diagnostic.Kind.NOTE, "init");
}
init 方法,processingEnvironment是类似context一样的东西,我们需要的“工具”都在其中,这里我只是本地存了一个Messager对象,主要用于打印日志。
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(ViewHolderBinder.class.getCanonicalName());
}
这个方法主要返回一个能够处理的注解列表,只有当前工程中有我们AnnotationProcessor支持处理的注解修饰,我们的processor才会跑起来。
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "process");
try {
createFactoryAndHelper(roundEnvironment);
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
如果当前工程中有我们定义的AnnotationProcessor能够处理的注解声明的话,那么process方法就会被调用。综合上面的需求,我们需要使用javapoet在这里为每一个ViewHolder生成一个Factory类以及一个MultiTypeHelper类。
我们先设定要生成的类的样子。
Factory类:
public class Bean1ViewHolderFactory extends ItemViewBinder<Bean1, Bean1ViewHolder> {
@Override
public Bean1ViewHolder onCreateViewHolder(LayoutInflater inflater, ViewGroup parent) {
final View view = inflater.inflate(2131296286, parent, false);
return new Bean1ViewHolder(view);
}
@Override
public void onBindViewHolder(final Bean1ViewHolder vh, final Bean1 data) {
vh.onBindData(data);
}
}
只有两个方法,第一个方法主要是把注解上的xml的id对应的布局给inflate出来,并且作为ViewHolder的构造器创建ViewHolder;第二个方法则是直接把数据丢给了ViewHolder做处理了。
Helper类:
public class MyMultiTypeHelper {
public static void bindVHFactory(MultiTypeAdapter adapter) {
adapter.register(Bean1.class, new Bean1ViewHolderFactory());
adapter.register(Bean2.class, new Bean2ViewHolderFactory());
}
}
只有一个静态方法,通过传入一个adapter进行注册操作,不用我们每次添加一种类型就需要register一次。
我们看看具体的生成逻辑:
private void createFactoryAndHelper(RoundEnvironment roundEnvironment) throws IOException {
Set<? extends Element> set = roundEnvironment.getElementsAnnotatedWith(ViewHolderBinder.class);
if (set != null) {
ArrayList<BinderInfo> binderList = new ArrayList<>();
for (Element element : set) {
if (element.getKind() == ElementKind.CLASS) {
BinderInfo binderInfo = createFactory((TypeElement)element);
binderList.add(binderInfo);
}
}
crateMyMultiTypeHelper(binderList);
}
}
先获取到ViewHolderBinder注解声明的类,然后遍历进行ViewHolderFactory的生成,然后每次生成都会返回一个BinderInfo对象,最后通过这个binderInfo的list进行MyMultiTypeHelper对象的生成。
class BinderInfo {
TypeName dataClass;
TypeName viewHolderClass;
}
BinderInfo主要存着一个Bean到ViewHolder的映射。
private static final ClassName inflaterClass = ClassName.get("android.view", "LayoutInflater");
private static final ClassName viewClass = ClassName.get("android.view", "View");
private static final ClassName viewGroupClass = ClassName.get("android.view", "ViewGroup");
private static final ClassName multiTypeAdapter = ClassName.get("me.drakeet.multitype", "MultiTypeAdapter");
private static final ClassName itemViewBinder = ClassName.get("me.drakeet.multitype", "ItemViewBinder");
我们先对几个后面会使用到的类进行定义(javapoet的具体使用方法可以查看github的介绍 https://github.com/square/javapoet )
/**
* 创建具体工厂类
* @param element
*/
private BinderInfo createFactory(TypeElement element) throws IOException {
// 获取到data ViewHolder 以及xml信息
TypeName contentDataClassName = getContentClassName(element);
TypeName viewHolderClassName = TypeName.get(element.asType());
int xmlType = element.getAnnotation(ViewHolderBinder.class).xml();
// 创建onCreateViewHolder方法
MethodSpec onCreateViewHolderMethod = MethodSpec.methodBuilder("onCreateViewHolder")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(inflaterClass, "inflater")
.addParameter(viewGroupClass, "parent")
.returns(TypeName.get(element.asType()))
.addCode("final $T view = inflater.inflate($L, parent, false);\n", viewClass, xmlType)
.addCode("return new $T(view);\n", viewHolderClassName)
.addModifiers()
.build();
// 创建onBindViewHolder方法
MethodSpec onBindViewHolderMethod = MethodSpec.methodBuilder("onBindViewHolder")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(viewHolderClassName,"vh", Modifier.FINAL)
.addParameter(contentDataClassName, "data", Modifier.FINAL)
.addCode("vh.onBindData(data);\n")
.build();
// 实现的接口对象
TypeName parentClassType = ParameterizedTypeName.get(itemViewBinder, contentDataClassName, viewHolderClassName);
// 创建XXFactory类对象
TypeSpec typeSpec = TypeSpec.classBuilder(element.getSimpleName() + "Factory")
.addModifiers(Modifier.PUBLIC)
.superclass(parentClassType)
.addMethod(onCreateViewHolderMethod)
.addMethod(onBindViewHolderMethod)
.build();
// 写入文件,生成.java文件
JavaFile javaFile = JavaFile.builder("com.hh.example", typeSpec).build();
javaFile.writeTo(processingEnv.getFiler());
// 返回获取到的信息
BinderInfo binderInfo = new BinderInfo();
binderInfo.viewHolderClass = viewHolderClassName;
binderInfo.dataClass = contentDataClassName;
return binderInfo;
}
这是生成XXXFactory的逻辑。
private void crateMyMultiTypeHelper(ArrayList<BinderInfo> binderList) throws IOException {
// 方法体内容
CodeBlock.Builder codeBlockBuilder = CodeBlock.builder();
for (BinderInfo info : binderList) {
codeBlockBuilder.add("adapter.register($T.class, new $TFactory());\n", info.dataClass, info.viewHolderClass);
}
// 创建bindVHFactory方法
MethodSpec getBindersMethod = MethodSpec.methodBuilder("bindVHFactory")
.addModifiers(Modifier.STATIC, Modifier.PUBLIC)
.addParameter(multiTypeAdapter, "adapter")
.returns(void.class)
.addCode(codeBlockBuilder.build())
.build();
// 创建MyMultiTypeHelper 对象
TypeSpec typeSpec = TypeSpec.classBuilder("MyMultiTypeHelper")
.addModifiers(Modifier.PUBLIC)
.addMethod(getBindersMethod)
.build();
// 写入文件
JavaFile javaFile = JavaFile.builder("com.hh.example", typeSpec).build();
javaFile.writeTo(processingEnv.getFiler());
}
生成MyMultiTypeHelper类的逻辑。
app工程
最后就是使用的逻辑了。
build.gradle
dependencies {
api project(':annotation')
api project(':api')
annotationProcessor project(':compiler')
}
主要是dependcies部分,需要依赖annotation和api,然后还需要注册annotation处理module compiler。
代码的话声明了两个Bean类,Bean1.java, Bean2.java
然后对应声明了两个ViewHolder类,Bean1ViewHolder.java Bean2ViewHolder.java. 都差不多,贴一下其中一个的代码:
@ViewHolderBinder(xml = R.layout.item_bean1)
public class Bean1ViewHolder extends RecyclerView.ViewHolder implements IViewHolder<Bean1> {
public Bean1ViewHolder(View itemView) {
super(itemView);
}
@Override
public void onBindData(Bean1 bean) {
}
}
指定了布局为item_bean1.
在RecyclerView的使用逻辑为:
MultiTypeAdapter adapter = new MultiTypeAdapter();
MyMultiTypeHelper.bindVHFactory(adapter);
adapter.setItems(createTestItem());
RecyclerView mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setAdapter(adapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
这样子就ok了。以后我们添加新的item只需要添加布局,Bean类,ViewHolder类即可,其他的逻辑都可以不用动了。
编译后我们可以在工程的app\build\generated\source\apt\debug\com\hh\example 路径下看到生成的类了。
总结
通过这样的一个demo学习到了APT工具的使用基本流程,不过还有一些其他的问题,比如AutoService原理,AnnotationProcessor注册的原理,javaPoet的生成原理等问题还可以继续学习。
这里附上demo的源码:https://github.com/huanghuanhuanghuan/APTDemo.git