Android开发之手把手教你写ButterKnife框架(三)
欢迎转载,转载请标明出处:
http://blog.csdn.net/johnny901114/article/details/52672188
本文出自:【余志强的博客】
一、概述
上一篇博客讲了,如何在android studio使用apt 《 Android开发之手把手教你写ButterKnife框架(二)》
然后在Processor里生成自己的代码,把要输出的类,通过StringBuilder拼接字符串,然后输出。
try { // write the file
JavaFileObject source = processingEnv.getFiler().createSourceFile("com.chiclaim.processor.generated.GeneratedClass");
Writer writer = source.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
输出简单的类这种方法还是挺好的,简单明了,如果要输出复杂点的java文件,这个就不是很方便了,接下来介绍一个square公司开源的框架javapoet来帮助我们构建java文件。
二、 javapoet简单使用
通过MethodSpec类来构建java方法,如:
MethodSpec main = MethodSpec.methodBuilder("main") //方法名
.addModifiers(Modifier.PUBLIC, Modifier.STATIC) //方法修饰符
.returns(void.class) //方法返回类型
.addParameter(String[].class, "args") //方法参数
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")//方法体语句
.build();
通过TypeSpec来构建java类的修饰符
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") //类名
.addModifiers(Modifier.PUBLIC, Modifier.FINAL) //类的修饰符
.addMethod(main) 添加类方法(MethodSpec main)
.build();
通过JavaFile输出类
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
通过CodeBlock来构建方法体
上面通过MethodSpec.addaddParameter(String[].class, “args”)方法构建方法体,对于比较复杂的可以通过CodeBlock来构建:
codeBlock.addStatement("$L.inject(this, this)", mViewInjectorName);
javapoet就先介绍到这里。更多具体的使用可以查看官方文档或者其他资料。
三、通过javapoet生成Bind类
在ButterKnifeProcessor process方法中获取基本信息
我们要想达到在生成的类中初始化activity Views,那么肯定需要如下类似下面的代码(伪代码):
public class MainActivity_Binding {
public MainActivity_Binding(MainActivity target,View view) {
target.text1 = (TextView)view.findViewById(id);
target.text2 = (TextView)view.findViewById(id);
target.text3 = (TextView)view.findViewById(id);
target.text4 = (TextView)view.findViewById(id);
}
}
据此,我们需要在ButterKnifeProcessor process
方法里获取三个基本信息:
1、注解所在的类
,用于生成类名,比如MainActivity使用了注解,那么生成的类就是 MainActivity_ViewBinding
2、注解的值
,用于findViewById,如:
@BindView(R.id.title)
TextView title;
那么我们要获取的值就是R.id.title
3、注解所在字段的类型
,用于强转。如:
@BindView(R.id.title)
TextView title;
那么我们要获取的类型就是TextView
通过下面的方法可以获取上面的信息
【element.getEnclosingElement()】 //注解所在的类
【element.getAnnotation(BindView.class).value()】 //注解上的值, 用于findViewById
【element.asType()】 //注解字段的类型,用于强转
通过上一篇博客知道,我们是在process方法里生成代码的:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//TODO do something
return true;
}
对RoundEnvironment里的信息进行分组处理
所有关于类上的注解信息,全部在 RoundEnvironment roundEnv里,而且可能的多个类用到了注解, 所以我们要对RoundEnvironment的信息进行分组处理。
我通过Map来保存分组的信息,
Map
//roundEnv里的信息进行分组
private void parseRoundEnvironment(RoundEnvironment roundEnv) {
// 保存分组信息
Map<TypeElement, BindClass> map = new LinkedHashMap<>();
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
//注解的值
int annotationValue = element.getAnnotation(BindView.class).value();
//如果不存在创建BindClass,要创建的代码都存在BindClass里
BindClass bindClass = map.get(enclosingElement);
if (bindClass == null) {
bindClass = BindClass.createBindClass(enclosingElement);
map.put(enclosingElement, bindClass);
}
String name = element.getSimpleName().toString();
TypeName type = TypeName.get(element.asType());
//ViewBinding用于保存每个注解的相关信息(比如注解所在字段的名称、注解所在字段的类型、注解上的值,)
ViewBinding viewBinding = ViewBinding.createViewBind(name, type, annotationValue);
//因为一个类上可能多处用了注解,所以用一个集合保存
bindClass.addAnnotationField(viewBinding);
}
//迭代分组后的信息,主义生成对应的类
for (Map.Entry<TypeElement, BindClass> entry : map.entrySet()) {
printValue("==========" + entry.getValue().getBindingClassName());
try {
entry.getValue().preJavaFile().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
ViewBinding
用于保存每个注解的相关信息,代码也很简单:
class ViewBinding {
private final String name;
private final TypeName type;
private final int value;
private ViewBinding(String name, TypeName type, int value) {
this.name = name;
this.type = type;
this.value = value;
}
static ViewBinding createViewBind(String name, TypeName type, int value) {
return new ViewBinding(name, type, value);
}
}
BindClass用于保存需要生成的代码,里面封装了javapoet相关处理,所有具有生成代码的功能.
先来看看创建BindClass构造方法:
private BindClass(TypeElement enclosingElement) {
//asType 表示注解所在字段是什么类型(eg. Button TextView)
TypeName targetType = TypeName.get(enclosingElement.asType());
if (targetType instanceof ParameterizedTypeName) {
targetType = ((ParameterizedTypeName) targetType).rawType;
}
//注解所在类名(包括包名)
String packageName = enclosingElement.getQualifiedName().toString();
packageName = packageName.substring(0, packageName.lastIndexOf("."));
String className = enclosingElement.getSimpleName().toString();
//我们要生成的类的类名
ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
//注解所在类,在生成的类中,用于调用findViewById
this.targetTypeName = targetType;
this.bindingClassName = bindingClassName;
//生成的类是否是final
this.isFinal = isFinal;
//用于保存多个注解的信息
fields = new ArrayList<>();
}
添加注解信息实体
void addAnnotationField(ViewBinding viewBinding) {
fields.add(viewBinding);
}
生成类的修饰符,方法:
private TypeSpec createTypeSpec() {
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
if (isFinal) {
result.addModifiers(FINAL);
}
result.addMethod(createConstructor(targetTypeName));
return result.build();
}
创建构造方法,在构造方法里生成初始化View的代码:
private MethodSpec createConstructor(TypeName targetType) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(PUBLIC);
//构造方法有两个参数,target和source,在本例子中,Target就是activity,source就是activity的DecorView
constructor.addParameter(targetType, "target", FINAL);
constructor.addParameter(VIEW, "source");
//可能有多个View需要初始化,也就是说activity中多个字段用到了注解
for (ViewBinding bindings : fields) {
//生成方法里的语句,也就是方法体
addViewBinding(constructor, bindings);
}
return constructor.build();
}
下面看看如何为activity中每个用到注解的View在构造方法中生成初始化代码:
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
//通过CodeBlock生成语句,因为生成的语句比较复杂。
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", binding.getName());
//判断是否需要强制类型转换,如果目标View本来就是View,那就不需要强转了
boolean requiresCast = requiresCast(binding.getType());
if (!requiresCast) {
builder.add("source.findViewById($L)", binding.getValue());
} else {
//我们使用ProcessorUtils重点工具方法findViewByCast进行强转 $T就是一个占位符,UTILS就是ClassName包含了UTILS的包名和类名
//用ProcessorUtils替换成$T CodeBlock还支持很多占位符,需要了解更多可以去看看文档.
builder.add("$T.findViewByCast", UTILS);
//ProcessorUtils.findViewByCast需要的参数source就是DecorView
builder.add("(source, $L", binding.getValue());
//ProcessorUtils.findViewByCast需要的参数$T.class,就是目标View需要强转的类型
builder.add(", $T.class", binding.getRawType());
builder.add(")");
}
result.addStatement("$L", builder.build());
}
下面就是强转用到的工具类:
public class ProcessorUtils {
public static <T> T findViewByCast(View source, @IdRes int id, Class<T> cls) {
View view = source.findViewById(id);
return castView(view, id, cls);
}
private static <T> T castView(View view, @IdRes int id, Class<T> cls) {
try {
return cls.cast(view);
} catch (ClassCastException e) {
//提示使用者类型转换异常
throw new IllegalStateException(view.getClass().getName() + "不能强转成" + cls.getName());
}
}
}
注意, 如果你需要调试public boolean process(Set
// Generated code from My Butter Knife. Do not modify!!!
package com.chiclaim.sample;
import android.view.View;
import android.widget.TextView;
import com.chiclaim.butterknife.ProcessorUtils;
public class MainActivity_ViewBinding {
public MainActivity_ViewBinding(final MainActivity target, View source) {
target.textView = ProcessorUtils.findViewByCast(source, 2131427414, TextView.class);
target.view = ProcessorUtils.findViewByCast(source, 2131427415, TextView.class);
}
}
接下来就简单了,在MainActivity中调用MainActivity_ViewBinding的构造方法就可以了。因为我们生成的类是有规律的,包名就是使用者的包名,类名是使用者类名加ViewBinding。然后通过反射调用下就可以了:
public class MyButterKnife {
public static void bind(Activity activity) {
//获取activity的decorView
View view = activity.getWindow().getDecorView();
String qualifiedName = activity.getClass().getName();
//找到该activity对应的Bind类
String generateClass = qualifiedName + "_ViewBinding";
try {
//然后调用Bind类的构造方法,从而完成activity里view的初始化
Class.forName(generateClass)
.getConstructor(activity.getClass(), View.class).newInstance(activity, view);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
所以只需要在跟butterknife一样在activity的onCreate声明周期方法里调用bind方法即可,如下所示:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.text_view)
TextView textView;
@BindView(R.id.view)
TextView view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//完成初始化操作
MyButterKnife.bind(this);
Toast.makeText(this, textView + "--textView", Toast.LENGTH_LONG).show();
Log.d("MainActivity", textView + "," + view);
textView.setText("initialed by my butter knife");
}
}
四、总结
1> butterknife 是一个运行时依赖祝框架,简化android的大量模板代码,使用apt来生成代码
2> 像很多框架都是跟butterKnife的机制太不多的,比如下面几款流行的框架:
greendao 流行的sqlite框架
dagger2 依赖注入框架
PermissionsDispatcher 处理Android6.0权限的框架
所以利用这个技术,也可以整个自己的框架。
更多实现信息,可以查看github上的源码 https://github.com/chiclaim/study-butterknife
Android开发之手把手教你写ButterKnife框架(三)的更多相关文章
- Android开发之手把手教你写ButterKnife框架(二)
欢迎转载,转载请标明出处: http://blog.csdn.net/johnny901114/article/details/52664112 本文出自:[余志强的博客] 上一篇博客Android开 ...
- Android开发之手把手教你写ButterKnife框架(一)
欢迎转载,转载请标明出处: http://blog.csdn.net/johnny901114/article/details/52662376 本文出自:[余志强的博客] 一.概述 JakeWhar ...
- 手把手教你写DI_1_DI框架有什么?
DI框架有什么? 在上一节:手把手教你写DI_0_DI是什么? 我们已经理解DI是什么 接下来我们就徒手撸一撸,玩个支持构造函数注入的DI出来 首先我们回顾一下 构造函数注入 的代码形式, 大概长这模 ...
- 手把手教你写DI_2_小白徒手撸构造函数注入
小白徒手撸构造函数注入 在上一节:手把手教你写DI_1_DI框架有什么? 我们已经知道我们要撸哪些东西了 那么我们开始动工吧,这里呢,我们找小白同学来表演下 小白同学 :我们先定义一下我们的广告招聘纸 ...
- 手把手教你写DI_0_DI是什么?
DI是什么? Dependency Injection 常常简称为:DI. 它是实现控制反转(Inversion of Control – IoC)的一个模式. fowler 大大大神 "几 ...
- 网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接
本文原作者:“水晶虾饺”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.引言 好多小白初次接触即时通讯(比如:IM或者消息推送应用)时,总是不 ...
- 只有20行Javascript代码!手把手教你写一个页面模板引擎
http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...
- 手把手教你写Sublime中的Snippet
手把手教你写Sublime中的Snippet Sublime Text号称最性感的编辑器, 并且越来越多人使用, 美观, 高效 关于如何使用Sublime text可以参考我的另一篇文章, 相信你会喜 ...
- 手把手教你写LKM rookit! 之 第一个lkm程序及模块隐藏(一)
唉,一开始在纠结起个什么名字,感觉名字常常的很装逼,于是起了个这<手把手教你写LKM rookit> 我觉得: 你们觉得:...... 开始之前,我们先来理解一句话:一切的操作都是系统调用 ...
随机推荐
- 一次完败的Release
一次完败的Release 去年8月份加入一家创业公司,和原同事做VR相关的产品开发,到18年正月初七,总共release过两次,真正经理了一次从0到1的过程.第一次release产品初步成型,大概在1 ...
- 用 k8s 管理机密信息 - 每天5分钟玩转 Docker 容器技术(155)
应用启动过程中可能需要一些敏感信息,比如访问数据库的用户名密码或者秘钥.将这些信息直接保存在容器镜像中显然不妥,Kubernetes 提供的解决方案是 Secret. Secret 会以密文的方式存储 ...
- 简述SharePoint designer 工作流实现用户撤回申请得解决方案,how to revoke/recall application in SharePoint designer workflow
在工作中很容易碰到用户提出,需要可以撤回申请得需求.有这么几种实现方法. 方案一:designer中加一个parallel(平行)得anction, 然后在里面添加两个平行运行得审批 第一个是给真正得 ...
- 《阿里巴巴 Java 开发手册》读书笔记
偶然看到阿里巴巴居然出书了???趁着满减活动(节约节约....)我赶紧买来准备看看,刚拿到的时候掂量了好多下,总觉得商家给我少发了一本书,结果打开才知道..原来这本书这么小.... 编码规范的重要性 ...
- tmux 终端复用详解
tmux是什么 我们在linux服务器上的工作一般都是通过一个远程的终端连接软件连接到远端系统进行操作,例如使用xshell或者SecureCRT工具通过ssh进行远程连接.在使用过程中,如果要做比较 ...
- ActiveMQ笔记:一个高稳定,可扩展的的部署方案
本文介绍一个笔者在实际工作中的实施的基于ActiveMQ的一个高稳定,可扩展的异步消息系统. ActiveMQ是一个成熟的基于Java语言的开源消息系统,在实际应用中被大量使用.ActiveMQ在系统 ...
- [Luogu 3768]简单的数学题
Description 输入一个整数n和一个整数p,你需要求出$(\sum_{i=1}^n\sum_{j=1}^n ijgcd(i,j))~mod~p$,其中gcd(a,b)表示a与b的最大公约数. ...
- bzoj 5286: [Hnoi2018]转盘
Description Solution 首先注意到一个点不会走两次,只会有停下来等待的情况,把序列倍长 那么如果枚举一个起点\(i\),答案就是 \(min(max(T[j]+n-(j-i)-1)) ...
- IP地址、子网掩码、网关、DNS服务器
1. IP地址 IP是英文Internet Protocol的缩写,意思是"网络之间互连的协议",也就是为计算机网络相互连接进行通信而设计的协议.在因特网中,它是能使连接到网上的所 ...
- C语言如何输出%
两个%即可,C语言中%有两个作用: 第一种是作为运算符,取余,例如:9%4=1(9/4=2--1). 第二种是转义符,比如在scanf()和printf()中的输入参数常出现带有%的表示参数类型的变量 ...