从零编写一个一键生成mvp的android studio插件
实现的效果
首先展示一下运行的效果
这里只展示了自动新建mvp架构,后面还有自动插入dragger2没有加入,不过效果也差不多
工欲善其事必先利其器,如果要开发一个软件,那么我们首先需要了解的这个软件的开发工具。平常我们开发android 使用都是android studio ,但是很遗憾,android studio 由于太过于定制化导致无法用于插件的开发。而这时候我们就需要android studio 的老爸 IntelliJ IDEA。
idea 的下载地址:https://www.jetbrains.com/idea/
在idea的官网,我们可以看到有收费版和社区版(也就是免费版)。这里我们选择idea的免费版就可以了。后面的安装过程暂且不表,相信都会安装了
新建项目的时候如下图一样选择IntelliJ Platform Plugin,sdk也如下图一样选择,后面直接下一步
这时候,接着填写我们的项目名称和选择我们项目目录。然后点finish即可。这样我们的第一个项目就建好了。
建好项目以后,我们就可以看到如下的项目结构
- .idea : idea的一些配置信息。
- resources/META-INF/plugin.xml: 插件的一些描述信息。类似android中的Manifest文件。
- src: 这里就是写代码的地方,类似android 中src
这里和android 不一样的地方就是,idea并没有自动帮我们生成src下面的文件夹。这里需要我们手动去建立。在你建立完成以后,src目录下面就如图所示
和android 项目类似。插件的也是有一个总体的配置文件,也就是我们在上面看到的plugin.xml文件。
这个文件看起来很多很乱,其实我们一项一项的去看的时候,一点都不复杂。
- id: 这个就是该插件的唯一标识符了。类似于android的包名。在android 中如果包名重复的话,就会出现后面的软件无法安装的问题。而在插件中表现出来的就是后面安装的插件会覆盖前面的。所以一定要记得改成自己的id。
- name: 这个就类似于android 中的应用名称
- version: 插件的版本
- vendor: 这个里面就是你的个人信息了。如果你想给自己写的插件打上自己的烙印,就改写这里。
- description: 这个里面就是描述一下,这个插件是干啥的。可以使用html标签来编写
- change-notes 更新信息
- extensions defaultExtensionNs 默认依赖的库
- actions 类似于android 中的activity。所有使用到的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,会发现里面空空如也。只有一个actionPerformed的函数。其实你用Android
的想法去了解很自然的就能猜到这个函数的作用,就是类似oncreate,是作为我们整个程序的入口。但是这个函数被调用的时机,是在用户去点击你的action。
这里我们发现我们的action是继承自一个AnAction,就像我们的activity都继承自activity一样。但是AnAction,功能太少了,我们需要将继承的父类换成BaseGenerateAction。就像我们现在写activity基本上都继承自AppCompatActivity一样。
为了代码更新规整一些,所以对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);
}
@Override
public void actionPerformedImpl(@NotNull 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() {
}
其实这程序比较好理解,就是获取了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(@NotNull 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
从字面上,我们就很好理解这个函数,但是由于我对插件库提供的方法不是很熟。所以采用的原生方法。
以此类推,我们接着创建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去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呢?
第一步,选择build,然后选择下面的prepare plugin…
后面就会在你项目的根目录下面生成了一个jar包
第二步,as安装jar包的插件
1.选择 File —> Settings -----> Plugins ----> Install plugin from disk 然后选择下面的jar包。之后就会出现下面的界面
之后选择apply,再重启android studio就可以完成整个插件的安装
其实这个插件还有很多优化的空间,例如
1.可以选择activity中方法是否提取到viewI中
2.在activity修改的时候,判断是否已经继承viewI接口,是否已经实现这些方法。如果已经有了,就不再重复添加
但是这个插件已经基本实现了我需要功能,所以我就没有再修改
插件的下载地址:https://download.csdn.net/download/qq_17810899/10850847
源码的地址:https://github.com/richmond-rui/mvpHelper