使用APT减少MVP的冗余代码
前言
不知道从何时起,移动端开发都开始采用MVP。我们在认识到MVP有点的时候,也不妨会察觉到它其实也有很多恼人的地方,比如,我们针对每种状态渲染不同的视图:
privatevoidrenderInit(){
mViewA.setVisibility(View.VISIBLE);
mViewB.setVisibility(View.GONE);
mViewC.setVisibility(View.GONE);
mViewD.setVisibility(View.GONE);
mViewE.setVisibility(View.GONE);
}
privatevoidrenderSummary(){
mViewA.setVisibility(View.GONE);
mViewB.setVisibility(View.VISIBLE);
mViewC.setVisibility(View.GONE);
mViewD.setVisibility(View.GONE);
mViewE.setVisibility(View.GONE);
}
可以看到在这里,我们渲染Init状态时,把ViewA设为可见,把其他的View设为不可见,当我们又去渲染Summary状态是,又重复上面的动作,不过这次是吧ViewB设为可见。这种冗余代码(或者说是模板代码)非常的烦人,因为我们在复制粘贴的时候极有可能设置错误的View为可见了。那么我们有没有什么办法来避免这样的问题呢。其实是有的,我们不妨回忆下ButterKnife怎么做的——对于findViewById这样的冗余代码,ButterKnife是采用注解的方式解决的:
@Bind(R.id.id_name)
TextViewm_name;
@Bind(R.id.id_who)
TextViewm_who;
@Bind(R.id.id_musicBar)
MusicBarm_musicBar;
@Bind(R.id.id_playControl)
ImageViewm_bottomPlayControlView;
@Override
protectedIViewcreateView(){
returnthis;
}
@Override
protectedvoidonCreate(@NullableBundlesavedInstanceState){
super.onCreate(savedInstanceState);
ButterKnife.bind(this);
...
}
在执行
ButterKnife.bind(this);
1
后,ButterKnife会采用APT自动生成代码执行findViewById操作。
同样的,我们在解决MVP冗余代码时,我们也可以使用APT生成代码执行
setVisibility(View.VISIBLE);操作。
思路
1:模仿ButterKnife对于要setVisibility的View我们使用注解来标示
2:当知道有哪些View要setVisibility后,我们可以把它们存到容器里
3:当外部要setVisibility某些View时,我们可以提供一个类似
4:为了避免APT生成的代码和现有的代码重复类名,我们可以尝试在APT的类名中出现$符号,但是这样用户用起来很难受,我们可以是APT生成的代码都实现某个接口,当new出对象后以接口类型返回以保障代码整洁性。
voidsetVisible(View...target)
1
的接口去遍历容器,如果容器中的View在集合target中,就设为可见,否则不可见。
1:如果你最APT还不是很了解,建议阅读下鸿洋的文章鸿洋APT
实现
0x01:
在AndroidStudio里新建一个java工程:
这里写图片描述
在java工程的build.gradle脚本里添加依赖:
applyplugin:''java''
sourceCompatibility=JavaVersion.VERSION_1_7
targetCompatibility=JavaVersion.VERSION_1_7
dependencies{
compilefileTree(dir:''libs'',include:[''.jar''])
compile''com.google.auto.service:auto-service:1.0-rc2''
}
0x02:
然后我们定义注解:
/
Createdbychanon16/10/15.
jiacheng.li@shanbay.com
/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Inherited
@Target(ElementType.FIELD)
public@interfaceJoinView{
}
只能用于field,它用于标示我们要setVisibility的view,像这样:
@JoinView
ViewmViewC;
1
2
0x03:
当注解标示某个field之后,我们就可以拿到field的变量名,我们可以通过activity.mViewC的方式读取里面的值,不过这有个前提——mView最起码应该是protected,或者public的,但是我们还是选用protected,毕竟这样可以最大化数据的封装程度。如果是这样的话我们生成的类必须得和被注解的类在同一包下面当然这很容易实现。
我们自定义Processor:
@AutoService(Processor.class)
publicclassYellowPeachProcessorextendsAbstractProcessor{
/
用于写java文件
/
privateFilermFiler;
/
可以理解为log
/
privateMessagermMessager;
/
注解检查器,用于判断被注解的field不是private的
/
privateAnnotationCheckermAnnotationChecker;
@Override
publicsynchronizedvoidinit(ProcessingEnvironmentprocessingEnv){
super.init(processingEnv);
mFiler=processingEnv.getFiler();
mMessager=processingEnv.getMessager();
mAnnotationChecker=newAnnotationChecker(mMessager);
}
@Override
publicbooleanprocess(Setannotations,RoundEnvironmentroundEnv){
//找到被注解的field
Setset=roundEnv.getElementsAnnotatedWith(JoinView.class);
if(set!=null){
CodeGeneratorcodeGenerator=newCodeGenerator(mFiler,mMessager);
for(Elementelement:set){
//先检查权限
if(!mAnnotationChecker.checkAnnotation(element)){
returnfalse;
}
//把备注解的field添加到生成器里,准备用来生成代码
codeGenerator.add((VariableElement)element);
}
//开始生成代码
codeGenerator.generate();
}
returntrue;
}
@Override
publicSetgetSupportedAnnotationTypes(){
//添加支持的注解类型我们支持JoinView
Setset=newHashSet<>();
set.add(JoinView.class.getCanonicalName());
returnset;
}
@Override
publicSourceVersiongetSupportedSourceVersion(){
returnSourceVersion.RELEASE_7;
}
}
整体代码还是很简单,不过里面有两个类我们依次看下实现方式。
0x04:
检查被注解的field的访问权限
/
Createdbychanon16/10/15.
jiacheng.li@shanbay.com
/
publicclassAnnotationChecker{
privateMessagermMessager;
publicAnnotationChecker(Messagermessager){
mMessager=messager;
}
publicbooleancheckAnnotation(Elementelement){
VariableElementvariableElement=(VariableElement)element;
if(variableElement.getModifiers().contains(Modifier.PRIVATE)){
mMessager.printMessage(Diagnostic.Kind.ERROR,"JoinView不能用于privatefield:"
+variableElement.getEnclosingElement()+"->"+variableElement.getSimpleName());
returnfalse;
}
returntrue;
}
}
可以看到如果针对privatefield,我们是不能通过类似activity.mViewC的方式访问的,所以这里会报错。
0x05:
生成代码,这里比较复杂,我特意建一个Title进行解释。
生成代码
当我们收集到备注注解的field信息之后,我们就可以生成代码,不过怎么处理这些field是个问题。我们首先想到的就是创建一个Map,key为被注解域的class,而值就是它一系列的被注解的field:
publicclassCodeGenerator{
privateMap>mVariableElementMap=newHashMap<>();
publicvoidadd(VariableElementelement){
ListvariableElements=mVariableElementMap.get(element.getEnclosingElement().toString());
if(variableElements==null){
variableElements=newArrayList<>();
//获得被注解的class的名称作为键
mVariableElementMap.put(element.getEnclosingElement().toString(),variableElements);
}
//当前class下备注解的field
variableElements.add(element);
}
}
这里可能有些人对于
element.getEnclosingElement().toString()
1
感到困惑,举个例子:
packagecom.chan.yellowpeach;
importandroid.support.v7.app.AppCompatActivity;
importandroid.view.View;
publicclassMainActivityextendsAppCompatActivity{
@JoinView
ViewmViewC;
}
这里element.getEnclosingElement().toString()返回的就是com.chan.yellowpeach.MainActivity,这必定是唯一的啊,所以作为key再合适不过了,而element就是对应的ViewmViewC,有了这些生成代码只是分分钟的事。
我们可以尝试看下完整的代码:
packagecom.chan.apt.core;
importjava.io.IOException;
importjava.io.Writer;
importjava.util.ArrayList;
importjava.util.HashMap;
importjava.util.Iterator;
importjava.util.List;
importjava.util.Map;
importjavax.annotation.processing.Filer;
importjavax.annotation.processing.Messager;
importjavax.lang.model.element.Element;
importjavax.lang.model.element.VariableElement;
importjavax.tools.Diagnostic;
importjavax.tools.JavaFileObject;
/
Createdbychanon16/10/15.
jiacheng.li@shanbay.com
/
publicclassCodeGenerator{
privateMap>mVariableElementMap=newHashMap<>();
/
用于写java文件
/
privateFilermFiler;
/
logger
/
privateMessagermMessager;
/
APT生成代码所在的包名
/
privateStringmPackage;
publicCodeGenerator(Filerfiler,Messagermessager){
mFiler=filer;
mMessager=messager;
}
publicvoidadd(VariableElementelement){
ListvariableElements=mVariableElementMap.get(element.getEnclosingElement().toString());
if(variableElements==null){
variableElements=newArrayList<>();
//获得被注解的class的名称作为键
mVariableElementMap.put(element.getEnclosingElement().toString(),variableElements);
}
//当前class下备注解的field
variableElements.add(element);
}
publicvoidgenerate(){
if(mVariableElementMap.isEmpty()){
return;
}
init();
try{
for(Map.Entry>entry:mVariableElementMap.entrySet()){
StringclazzName="YellowPeach$"+entry.getKey().replaceAll("\\.","\\$");
JavaFileObjectjavaFileObject=mFiler.createSourceFile(mPackage+"."+clazzName);
mMessager.printMessage(Diagnostic.Kind.NOTE,"在"+mPackage+"."+clazzName+"生成代码");
Writerwriter=javaFileObject.openWriter();
writer.write(generateSourceCode(entry,mPackage,clazzName));
writer.flush();
writer.close();
}
}catch(IOExceptione){
e.printStackTrace();
}
}
privatevoidinit(){
//先获得包名
Iterator>>iterator=mVariableElementMap.entrySet().iterator();
Map.Entry>elementEntry=iterator.next();
VariableElementvariableElement=elementEntry.getValue().get(0);
Elementelement=variableElement.getEnclosingElement();
while(element!=null&&element.getEnclosingElement()!=null){
mPackage=element.toString();
element=element.getEnclosingElement();
}
mPackage=mPackage.substring(0,mPackage.lastIndexOf("."));
}
privatestaticStringgenerateSourceCode(Map.Entry>entry,StringpackageName,StringclazzName){
//包
StringBuilderstringBuilder=newStringBuilder("package");
stringBuilder.append(packageName);
stringBuilder.append(";\n");
//import
stringBuilder.append("importandroid.view.View;\n"+
"\n"+
"importcom.chan.lib.Peach;\n"+
"\n"+
"importjava.util.ArrayList;\n"+
"importjava.util.List;");
stringBuilder.append("publicclass");
stringBuilder.append(clazzName);
stringBuilder.append("implementsPeach{\n");
//成员变量
stringBuilder.append("privateListmViews=newArrayList<>();\n");
//构造函数
stringBuilder.append("public");
stringBuilder.append(clazzName);
stringBuilder.append("(");
stringBuilder.append(entry.getKey());
stringBuilder.append("o){");
for(VariableElementitem:entry.getValue()){
stringBuilder.append("mViews.add(");
stringBuilder.append("o.");
stringBuilder.append(item.getSimpleName());
stringBuilder.append(");");
}
stringBuilder.append("}");
//override的内容
stringBuilder.append("@Override\n"+
"publicvoidsetVisible(View...target){\n"+
"\n"+
"for(Viewv:mViews){\n"+
"v.setVisibility(View.GONE);\n"+
"}\n"+
"\n"+
"for(inti=0;i "finalintindex=mViews.indexOf(target[i]);\n"+
"if(index!=-1){\n"+
"mViews.get(index).setVisibility(View.VISIBLE);\n"+
"}\n"+
"}\n"+
"}");
//结尾
stringBuilder.append("}");
returnstringBuilder.toString();
}
}
从之前的例子可以看到在add(xxx)之后就是收集完所有的信息,我们所要做的就是调用codeGenerator.generate()生成代码
在codeGenerator.generate()函数里,我们首先调用init来获取包名:
privatevoidinit(){
//先获得包名
Iterator>>iterator=mVariableElementMap.entrySet().iterator();
Map.Entry>elementEntry=iterator.next();
VariableElementvariableElement=elementEntry.getValue().get(0);
Elementelement=variableElement.getEnclosingElement();
while(element!=null&&element.getEnclosingElement()!=null){
mPackage=element.www.baiyuewang.nettoString();
element=element.getEnclosingElement();
}
mPackage=mPackage.substring(0,mPackage.lastIndexOf("."));
}
读者可以通过打mMessager打log查看执行的过程,本身也比较简单,讲解却十分烦,光是例子就不少代码。
在获得包名之后就是生成响应的java代码:
for(Map.Entry>entry:mVariableElementMap.entrySet()){
//把.都换成$
StringclazzName="YellowPeach$"+entry.getKey().replaceAll("\\.","\\$");
//指定java文件写入的位置
JavaFileObjectjavaFileObject=mFiler.createSourceFile(mPackage+"."+clazzName);
mMessager.printMessage(Diagnostic.Kind.NOTE,"在"+mPackage+"."+clazzName+"生成代码");
//开始写文件
Writerwriter=javaFileObject.openWriter();
writer.write(generateSourceCode(entry,mPackage,clazzName));
writer.flush();
writer.close();
}
写文件再上文已经给出,其中没有多少技术难度,只有有一点核心代码需要解释:
//构造函数参数为被注解的class
stringBuilder.append("public");
stringBuilder.append(clazzName);
stringBuilder.append("(");
stringBuilder.append(entry.getKey());
stringBuilder.append("o){");
for(VariableElementitem:entry.getValue()){
stringBuilder.append("mViews.add(");
stringBuilder.append("o.");
//返回field的名字
stringBuilder.append(item.getSimpleName());
stringBuilder.append(");");
}
我们不妨看下APT生成的代码。如果你一切顺利地话,会在这个目录下看到apt代码:
这里写图片描述
packagecom.chan.yellowpeach;
importandroid.view.View;
importcom.chan.lib.Peach;
importjava.util.ArrayList;
importjava.util.List;
publicclassYellowPeach$com$chan$yellowpeach$Main2ActivityimplementsPeach{
privateListmViews=newArrayList<>();
publicYellowPeach$com$chan$yellowpeach$Main2Activity(
com.chan.yellowpeach.Main2Activityo){
mViews.add(o.mView);
}
@Override
publicvoidsetVisible(View...target){
for(Viewv:mViews){
v.setVisibility(View.GONE);
}
for(inti=0;i finalintindex=mViews.indexOf(target[i]);
if(index!=-1){
mViews.get(index).setVisibility(View.VISIBLE);
}
}
}
}
还是很简单的,那么下面的问题就只剩下如何new一个apt生成的class的对象
new一个对象
packagecom.chan.lib;
importjava.lang.reflect.Constructor;
importjava.lang.reflect.InvocationTargetException;
/
Createdbychanon16/10/15.
jiacheng.li@shanbay.com
/
publicclassYellowPeach{
publicstaticPeachbind(Objecto){
try{
finalStringclazzName=o.getClass().getPackage().getName().toString()+
".YellowPeach$"+o.getClass().getCanonicalName().replaceAll("\\.","\\$");
Class>clazz=o.getClass().getClassLoader().loadClass(clazzName);
Constructor>constructors[]=clazz.getConstructors();
return(Peach)constructors[0].newInstance(o);
}catch(ClassNotFoundExceptione){
e.printStackTrace();
}catch(IllegalAccessExceptione){
e.printwww.wang027.comStackTrace();
}catch(InstantiationExceptione){
e.printStackTrace();
}catch(InvocationTargetExceptione){
e.printStackTrace();
}
returnnull;
}
}
我们使用反射的方式获得APT生成的类,之后直接new出来然后作为Peach接口类型返回。我们看下客户端是如何使用的
使用
packagecom.chan.yellowpeach;
importandroid.content.Intent;
importandroid.os.Bundle;
importandroid.support.v7.app.AppCompatActivity;
importandroid.view.View;
importcom.chan.apt.annotations.JoinView;
importcom.chan.lib.Peach;
importcom.chan.lib.YellowPeach;
publicclassMainActivityextendsAppCompatActivity{
@JoinView
ViewmViewC;
privatePeachmPeach;
@Override
protectedvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewC=findViewById(R.id.viewC);
mPeach=YellowPeach.bind(this);
findViewById(R.id.button).setOnClickListener(newView.OnClickListener(){
@Override
publicvoidonClick(Viewv){
ViewHolderholder=newViewHolder(findViewById(R.id.viewA));
ViewHolder.VieHolderAviewHolder=holder.newViewHolderA(findViewById(R.id.viewB));
viewHolder.foo();
holder.foo();
mPeach.setVisible(mViewC);
}
});
findViewById(R.id.button2).setOnClickListener(newView.OnClickListener(){
@Override
publicvoidonClick(Viewv){
Intentintent=newIntent(MainActivity.this,Main2Activity.class);
startActivity(intent);
}
});
}
publicclassViewHolder{
@JoinView
ViewmView;
privatePeachmPeach;
publicViewHolder(Viewview){
mView=view;
mPeach=YellowPeach.bind(this);
}
publicvoidfoo(){
mPeach.setVisible(mView);
}
publicclassViewHolderA{
@JoinView
ViewmView;
privatePeachmPeach;
publicViewHolderA(Viewview){
mView=view;
mPeach=YellowPeach.bind(this);
}
publicvoidfoo(){
mPeach.setVisible(mView);
}
}
}
}
|
|