从零编写一个一键生成mvp的android studio插件

实现的效果

首先展示一下运行的效果
运行效果
这里展示了自动新建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 ,也就是第一个
在这里插入图片描述

在这里插入图片描述

开始开发

好了,有了上面两点的基本概念,我们就可以开始我们本次的开发了

解析需求

需要开发的功能

这次我们只做一个最简单的版本,只实现下面的两个功能

  1. 自动新建mvp文件夹,并自动自动对应的新建Model,Presenter,ViewI 文件
  2. 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不一样的地方就是

  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

从字面上,我们就很好理解这个函数,但是这里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

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值