从零编写一个一键生成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,都要在这里进行注册。

开始编写插件

新建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。
在这里插入图片描述
这里我们发现我们的action是继承自一个AnAction,就像我们的activity都继承自activity一样。但是AnAction,功能太少了,我们需要将继承的父类换成BaseGenerateAction。就像我们现在写activity基本上都继承自AppCompatActivity一样。
在这里插入图片描述

创建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);
    }

    @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不一样的地方就是

  1. 最上面的package不一样
  2. 类名不一样
  3. ViewI的名称不一样
  4. Model名称不一样
  5. 构造函数的名字和参数不一样

一样的地方有

  1. 结构一样
  2. 都继承自BasePresenter
  3. 都包含ViewI 和model
  4. 都包含构造函数和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(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呢?
第一步,选择build,然后选择下面的prepare plugin…
在这里插入图片描述
后面就会在你项目的根目录下面生成了一个jar包
在这里插入图片描述

第二步,as安装jar包的插件
1.选择 File —> Settings -----> Plugins ----> Install plugin from disk 然后选择下面的jar包。之后就会出现下面的界面
在这里插入图片描述
之后选择apply,再重启android studio就可以完成整个插件的安装

后话

其实这个插件还有很多优化的空间,例如
1.可以选择activity中方法是否提取到viewI中
2.在activity修改的时候,判断是否已经继承viewI接口,是否已经实现这些方法。如果已经有了,就不再重复添加

但是这个插件已经基本实现了我需要功能,所以我就没有再修改

下载地址

 

posted @ 2018-12-14 16:17  蓝冷然  阅读(455)  评论(0编辑  收藏  举报