使用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.
* [email protected]
*/
@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.
* [email protected]
*/
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.
* [email protected]
*/
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.
* [email protected]
*/
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);

时间: 2024-12-27 09:10:16

使用APT减少MVP的冗余代码的相关文章

Teleport Ultra/Teleport Pro的冗余代码批量清理方法

Teleport Pro 是款优秀的网站离线浏览工具(即网站整站下载工具),Teleport Ultra是其增强版,但使用此系列软件下载的离线网页里会包含大量冗余代码(如tppabs),手动去修改工作量很大,下面介绍如何通过软件进行正则表达式批量替换冗余代码(推荐DreamWeaver的正则替换功能). 清除tppabs标签: html文件中: 查找:\btppabs="h[^"]*" 替换:(空) css文件中的图片链接(以gif图片为例): 查找:tpa=http://[

批量去除Teleport Pro整站下载文件冗余代码

teleport pro tppabs标签批量删除 teleport pro tppabs标签批量删除 使 用Teleport Pro下载的网页代码中包含了很多垃圾代码,比如下载的html网页代码中会出现tppabs标签,而且还会将所有的href标签中加入了很多垃圾代码, 在css会加入了tpa标签,这些都是冗余代码,可以将其全部删除,但是由于代码太多,我们不可能一个个删除,因此可以使用Dreamweaver的查找 /替换工具中的正则表达式来进行替换.1.替换tppabs标签,使用Dreamwe

word表格转html后去除冗余代码

word可以另存为html文件,通过这个功能,可以快速实现网页展示word内容,特别是表格的编辑,它包含tr.td.th.rowspan.colspan等内容,直接写比较繁琐. 但word转换过来的html默认是带有很多格式代码,那么如何去除这些冗余代码,只保留主内容呢? 本来是打算从网上找工具的,但发现没有现成的,一般都是推荐用工具的文本替换来去除,这样不能复用.因此,本人采用nodejs写了一小段代码,来去除冗余代码. 主要思路是: nodejs读取html文件的文本内容 用substrin

冗余代码都走开——前端模块打包利器 Rollup.js 入门

之前翻译过一篇文章,介绍了通过 ES2015 的解构赋值语法引入模块,可以让打包工具(browserify)最终编译出来的代码量最小化. 殊不知在 webpack 1.X 版本是无法利用该特性来避免引入冗余模块代码的,导致打出来的 bundle 文件大小难免略有臃肿. 今天则向大家介绍一个当红炸子鸡——Rollup.js,通过它可以让你的 bundle 最小化,有效减少文件请求大小——以至于连 vue 都迅速地转投它来打包模块. Tree-shaking 在 Rollup 编译模块的过程中,通过

【手撕设计模式】减少业务中冗余的请求——备忘录模式

备忘录模式-- 在不破坏对象封装性的前提下,在对象之外捕获并保存对象内部的状态以便日后对象使用或者对象恢复到以前的某个状态: 相信大家在工作中一定会遇到这样的情况,在不同的页面或逻辑中多次发送相同的请求.举个常见的情景,在实现页面翻页的逻辑中,我们经常在点击下一页或者上一页的时候向后台发送相同的请求(请求的页码不同): //这里我使用的是vue框架,第一个参数当前页码,第二个参数是一页展示多少个数据 fetch: function(curPage,pageNum) { var self = th

批量去掉tppabs冗余代码方法

今天接了一个这样的代码修改活,网页中的图片有的浏览器不显示,我看了下每个本地图片路径后面都加着tppabs= 这个代码有的浏览器认有的不认 因为是下载别人的网页才会出现这样 大部分浏览器会忽略 但也有的浏览器会认 导致不兼容 专业点的设计网站肯定要去掉这些代码的 下面告诉大家去除方法 工具:Dreamweaver. 查找 tppabs="h[^"]*" 替换为空 这样就批量替换完了

DW快速去除tppabs冗余代码

对于刚刚开始学习网站建设的朋友来说,网页的设计与布局也是一个困难的过程.那么大家可能都有这样的想法:我看到一个不错的网站,我可不可以用它的网页呢?答案当然是可以的.但是这种方法仅供学习,切勿用于商业用途(侵犯原作者的劳动成果,你懂的).这里涉及到两个软件,一个是teleport pro.而另一个当然就是DW(全称:dreamweaver)了.关于teleport pro,这是一个非常好用的整站下载软件,我一直都在用这个!使用过的朋友都知道下载完后所有的超链都被强行加了一句tppabs="...&

[转]最全的用正则批量去除Teleport Pro整站下载文件冗余代码

原文地址:http://www.jb51.net/article/43650.htm html原文件中tppabs标记是Teleport Pro软件留下的标记.该软件是离线浏览器,下载完整个网页后,它会在图片或超级链接标签内插入tppabs标签,以记录该图片或超级链接指向的原始地址.因为这个标签不是合法标签,所以普通浏览器会忽略它.但可以通过element.getAttribute("tppabs")在JS中读取这个属性. 如我们手动清除的话,那将是一个无法估计的工作量,需要批量清除.

消除Java 的冗余代码

大家在使用Eclipse集成的环境时候,肯定会出现类似的问题:我们有些实际项目中的代码例如:setter ,getter等太多,从而使项目看上去代码很臃肿: 我们来讲讲如何安装此插件,首先在网上下载一个名为:lombok.jar:放到Eclipse开发环境的根目录下如下图: 不是放在这里就OK了,需要进行配置,打开此下面的eclipse.ini文件,在最后的位置添加如下代码:-javaagent:lombok.jar   :保存后启动Eclipse就会有此功能了:自己动手试试吧.... 版权声明