使用APT减少MVP的冗余代码
前言
不知道从何时起,移动端开发都开始采用MVP。我们在认识到MVP有点的时候,也不妨会察觉到它其实也有很多恼人的地方,比如,我们针对每种状态渲染不同的视图:
private void renderInit() {
mViewA.setVisibility(View.VISIBLE);
mViewB.setVisibility(View.GONE);
mViewC.setVisibility(View.GONE);
mViewD.setVisibility(View.GONE);
mViewE.setVisibility(View.GONE);
}
private void renderSummary() {
mViewA.setVisibility(View.GONE);
mViewB.setVisibility(View.VISIBLE);
mViewC.setVisibility(View.GONE);
mViewD.setVisibility(View.GONE);
mViewE.setVisibility(View.GONE);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可以看到在这里,我们渲染Init状态时,把View A设为可见,把其他的View设为不可见,当我们又去渲染Summary状态是,又重复上面的动作,不过这次是吧View B设为可见。这种冗余代码(或者说是模板代码)非常的烦人,因为我们在复制粘贴的时候极有可能设置错误的View为可见了。那么我们有没有什么办法来避免这样的问题呢。其实是有的,我们不妨回忆下ButterKnife怎么做的——对于findViewById这样的冗余代码,ButterKnife是采用注解的方式解决的:
@Bind(R.id.id_name)
TextView m_name;
@Bind(R.id.id_who)
TextView m_who;
@Bind(R.id.id_musicBar)
MusicBar m_musicBar;
@Bind(R.id.id_playControl)
ImageView m_bottomPlayControlView;
@Override
protected IView createView() {
return this;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife.bind(this);
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在执行
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出对象后以接口类型返回以保障代码整洁性。
void setVisible(View... target)
1
的接口去遍历容器,如果容器中的View在集合target中,就设为可见,否则不可见。
1:如果你最APT还不是很了解,建议阅读下鸿洋的文章鸿洋APT
实现
0x01:
在Android Studio里新建一个java工程:
这里写图片描述
在java工程的build.gradle脚本里添加依赖:
apply plugin: 'java'
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.auto.service:auto-service:1.0-rc2'
}
1
2
3
4
5
6
7
8
9
0x02:
然后我们定义注解:
/**
* Created by chan on 16/10/15.
* jiacheng.li@shanbay.com
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Inherited
@Target(ElementType.FIELD)
public @interface JoinView {
}
1
2
3
4
5
6
7
8
9
10
只能用于field,它用于标示我们要setVisibility的view,像这样:
@JoinView
View mViewC;
1
2
0x03:
当注解标示某个field之后,我们就可以拿到field的变量名,我们可以通过activity.mViewC的方式读取里面的值,不过这有个前提——mView最起码应该是protected, 或者public的,但是我们还是选用protected,毕竟这样可以最大化数据的封装程度。如果是这样的话我们生成的类必须得和被注解的类在同一包下面当然这很容易实现。
我们自定义Processor:
@AutoService(Processor.class)
public class YellowPeachProcessor extends AbstractProcessor {
/**
* 用于写java文件
*/
private Filer mFiler;
/**
* 可以理解为log
*/
private Messager mMessager;
/**
* 注解检查器,用于判断被注解的field不是private的
*/
private AnnotationChecker mAnnotationChecker;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
mAnnotationChecker = new AnnotationChecker(mMessager);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//找到被注解的field
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(JoinView.class);
if (set != null) {
CodeGenerator codeGenerator = new CodeGenerator(mFiler, mMessager);
for (Element element : set) {
//先检查权限
if (!mAnnotationChecker.checkAnnotation(element)) {
return false;
}
//把备注解的field添加到生成器里,准备用来生成代码
codeGenerator.add((VariableElement) element);
}
//开始生成代码
codeGenerator.generate();
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
//添加支持的注解类型 我们支持JoinView
Set<String> set = new HashSet<>();
set.add(JoinView.class.getCanonicalName());
return set;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
整体代码还是很简单,不过里面有两个类我们依次看下实现方式。
0x04:
检查被注解的field的访问权限
/**
* Created by chan on 16/10/15.
* jiacheng.li@shanbay.com
*/
public class AnnotationChecker {
private Messager mMessager;
public AnnotationChecker(Messager messager) {
mMessager = messager;
}
public boolean checkAnnotation(Element element) {
VariableElement variableElement = (VariableElement) element;
if (variableElement.getModifiers().contains(Modifier.PRIVATE)) {
mMessager.printMessage(Diagnostic.Kind.ERROR, "JoinView不能用于private field: "
+ variableElement.getEnclosingElement() + " -> " + variableElement.getSimpleName());
return false;
}
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
可以看到如果针对private field,我们是不能通过类似activity.mViewC的方式访问的,所以这里会报错。
0x05:
生成代码,这里比较复杂,我特意建一个Title进行解释。
生成代码
当我们收集到备注注解的field信息之后,我们就可以生成代码,不过怎么处理这些field是个问题。我们首先想到的就是创建一个Map, key为被注解域的class,而值就是它一系列的被注解的field:
public class CodeGenerator {
private Map<String, List<VariableElement>> mVariableElementMap = new HashMap<>();
public void add(VariableElement element) {
List<VariableElement> variableElements = mVariableElementMap.get(element.getEnclosingElement().toString());
if (variableElements == null) {
variableElements = new ArrayList<>();
//获得被注解的class的名称作为键
mVariableElementMap.put(element.getEnclosingElement().toString(), variableElements);
}
//当前class下备注解的field
variableElements.add(element);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这里可能有些人对于
element.getEnclosingElement().toString()
1
感到困惑,举个例子:
package com.chan.yellowpeach;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
public class MainActivity extends AppCompatActivity {
@JoinView
View mViewC;
}
1
2
3
4
5
6
7
8
9
10
11
这里element.getEnclosingElement().toString()返回的就是com.chan.yellowpeach.MainActivity,这必定是唯一的啊,所以作为key再合适不过了,而element就是对应的View mViewC,有了这些生成代码只是分分钟的事。
我们可以尝试看下完整的代码:
package com.chan.apt.core;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Element;
import javax.lang.model.element.VariableElement;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
/**
* Created by chan on 16/10/15.
* jiacheng.li@shanbay.com
*/
public class CodeGenerator {
private Map<String, List<VariableElement>> mVariableElementMap = new HashMap<>();
/**
* 用于写java文件
*/
private Filer mFiler;
/**
* logger
*/
private Messager mMessager;
/**
* APT生成代码所在的包名
*/
private String mPackage;
public CodeGenerator(Filer filer, Messager messager) {
mFiler = filer;
mMessager = messager;
}
public void add(VariableElement element) {
List<VariableElement> variableElements = mVariableElementMap.get(element.getEnclosingElement().toString());
if (variableElements == null) {
variableElements = new ArrayList<>();
//获得被注解的class的名称作为键
mVariableElementMap.put(element.getEnclosingElement().toString(), variableElements);
}
//当前class下备注解的field
variableElements.add(element);
}
public void generate() {
if (mVariableElementMap.isEmpty()) {
return;
}
init();
try {
for (Map.Entry<String, List<VariableElement>> entry : mVariableElementMap.entrySet()) {
String clazzName = "YellowPeach$" + entry.getKey().replaceAll("\\.", "\\$");
JavaFileObject javaFileObject = mFiler.createSourceFile(mPackage + "." + clazzName);
mMessager.printMessage(Diagnostic.Kind.NOTE, "在" + mPackage + "." + clazzName + "生成代码");
Writer writer = javaFileObject.openWriter();
writer.write(generateSourceCode(entry, mPackage, clazzName));
writer.flush();
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void init() {
//先获得包名
Iterator<Map.Entry<String, List<VariableElement>>> iterator = mVariableElementMap.entrySet().iterator();
Map.Entry<String, List<VariableElement>> elementEntry = iterator.next();
VariableElement variableElement = elementEntry.getValue().get(0);
Element element = variableElement.getEnclosingElement();
while (element != null && element.getEnclosingElement() != null) {
mPackage = element.toString();
element = element.getEnclosingElement();
}
mPackage = mPackage.substring(0, mPackage.lastIndexOf("."));
}
private static String generateSourceCode(Map.Entry<String, List<VariableElement>> entry, String packageName, String clazzName) {
//包
StringBuilder stringBuilder = new StringBuilder("package ");
stringBuilder.append(packageName);
stringBuilder.append(";\n");
//import
stringBuilder.append("import android.view.View;\n" +
"\n" +
"import com.chan.lib.Peach;\n" +
"\n" +
"import java.util.ArrayList;\n" +
"import java.util.List;");
stringBuilder.append("public class ");
stringBuilder.append(clazzName);
stringBuilder.append(" implements Peach {\n");
//成员变量
stringBuilder.append("private List<View> mViews = new ArrayList<>();\n");
//构造函数
stringBuilder.append("public ");
stringBuilder.append(clazzName);
stringBuilder.append("(");
stringBuilder.append(entry.getKey());
stringBuilder.append(" o){");
for (VariableElement item : entry.getValue()) {
stringBuilder.append("mViews.add(");
stringBuilder.append("o.");
stringBuilder.append(item.getSimpleName());
stringBuilder.append(");");
}
stringBuilder.append("}");
//override的内容
stringBuilder.append(" @Override\n" +
" public void setVisible(View... target) {\n" +
"\n" +
" for (View v : mViews) {\n" +
" v.setVisibility(View.GONE);\n" +
" }\n" +
"\n" +
" for (int i = 0; i < target.length; ++i) {\n" +
" final int index = mViews.indexOf(target[i]);\n" +
" if (index != -1) {\n" +
" mViews.get(index).setVisibility(View.VISIBLE);\n" +
" }\n" +
" }\n" +
" }");
//结尾
stringBuilder.append("}");
return stringBuilder.toString();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
从之前的例子可以看到在add(xxx)之后就是收集完所有的信息,我们所要做的就是调用codeGenerator.generate()生成代码
在codeGenerator.generate()函数里,我们首先调用init来获取包名:
private void init() {
//先获得包名
Iterator<Map.Entry<String, List<VariableElement>>> iterator = mVariableElementMap.entrySet().iterator();
Map.Entry<String, List<VariableElement>> elementEntry = iterator.next();
VariableElement variableElement = elementEntry.getValue().get(0);
Element element = variableElement.getEnclosingElement();
while (element != null && element.getEnclosingElement() != null) {
mPackage = element.toString();
element = element.getEnclosingElement();
}
mPackage = mPackage.substring(0, mPackage.lastIndexOf("."));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
读者可以通过打mMessager打log查看执行的过程,本身也比较简单,讲解却十分烦,光是例子就不少代码。
在获得包名之后就是生成响应的java代码:
for (Map.Entry<String, List<VariableElement>> entry : mVariableElementMap.entrySet()) {
//把.都换成$
String clazzName = "YellowPeach$" + entry.getKey().replaceAll("\\.", "\\$");
//指定java文件写入的位置
JavaFileObject javaFileObject = mFiler.createSourceFile(mPackage + "." + clazzName);
mMessager.printMessage(Diagnostic.Kind.NOTE, "在" + mPackage + "." + clazzName + "生成代码");
//开始写文件
Writer writer = javaFileObject.openWriter();
writer.write(generateSourceCode(entry, mPackage, clazzName));
writer.flush();
writer.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
写文件再上文已经给出,其中没有多少技术难度,只有有一点核心代码需要解释:
//构造函数 参数为被注解的class
stringBuilder.append("public ");
stringBuilder.append(clazzName);
stringBuilder.append("(");
stringBuilder.append(entry.getKey());
stringBuilder.append(" o){");
for (VariableElement item : entry.getValue()) {
stringBuilder.append("mViews.add(");
stringBuilder.append("o.");
//返回field的名字
stringBuilder.append(item.getSimpleName());
stringBuilder.append(");");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
我们不妨看下APT生成的代码。如果你一切顺利地话,会在这个目录下看到apt代码:
这里写图片描述
package com.chan.yellowpeach;
import android.view.View;
import com.chan.lib.Peach;
import java.util.ArrayList;
import java.util.List;
public class YellowPeach$com$chan$yellowpeach$Main2Activity implements Peach {
private List<View> mViews = new ArrayList<>();
public YellowPeach$com$chan$yellowpeach$Main2Activity(
com.chan.yellowpeach.Main2Activity o) {
mViews.add(o.mView);
}
@Override
public void setVisible(View... target) {
for (View v : mViews) {
v.setVisibility(View.GONE);
}
for (int i = 0; i < target.length; ++i) {
final int index = mViews.indexOf(target[i]);
if (index != -1) {
mViews.get(index).setVisibility(View.VISIBLE);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
还是很简单的,那么下面的问题就只剩下如何new一个apt生成的class的对象
new 一个对象
package com.chan.lib;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Created by chan on 16/10/15.
* jiacheng.li@shanbay.com
*/
public class YellowPeach {
public static Peach bind(Object o) {
try {
final String clazzName = 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 (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
我们使用反射的方式获得APT生成的类,之后直接new出来然后作为Peach接口类型返回。我们看下客户端是如何使用的
使用
package com.chan.yellowpeach;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import com.chan.apt.annotations.www.yinbaovip.cn/ JoinView;
import com.chan.lib.Peach;
import com.chan.lib.YellowPeach;
public class MainActivity extends AppCompatActivity {
@JoinView
View mViewC;
private Peach mPeach;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewC = findViewById(R.id.viewC);
mPeach = YellowPeach.bind(this);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewHolder holder = new ViewHolder(findViewById(R.id.viewA));
ViewHolder.ViewHolderA viewHolder =www.zhenloyl88.cn holder.new ViewHolderA(findViewById(R.id.viewB));
viewHolder.foo();
holder.foo();
mPeach.setVisible(mViewC);
}
});
findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, Main2Activity.class);
startActivity(intent);
}
});
}
public class ViewHolder {
@JoinView
View mView;
private Peach mPeach;
public ViewHolder(Viewwww.huacairen88.cn view) {
mView = view;
mPeach = YellowPeach.bind(this);
}
public void foo() {
mPeach.setVisible(mView);
}
public class ViewHolderA {
@JoinView
View mView;
private Peach mPeach;
public ViewHolderA(View view) {
mView = view;
mPeach = YellowPeach.www.yghrcp88.cn bind(this);
}
public void foo() {
mPeach.setVisible(mView);
使用APT减少MVP的冗余代码的更多相关文章
- 批量去除Teleport Pro整站下载文件冗余代码
teleport pro tppabs标签批量删除 teleport pro tppabs标签批量删除 使 用Teleport Pro下载的网页代码中包含了很多垃圾代码,比如下载的html网页代码中会 ...
- 去掉tppabs冗余代码,怎样批量去掉tppabs代码
去掉tppabs冗余代码,怎样批量去掉tppabs代码 刚用teleport pro拉了一个整站到本地 所有的超链都被强行加了一句tppabs=" 就玩了一把dw的替换功能 查找范围:整 ...
- Teleport Ultra/Teleport Pro的冗余代码批量清理方法
Teleport Pro 是款优秀的网站离线浏览工具(即网站整站下载工具),Teleport Ultra是其增强版,但使用此系列软件下载的离线网页里会包含大量冗余代码(如tppabs),手动去修改工作 ...
- word表格转html后去除冗余代码
word可以另存为html文件,通过这个功能,可以快速实现网页展示word内容,特别是表格的编辑,它包含tr.td.th.rowspan.colspan等内容,直接写比较繁琐. 但word转换过来的h ...
- [转]Teleport Ultra/Teleport Pro的冗余代码批量清理方法
原文地址:http://www.abcd9.com/?post=402 Teleport Pro 是款优秀的网站离线浏览工具(即网站整站下载工具),Teleport Ultra是其增强版,但使用此系列 ...
- 去除整站下载文件中的tppabs等冗余代码
用TeleprotUltra复制了一个网站,结果网页中出现了很多形如tppabs=””的冗余代码,点击vs中的“在文件中查找”图标,打开“查找和替换”对话框,转到“快速替换”,然后进行以下设置: “查 ...
- 组件化框架设计之apt编译时期自动生成代码&动态类加载(二)
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680 本篇文章将继续从以下两个内容来介绍组件化框架设计: apt编译时 ...
- 冗余代码都走开——前端模块打包利器 Rollup.js 入门
之前翻译过一篇文章,介绍了通过 ES2015 的解构赋值语法引入模块,可以让打包工具(browserify)最终编译出来的代码量最小化. 殊不知在 webpack 1.X 版本是无法利用该特性来避免引 ...
- 使用gulp-uncss精简css,去除冗余代码
写html页面的时候,多修改几次就会出现很多无用的css代码,下面使用gulp-uncss来精简css文件,去掉没用的css代码 1.首先找个目录创建一个gulp项目在命令行输入:npm init ...
随机推荐
- 17 Great Machine Learning Libraries
17 Great Machine Learning Libraries 08 October 2013 After wonderful feedback on my previous post on ...
- 一个NB的安全认证机制
这是一个NB的安全认证机制. 1.这是一个安全认证机制 2.可以防止黑客截获到客户端发送的请求消息,避免了黑客冒充客户端向服务器发送操作的请求. 原理与步骤: 1.客户端与服务器端都会放着一份验证用的 ...
- ActiveMQ使用总结
一.下载使用: 官网下载apache-activemq-5.8.0-bin.tar.gz.apache-activemq-5.8.0-bin.zip 解压,然后启动ActiveMQ服务器 方法1: 直 ...
- 当可以设置src时,不必发ajax请求,如果没有参数设置src即可
var params = (function(obj){ var string = []; for(var key in obj){ string.push(window.encodeURI(key) ...
- mvn开发可执行的java程序
1. 用maven-assembly-plugin插件 2. 在项目的pom文件中加入以下该插件的配置 <span style="font-size:18px;">& ...
- HTML里面Textarea换行总结
近期碰到一个数据转来转去转到Textrea里面能否真正按行存放的问题,在这里总结一下: 问题描写叙述: 比方get数据到一个TextArea里面,如“AAA BBB”,想把这段文字在TextAre ...
- GridView控件
GridView是ASP.NET 1.x的DataGrid控件的后继者.它提供了同样的基本功能集,同一时候添加�了大量扩展和改进.如前所述,DataGrid(ASP.NET 2.0仍然全然支持)是一个 ...
- 设计模式 - 观察者模式(Observer Pattern) 详细说明
观察者模式(Observer Pattern) 详细说明 本文地址: http://blog.csdn.net/caroline_wendy/article/details/26583157 版权全部 ...
- Load Balance Tomcat with Nginx and Store Sessions in Redis--reference
An awkward title, but that’s exactly what we’re going to do. For some time, I was looking for a way ...
- MVVM架构的一次实践,重写iOS头条客户端
前言: 一个iOS头条APP,使用MVVM架构实现,代码中有注释,封装了AFN网络请求,解媾代码,使用起来非常方便.用最经典的TableView展示,后续不断更新,喜欢就star或fork一下,有问题 ...