实现的效果
首先展示一下运行的效果
这里展示了自动新建mvp架构
了解基本概念
开发环境
工欲善其事必先利其器,如果要开发一个软件,那么我们首先需要了解的这个软件的开发工具。平常我们开发android 使用都是android studio ,但是很遗憾,android studio 由于太过于定制化导致无法用于插件的开发。而这时候我们就需要android studio 的老爸 IntelliJ IDEA。
idea 的下载地址:https://www.jetbrains.com/idea/
在idea的官网,我们可以看到有收费版和社区版(也就是免费版)。这里我们选择idea的免费版就可以了。后面的安装过程暂且不表,相信都会安装了
新建项目
新建项目的时候如下图一样选择IDE Plugin,填写我们的项目名称和选择我们项目目录,sdk也如下图一样选择,后面直接create,这样我们的第一个项目就建好了。
这时候,接。然后点finish即可。
项目结构
建好项目以后,我们就可以看到如下的项目结构
- .idea : idea的一些配置信息。
- resources/META-INF/plugin.xml: 插件的一些描述信息。类似android中的Manifest文件。
- java: 这里就是写代码的地方,类似android 中java
项目整体配置
和android 项目类似。插件的也是有一个总体的配置文件,也就是我们在上面看到的plugin.xml文件。
这个文件看起来很多很乱,其实我们一项一项的去看的时候,一点都不复杂。
- id: 这个就是该插件的唯一标识符了。类似于android的包名。在android 中如果包名重复的话,就会出现后面的软件无法安装的问题。而在插件中表现出来的就是后面安装的插件会覆盖前面的。所以一定要记得改成自己的id。
- name: 这个就类似于android 中的应用名称
- version: 插件的版本
- vendor: 这个里面就是你的个人信息了。如果你想给自己写的插件打上自己的烙印,就改写这里。
- description: 这个里面就是描述一下,这个插件是干啥的。可以使用html标签来编写
- change-notes 更新信息
- extensions defaultExtensionNs 默认依赖的库
- actions 类似于android 中的activity。所有使用到的action,都要在这里进行注册。
开始编写插件
新建action
我们知道,在android 中,我们开始一个项目的第一步就是新建一共activity。而在这个项目中也是类似的,我们需要新建一共action。这里我们只需要在src下面的某个需要添加action的文件夹,右击->New->Plugn DevKit->Action。即可
这时候,我们会弹出一共弹窗。
其中,Action Id,Class Name就不多说了,Name为显示给用户的动作名称,Description为操作的描述。
Groups是比较重要的,他代表了我们按钮展示的位置。比如选择GenerateGroup,就是在Generate中显示(Windows中快捷键alt+insert,Mac快捷键control+enter)。还有build、code(显示在菜单栏上build、code按钮中)等等一系列Groups的位置,大家根据需要自己选择。不知道意思的网上查一下就好。
右边Actions是选择按钮位置的,First和Last分别为菜单最上方和最下方,点击Actions中的按钮,可以选择在该按钮的下方和上方。我这里模仿了ButterKnife Zelezny选择了GenerateGroup,并且放在了最下方。
这里我们再点击ok,就完成了第一个action的新建。
这时候我们再回头看看我们的plugin.xml.就会发现我们刚刚新建的action,已经自动添加好了。
这时候,我们只要点击右上角的运行,我们的第一个插件就运行起来了。
在新打开的idea的选择自己的项目,然后右击点击genetate.就能看到我们刚刚注册的"MvpAction"。当然这个时候你点击是没有任何效果的。
了解action
这里我们打开刚刚新建的action,会发现里面空空如也。只有一个actionPerformed的函数。其实你用Android
的想法去了解很自然的就能猜到这个函数的作用,就是类似oncreate,是作为我们整个程序的入口。但是这个函数被调用的时机,是在用户去点击你的action。
显示Dialog
我们都知道 ,plugin基本上都是基于java来开发的。所以理所当然的plugin的界面也是采用了java 的Swing。
就如同我们在android 中创建View一样,在plugin 中,我们也有两种方式创建UI界面,一种是通过Code直接创建。还有一种是通过IDE提供的工具,可视化创建。
目前在最新版本的Idea中,对于创建dialog,做了非常好的封装,体验非常好。所以这里我们选择通过可视化工具创建。
新建dialog
就如同之前创建Action一样简单,这里只需要点击Create Dialog Class 按钮,就会自动创建对应的文件。
其中我们发现生成了两个文件
其中.java 文件,就是我们dialog 的逻辑控制类似android中的dialog。
而.form 文件,就是dialog的 layout 了,类似android中的 layout.xml
我们点开.form 看一眼
相信所有android 开发人员不用教,就知道咋使用了。这不就是android 的拖拽生成布局么?
确实它和android的一模一样,并且更进一步,你只需要 将view 拖拽到布局中,java文件中,就会自动帮你声明并绑定。
这里我们试着添加一个文本框。
field name :类似android view id
text:和android text类似
这里我们回头再看java 文件
已经自动生成了TextField ,这里我们就可以直接拿这个对象去进行文本的操作了
并且下面还自动帮我们生成了显示dialog 的代码,实乃业界良心啊。
显示dialog
还记得我们之前在action 中提起的actionPerformed 方法么?
我们只需要把上面dialog 中的main 方法删除,并将其中代码转移到actionPerformed中即可
好了,我们的第一个插件,也是第一个dialog 就写好了。我们运行一下。
还记得,我们是把action 添加到了GenerateGroup 么?所以运行后,在新打开的idea 中,右击
点击Generate,就能找到我们的action ,也就是第一个
开始开发
好了,有了上面两点的基本概念,我们就可以开始我们本次的开发了
解析需求
需要开发的功能
这次我们只做一个最简单的版本,只实现下面的两个功能
- 自动新建mvp文件夹,并自动自动对应的新建Model,Presenter,ViewI 文件
- activity 自动添加 Presenter的 声明
开始实现
创建mvp文件夹
为了代码更新规整一些,所以对actionPerformed的方法做一点小小的改造。
@Override
public void actionPerformed(AnActionEvent event) {
// TODO: insert action logic here
//获取当前点击工程
Project project = event.getData(PlatformDataKeys.PROJECT);
Editor editor = event.getData(PlatformDataKeys.EDITOR);
actionPerformedImpl(project, editor);
}
public void actionPerformedImpl(Project project, Editor editor) {
mFile = PsiUtilBase.getPsiFileInEditor(editor, project); //获取点击的文件
mClass = getTargetClass(editor, mFile); //获取点击的类
if (mClass.getName() == null) {
return;
}
log.info("mClass=====" + mClass.getName());
mFactory = JavaPsiFacade.getElementFactory(project);
createMVPDir(); //创建mvp文件夹
}
private void createMVPDir() {
}
protected PsiClass getTargetClass(Editor editor, PsiFile file) {
int offset = editor.getCaretModel().getOffset();
PsiElement element = file.findElementAt(offset);
if (element == null) {
return null;
} else {
PsiClass target = (PsiClass)PsiTreeUtil.getParentOfType(element, PsiClass.class);
return target instanceof SyntheticElement ? null : target;
}
}
其实这程序比较好理解,就是获取了project和editor,然后调用了actionPerformedImpl。随后再次获取到了用户右击的文件和用户右击的class。
好了,有了上面获取的东西 ,我们就可以痛快的创建我们的mvp文件夹了
private PsiDirectory createMVPDir() {
PsiDirectory mvpDir = mFile.getParent().findSubdirectory("mvp"); //获取mvp文件夹
if (mvpDir == null) {
//如果没有找到mvp文件夹,则创建一个
mvpDir = mFile.getParent().createSubdirectory("mvp");
}
return mvpDir;
}
我们创建好了文件夹以后,接下来就是创建mvp文件了。但是在此之前,我们先要获取activity的名字,毕竟我们后面的创建的mvp的名字都和activity相关,同时我们也需要将我们刚刚创建的mvp文件夹的信息保存下来,因为我们的mvp文件都在该文件夹下。所以我们的actionPerformedImpl方法就变成了这样。
public void actionPerformedImpl( Project project, Editor editor) {
mFile = PsiUtilBase.getPsiFileInEditor(editor, project); //获取点击的文件
mClass = getTargetClass(editor, mFile); //获取点击的类
if (mClass.getName() == null) {
return;
}
log.info("mClass=====" + mClass.getName());
mFactory = JavaPsiFacade.getElementFactory(project);
mMVPDir = createMVPDir(); //创建mvp文件夹
viewName = mClass.getName();
creatMVPFile();
}
在创建的时候,我们首先需要确定mvp的名称
viewIName = mClass.getName() + "ViewI"; //viewI的名称
modelName = mClass.getName() + "Model"; //model的名称
presenterName = mClass.getName() + "Presenter"; //presenter的名称
如果已经包含了mvp文件,则不再重复创建
boolean hasModel = false; //是否包含model
boolean hasPresenter = false; //是否包含presenter
boolean hasViewI = false; //是否包含viewI
//查找是否已经包含有mvp文件,如果有的话,则不再创建
for (PsiFile f : mMVPDir.getFiles()) {
if (f.getName().contains("Model")) {
String realName = f.getName().split("Model")[0];
if (mClass.getName().contains(realName)) {
hasModel = true;
modelName = f.getName().replace(".java", "");
}
}
if (f.getName().contains("Presenter")) {
String realName = f.getName().split("Presenter")[0];
if (mClass.getName().contains(realName)) {
hasPresenter = true;
presenterName = f.getName().replace(".java", "");
}
}
if (f.getName().contains("ViewI")) {
String realName = f.getName().split("ViewI")[0];
if (mClass.getName().contains(realName)) {
hasViewI = true;
viewIName = f.getName().replace(".java", "");
}
}
}
这里之所以不直接用我们前面生成的mvp名字去查找,是害怕用户手动创建的名称,可能并不是我们的那种格式。这样处理可以更好的去重。
好了,现在我们就可以去创建我们的mvp文件了,但是首先我们先观察一下,我们的mvp文件结构,这里就以presenter举例
package com.yudao.truckmanager.test.mvp;
/**
1. Created on 2018-12-13 14:47:22.
*/
public class PluginTestActivityPresenter extends BasePresenter{
PluginTestActivityViewI mView;
@Inject
PluginTestActivityModel mModel;
public PluginTestActivityPresenter(PluginTestActivityViewI arg) {
super(arg);
this.mView = arg;
this.mModel = this.mView.getActivityComponent().getPluginTestActivityModel();
}
@Override
public BaseModel getBaseModel() {
return mModel;
}}
我们仔细观察过以后发现,每个presenter不一样的地方就是
- 最上面的package不一样
- 类名不一样
- ViewI的名称不一样
- Model名称不一样
- 构造函数的名字和参数不一样
一样的地方有
- 结构一样
- 都继承自BasePresenter
- 都包含ViewI 和model
- 都包含构造函数和getBaseModel函数
我们发现那些不一样的地方,除了package,其他都已经知道了。我们只需要再获取package 就一定能将这个presenter写出来。
虽然plugin官方库中并没有提供直接获取package的方法。但是我们在前面已经获取到了presenter的文件,并且package是和文件的path相关的。所以我们可以通过以下函数获取package
public static String getFilePackageName(VirtualFile dir) {
if(!dir.isDirectory()) {
// 非目录的取所在文件夹路径
dir = dir.getParent();
}
String path = dir.getPath().replace("/", ".");
String preText = "src.main.java";
int preIndex = path.indexOf(preText) + preText.length() + 1;
path = path.substring(preIndex);
return path;
}
好了,万事俱备。我们可以写出下面的代码
private void createPresenter() {
//创建文件
PsiFile presenterFile = mMVPDir.createFile(presenterName + ".java");
//生成要写入的字符串
StringBuffer modelText = new StringBuffer();
modelText.append("package " + AndroidUtils.getFilePackageName(mMVPDir.getVirtualFile()) + ";\n\n\n");
modelText.append(getHeaderAnnotation() + "\n");
modelText.append("public class " + presenterName + " extends BasePresenter{\n\n\n");
modelText.append(viewIName + " mView;\n");
modelText.append(" @Inject\n");
modelText.append(modelName + " mModel;\n");
modelText.append(" public " + presenterName + "(" + viewIName + " arg) {\n" +
" super(arg);\n" +
" this.mView = arg;\n" +
" this.mModel = this.mView.getActivityComponent().get" + modelName + "();\n" +
"\n" +
" }\n");
modelText.append(" @Override\n" +
" public BaseModel getBaseModel() {\n" +
" return mModel;\n" +
" }");
modelText.append("}");
//将字符串写入文件
Util_File.string2Stream(modelText.toString(), presenterFile.getVirtualFile().getPath());
}
/**
* 生成该代码生成的时间
* @return
*/
private String getHeaderAnnotation() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = sdf.format(System.currentTimeMillis());
String annotation = "/**\n" +
" * Created on " + time + ".\n" +
" */";
return annotation;
}
/**
* 将字符串写入文件
* @param javatempelt
* @param fileName
*/
public static void string2Stream(String javatempelt, String fileName) {
File file=new File(fileName);
if (file.exists()){
file.delete();
}else {
if (!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
}
try {
PrintWriter printWriter=new PrintWriter(file);
printWriter.print(javatempelt);
printWriter.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
上面除了创建文件使用的是插件库特有的方法外,其他都是使用的java的方法。相信你们一看就懂。就是拼接字符串,然后将字符串写入文件。
其实除了这种使用java原生的方法外,我们也可以使用插件库提供的方法:
PsiFileFactory.getInstance(project).createFileFromText
从字面上,我们就很好理解这个函数,但是这里java提供的方法就能满足需求。所以偷懒采用的原生方法。
以此类推,我们接着创建viewI 和model。
private void createModel() {
PsiFile ModelFile = mMVPDir.createFile(modelName + ".java");
StringBuffer modelText = new StringBuffer();
modelText.append("package " + AndroidUtils.getFilePackageName(mMVPDir.getVirtualFile()) + ";\n\n\n");
modelText.append(getHeaderAnnotation() + "\n");
modelText.append("public class " + modelName + " extends BaseModel{\n\n\n");
modelText.append(" @Inject\n" +
" public " + modelName + "() {\n" +
" }");
modelText.append("}");
Util_File.string2Stream(modelText.toString(), ModelFile.getVirtualFile().getPath());
}
private void createViewI() {
PsiFile viewIFile = mMVPDir.createFile(viewIName + ".java");
StringBuffer modelText = new StringBuffer();
modelText.append("package " + AndroidUtils.getFilePackageName(mMVPDir.getVirtualFile()) + ";\n\n\n");
modelText.append(getHeaderAnnotation() + "\n");
modelText.append("public interface " + viewIName + " extends BaseViewI{\n\n\n");
modelText.append("}");
Util_File.string2Stream(modelText.toString(), viewIFile.getVirtualFile().getPath());
}
将上面的判断mvp文件是否存在和拼接mvp名称,可以得到这个函数
private void creatMVPFile() {
viewIName = mClass.getName() + "ViewI"; //viewI的名称
modelName = mClass.getName() + "Model"; //model的名称
presenterName = mClass.getName() + "Presenter"; //presenter的名称
log.info("mClass=====" + mClass.getName());
boolean hasModel = false; //是否包含model
boolean hasPresenter = false; //是否包含presenter
boolean hasViewI = false; //是否包含viewI
//查找是否已经包含有mvp文件,如果有的话,则不再创建
for (PsiFile f : mMVPDir.getFiles()) {
if (f.getName().contains("Model")) {
String realName = f.getName().split("Model")[0];
if (mClass.getName().contains(realName)) {
hasModel = true;
modelName = f.getName().replace(".java", "");
}
}
if (f.getName().contains("Presenter")) {
String realName = f.getName().split("Presenter")[0];
if (mClass.getName().contains(realName)) {
hasPresenter = true;
presenterName = f.getName().replace(".java", "");
}
}
if (f.getName().contains("ViewI")) {
String realName = f.getName().split("ViewI")[0];
if (mClass.getName().contains(realName)) {
hasViewI = true;
viewIName = f.getName().replace(".java", "");
}
}
}
if (!hasPresenter) {
createPresenter();
}
if (!hasViewI) {
createViewI();
}
if (!hasModel) {
createModel();
}
}
好了,这样我们已经创建了一个建议的一键生成mvp文件的插件,让我们来试试威力。
很纳爱斯,现在我们的一个最简单的插件就已经写好了。已经可以帮我省很多事了。
但是你有木有发现,我们的activity还没有动,仍然需要activity去手动实现viewI的接口,去实现viewI的接口里面的方法,所以我们要继续开始下一步。
修改activity(fragment)
第一步,我们自然是让我们的activity去Implements的ViewI接口。
既然我们之前已经获得了activity 的class ,那么就去这个class里面去找找看有木有类似addImplements或者setImplements的方法。但是很可惜我们找到关于Implements的方法只有一个,就是
PsiReferenceList list = mClass.getImplementsList();
那么我们就尝试着往这个list去添加我们新的接口吧
list.add(mFactory.createReferenceFromText(viewIName,mClass));
我们想创建一个新的接口,就是使用createReferenceFromText方法
其中viewIName上面接口的名称,mClass就是我们要添加接口的mClass。
好了现在我们再来添加新的变量,就是presenter,我们知道view层是需要持有presenter对象的。
所以类似上面的,我们可以编写的下面代码
mClass.addBefore(mFactory.createAnnotationFromText("@Inject", mClass), mClass.getMethods()[0]);
mClass.addBefore(mFactory.createFieldFromText("protected " + presenterName + " mPrenseter;", mClass), mClass.getMethods()[0]);
由于我的项目中使用了dragger2,所以我的presenter是采用注入方法引入的,所以我要先注入注解
@Inject
而createAnnotationFromText方法就是用来创建注解的
mClass.addBefore(mFactory.createAnnotationFromText("@Inject", mClass), mClass.getMethods()[0]);
这句函数的意思就是,mClass的第一个方法上面添加一个注解“@Inject”。这样可以让格式更好看。
同样的createFieldFromText是用来创建变量的。后面那句话就是创建一个“protected Prensenter mPrensenter”在mClass的第一个方法上面。
既然接口,注解,变量,我们都已经知道了怎么添加,那么最重要的函数是怎么添加的呢?其实是和上面一模一样。只不过创建的方法变成了createMethodFromText
mClass.add(mFactory.createMethodFromText("@Override public BaseActivity getActivity() {return this;}", mClass));
只不过,我们在创建这些内容的时候,你的字符串前面千万不能包含空格,同时你的字符串中间也不能包含换行。程序在添加你的内容的时候,会自动格式化相关代码。
最后我们最终的代码如下
/**
* 修改activity
*
* @param project
*/
private void writeActivity(@NotNull Project project) {
WriteCommandAction.runWriteCommandAction(project, new Runnable() {
@Override
public void run() {
PsiReferenceList list = mClass.getImplementsList();
list.add(mFactory.createReferenceFromText(viewIName,mClass));
mClass.addBefore(mFactory.createAnnotationFromText("@Inject", mClass), mClass.getMethods()[0]);
mClass.addBefore(mFactory.createFieldFromText("protected " + presenterName + " mPrenseter;", mClass), mClass.getMethods()[0]);
mClass.add(mFactory.createMethodFromText("@Override public BaseActivity getActivity() {return this;}", mClass));
}
});
}
这时候,你肯定好奇 WriteCommandAction.runWriteCommandAction是啥。根本没提到过啊。其实就像我们在android中不能直接在ui线程进行网络访问等耗时操作一样。插件中也不能直接在主线程中直接修改代码。需要使用 WriteCommandAction.runWriteCommandAction进行包裹才可以。
当然我们之前创建mvp的时候直接使用java原生方法操作文件是属于作弊行为,可以避免这种限制。
好了,到这里,我们的代码就全部写完了。我们来运行一下,看看是不是符合我们的要求
导出插件
好了,我们已经成功的编写了我们的插件。那么问题来了,我们怎样将插件安装到我们的as呢?
Gradle|toolWindow |intelliJ |buildPlugin ,生成的插件zip文件在./build/distributions
第二步,as安装jar包的插件
1.选择 File —> Settings -----> Plugins ----> Install plugin from disk 然后选择下面的jar包。之后就会出现下面的界面
之后选择apply,再重启android studio就可以完成整个插件的安装
插件的发布
PublishPlugin
官方提供了两种方式:
1)发布到 JetBrains插件存储库
a) 注册 Jetbrains 账号,官网上传插件。
后话
其实这个插件还有很多优化的空间,例如
1.可以选择activity中方法是否提取到viewI中
2.在activity修改的时候,判断是否已经继承viewI接口,是否已经实现这些方法。如果已经有了,就不再重复添加
但是这个插件已经基本实现了我需要功能,所以我就没有再修改
下载地址
插件的下载地址:https://download.csdn.net/download/qq_17810899/10850847
源码的地址:https://github.com/richmond-rui/mvpHelper