项目的升级-给RemoveButterKnife插件增加新功能
前言
经过项目的初步编写和进一步改造,RemoveButterKnife插件终于也有模有样了,但是,功能上仅仅支持Activity/Fragment的BindView注解。
关于编写和优化的过程可以看下面两篇文章
项目构造RemoveButterKnife
当然,这里也附上这个项目的github地址
为了让插件支持更加彻底,我们还要支持组合自定义view以及viewholder中使用butterknife的情况,当然,我们也要支持OnClick注解以及一些其他的使用场景。
要增加哪些功能?
首先要确定需要增加哪些功能点,功能点的更新如下
- 增加对多moudle时的R2.id.xxx的支持
- 增加对OnClick注解的支持
- 增加对viewholder和自定义组合view的支持
确定增加功能的先后顺序
审阅我们的功能,发现1号功能是比较容易的,所以我们把1号定为优先
再次思考发现,2,3号功能之间存在关联性,即,3号功能提及的支持种类中也要支持Onclick注解的形式。
所以确定开发顺序为1->3->2功能拆分
在这里我们使用github提供的project功能,具体分解如下
可以看到,我们分为了todo,doing,done三个部分,而且把每个任务都细分为了几个步骤,这样我们就可以在开发某一功能时保持专注,而不需要东写一点西写一点了具体功能开发
1.对R2.id.xxxx的支持
由于我们原来的代码中寻找匹配使用的是正则表达式,如下
可以看到,我们的代码已经添加了对R2.id.xxx的支持,只需要给正则表达式增加一个条件。String pattern = "^@(BindView|InjectView|Bind)\\((R.id.*)|(R2.id.*)\\)$"; Pattern r = Pattern.compile(pattern);
最后,我们给这个功能添加上unit test,就可以完成对功能1的开发2.对自定义view和viewholder的支持
首先,我们要判别一个类到底是自定义view还是viewholder,对于activity和fragment很简单,因为初始函数是不同的,一个是oncreate,一个是oncreateview,但是由于增加了支持种类,老办法就行不通了,这时我们需要使用idea的sdk来进行判断,代码如下
对于原来的代码,我们已经能够找到activity/fragment的特定位置插入代码,但是对于自定义view和viewholder,又该用什么特征来定位该在哪里插入呢?GenCodeContext codeContext = new GenCodeContext(mClass, mFactory); String type = mClass.getSuperClassType().toString(); if (type.contains("Activity")){ codeContext.setStrategy(new ActivityStrategy(code,clickMap)); }else if (type.contains("Fragment")) { codeContext.setStrategy(new FragmentStrategy(code,clickMap)); }else if (type.contains("ViewHolder")||type.contains("Adapter<ViewHolder>")) { codeContext.setStrategy(new AdapterStrategy(code,clickMap)); }else { codeContext.setStrategy(new CustomViewStrategy(code,clickMap)); } codeContext.executeStrategy();
对于这个问题我们分情况讨论 - 自定义view
我们这里讨论的自定义view仅仅针对组合view,自绘和扩展方式不做讨论,因为这两种方式一般不会使用ButterKnife.
组合自定义view的特征
对于这种自定view,最大的特征就是在构造的时候会使用inflate方法将xml文件进行压入,那么,找到inflate或者R.layout.xxx的语句,这里就是我们插入生成后代码的位置
代码private PsiStatement findInflateStatement(PsiClass mClass){ PsiStatement result = null; PsiMethod[] methods = mClass.getAllMethods(); for (PsiMethod method:methods) { for (PsiStatement statement : method.getBody().getStatements()) { String returnValue = statement.getText(); if (returnValue.contains("R.layout") || returnValue.contains("LayoutInflater.from(context).inflate")) { result = statement; break; } } } return result; }
- viewholder
这里说的viewholder特指recyclerview.viewholder.
这种viewholder都有一个构造函数,参数为(View xxx)第一句是super(xxx);
我们可以基于这两个特征进行定位。
那么,既然能够识别和找到哪里插入代码了,我们的类型支持也就水到渠成了。private PsiStatement findSuperStatement(PsiMethod method,String viewName){ PsiStatement result = null; for (PsiStatement statement : method.getBody().getStatements()) { String returnValue = statement.getText(); if (returnValue.contains("super(" + viewName + ")")) { result = statement; break; } } return result; }
在类型支持的时候,我们使用了策略模式,这样根据类型不同,设置不同的策略就可以方便的进行处理。
目录结构如下3.对onclick注解的支持
我们对onclick的处理分以下几步 - 寻找注解
- 分析注解信息并保持
- 根据保存信息生成代码并插入
1.寻找注解
使用正则表达式很容易找到,这里不再重复贴代码2.分析注解信息并保存
onclick注解有几种情况 - 单id/多id的绑定
- 点击函数是否有参数的情况
我们要获取的信息有以下几个 - 绑定的id列表
- 点击函数的名称,是否有参数,参数的类型
针对第二点,我们使用一个对象将其封装起来
保存获取信息我们使用一个Map<>来进行
代码:@Override public void process() { String pattern = "^@OnClick\\(\\{*(R.id.*,|R.id.*|R2.id.*|R2.id.*,)+\\}*\\)$"; Pattern r = Pattern.compile(pattern); for (int i = 0;i < currentDoc.length;i++){ Matcher m = r.matcher(currentDoc[i].trim()); currentDoc[i] = currentDoc[i].trim(); if (m.find()) { method = detectMethod(currentDoc[i+1]); ids = detectID(currentDoc[i], method); methodAndIDMap.put(method,ids); deleteLineNumbers.add(i); } } }
3.根据保存信息生成代码并插入
这步我们需要根据保存的信息进行代码生成和插入,我们主要讨论生成,插入部分和findviewbyid代码大同小异
我们已经知道了注解的id和点击对应的方法,那么我们复原的结果就应该是
findViewById(R.id.xxx).setOnclickListener(new OnclickListener(….
我们需要注意的地方就是点击函数是否有参数,这会影响到我们生成的代码
看具体代码:
到了这里,我们的Onclick注解支持也完成了。protected StringBuilder getMethodInvokeString(ClickMehtod method) { StringBuilder methodString = new StringBuilder(); if (method.isHaveArg()){ methodString.append(method.getName()+"(("+method.getArgType()+")"+"v);"); }else{ methodString.append(method.getName()+"();"); } return methodString; } protected String getOnClickCode(StringBuilder methodString, String id) { return "findViewById("+id+").setOnClickListener(new View.OnClickListener() {\n" + " @Override\n" + " public void onClick(View v){\n"+ methodString.toString()+ "}"+ "});"; }
总结
通过对这个小小的插件的开发和重构以及功能添加,虽然项目很小,但是工程和面向对象的思想的重要性已经体现了出来,在一个拥有良好项目结构的工程下增加新功能是非常简答而明快的,如果像最初版本那样把所有的代码写在一个文件中而没有进行逻辑拆分的话,新增功能基本等于重写项目,这肯定是痛苦的。
还有一点值得一提,在做项目的时候第一步永远是总体构思,第二部是具体拆分,写代码这件事的优先级并没有那么高,容易犯的一个问题就是一提到某个功能马上就开始写具体代码,这样的结果往往费力不讨好,有一个明确的功能拆分和行进步骤会极大的增强开发体验。
至此,RemoveButterKnife系列文章就告一段落了,这几篇文章的目的不仅仅是记录开发RemoveButterKnife插件中的思路和遇到的问题,更重要的是总结了作者我开发软件项目的一个历程,而把这些写下来的过程,也是巩固这段历程的重要步骤。