玩转Android Studio自定义模板插件-MVP模板为例

玩转Android Studio自定义模板插件-MVP模板为例

得益于Android Studio强大的拓展功能,我们可以开发出适于自己项目的插件以满足快速高效的开发需求,本文以MVP模板插件为例

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

演示图


在日常开发中,新建一个如名为ZModuleActivity的Activity:

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

Empty Activity(插件)的视图面板

新建后Android Studio(下文简称:AS)会依据我们编辑和勾选的一些要求生成具有一定代码的.java和.xml的Activity和Layout文件,以及会帮我们在AndroidManifest.xml注册好这个新建的Activity。

大体这样:

graph TD
        A[创建ZModuleActivity]-->B[生成ZModuleActivity.java]
        A[创建ZModuleActivity]-->C[生成activity_zmodule.xml]
        A[创建ZModuleActivity]-->D[AndroidManifest.xml追加Activity注册]

像这样 -->

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

一定程度上为我们省下了很多创建文件,写一些基础代码的时间,但是这远远没有达到我们想要的效果。一个成熟的项目开发会有着自己的一套代码架构,层次和模板,这就意味着每次新建Activity都要修改些代码以沿用项目框架。

如MVP设计模式代码结构可能如下,以YModuleActivity为例:

graph TD
        A[MVP]-->B[YModuleActivity]
        A[MVP]-->C[IYModule]
        A[MVP]-->D[YModulePresenter]

代码可能像这样 -->

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

AndroidManifest就不贴了,不是本文重点。

每新建一个Activity都要做大量无意义的基础代码工作,不知道你有没有这样的感觉:好烦啊,总是重复这些工作有意思吗?能不能直接帮我们生成好啊?有的人在感叹之余默默地把之前已经做好的(如本文 的YModule系列代码:YModuleActivity,IYModule,YModulePresenter)相关文件给copy过来改文件名,改类名,改代码,改注释,删冗余代码,最后变成类似YModule系列文件和代码这样。嗯,的确有省了不少事,但毕竟还是一连串的事儿。

既然有了困扰,就成了需求,既然是需求,就得满足需求。好在AS允许我们开发这样的代码模板插件,那么正式进入今天的主题。

打开AS安装目录(本文为“android-studio”),依次打开:android-studio》plugins》android》lib》templates》activities 结果如下:

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

看到目录名是否觉得眼熟?如果你还反应过来,没关系,看下图:

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

选择插件模板

没错,这里就是传说中新建Activity的模板插件集中营,每个目录即为一个插件。

知道这些就好办了,对EmptyActivity这个目录进行令人窒息的Ctrl C and Ctrl V操作, 然后对这个副本重命名(本文为“MVPActivity”)。你可能会问为什么一定是EmptyActivity?我可没有强调,只是顾名思义,空荡荡的模板可以省去删除一些冗余的模板代码操作,是吧?

打开MVPActivity目录,你可看到如下文件结构:

graph TD
        A[MVPActivity]-->B[root]
        A[MVPActivity]-->C[template.xml]
        A[MVPActivity]-->D[globals.xml.ftl]
        A[MVPActivity]-->E[recipe.xml.ftl]
        A[MVPActivity]-->F[template_blank_activity.png]

下面先简单介绍这几个文件,然后开始写模板

1. root目录

用于存放源代码模板和资源模板的.ftl文件,我们可以一路展开到最后一个目录,可以看到文件SimpleActivity.java.ftl(如果AS版本较高的话还有SimpleActivity.kt.ftl,顾名思义,分别是java版和kotlin版的模板,本文仅专注java)。查看文件:

package ${packageName};

import ${superClassFqcn};
import android.os.Bundle;
<#if includeCppSupport!false>
import android.widget.TextView;
</#if>

public class ${activityClass} extends ${superClass} {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
<#if generateLayout>
        setContentView(R.layout.${layoutName});
</#if>
<#include "../../../../common/jni_code_usage.java.ftl">
    }
<#include "../../../../common/jni_code_snippet.java.ftl">
}

很明显是Activity的模板嘛,一看便懂,确认过眼神。

  • ${xxx}为引用值,由前台面板(如上文新建Activity的插件视图面板)填写或勾选的结果值赋值,xxx可能为变量,也可能为函数返回值。
  • <#if xxx!bool值>yyy</#if>为if判断语句,与常见语言判断语法规则并无区别,只是写法的区别。
  • <#include "xxx">导入某某文件的内容。类似于Android布局中的标签用法。

这里稍微提一下:EmptyActivityroot目录下只有源代码模板目录src,因为模板比较简单,其直接引用了EmptyActivity同级目录commonroot目录下资源模板目录res,下文会有提及,本文只讲源代码模板,不做深究。

2. template.xml

定义插件视图面板的外观,布局,类似咱Android的layout.xml布局文件,查看文件:

<?xml version="1.0"?>
<template
    format="5"
    revision="5"
    name="Empty Activity"
    minApi="9"
    minBuildApi="14"
    description="Creates a new empty activity">

    <category value="Activity" />
    <formfactor value="Mobile" />

    <parameter
        id="instantAppActivityHost"
        name="Instant App URL Host"
        type="string"
        suggest="${companyDomain}"
        default="instantapp.example.com"
        visibility="isInstantApp!false"
        help="The domain to use in the Instant App route for this activity"/>

    <parameter
        id="instantAppActivityRouteType"
        name="Instant App URL Route Type"
        type="enum"
        default="pathPattern"
        visibility="isInstantApp!false"
        help="The type of route to use in the Instant App route for this activity" >
        <option id="path">Path</option>
        <option id="pathPrefix">Path Prefix</option>
        <option id="pathPattern">Path Pattern</option>
    </parameter>

    <parameter
        id="instantAppActivityRoute"
        name="Instant App URL Route"
        type="string"
        default="/.*"
        visibility="isInstantApp!false"
        help="The route to use in the Instant App route for this activity"/>

    <parameter
        id="activityClass"
        name="Activity Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${layoutToActivity(layoutName)}"
        default="MainActivity"
        help="The name of the activity class to create" />

    <parameter
        id="generateLayout"
        name="Generate Layout File"
        type="boolean"
        default="true"
        help="If true, a layout file will be generated" />

    <parameter
        id="layoutName"
        name="Layout Name"
        type="string"
        constraints="layout|unique|nonempty"
        suggest="${activityToLayout(activityClass)}"
        default="activity_main"
        visibility="generateLayout"
        help="The name of the layout to create for the activity" />

    <parameter
        id="isLauncher"
        name="Launcher Activity"
        type="boolean"
        default="false"
        help="If true, this activity will have a CATEGORY_LAUNCHER intent filter, making it visible in the launcher" />

    <parameter
        id="backwardsCompatibility"
        name="Backwards Compatibility (AppCompat)"
        type="boolean"
        default="true"
        help="If false, this activity base class will be Activity instead of AppCompatActivity" />

    <parameter
        id="packageName"
        name="Package name"
        type="string"
        constraints="package"
        default="com.mycompany.myapp" />

    <!-- 128x128 thumbnails relative to template.xml -->
    <thumbs>
        <!-- default thumbnail is required -->
        <thumb>template_blank_activity.png</thumb>
    </thumbs>

    <globals file="globals.xml.ftl" />
    <execute file="recipe.xml.ftl" />

</template>

对照着上文的【Empty Activity(插件)的视图面板】示意图或者自己新建个Empty Activity呼出面板,差不多能看懂或猜测到一大半吧?

下面是对parameter模块补充介绍:

  • parameter标签:表示一个参数模块,为变量的赋值而服务的视图块
  • id:变量名,上文提到引用值${xxx}中的变量
  • type:变量的数据类型,同java数据类型差不多,这里常见的是string和boolean类型,boolean型模板面板以勾选框的形式显示
  • constraints:约束,主要有class(输入框填写需要创建的java类名),layout(输入框填写需要创建的布局名),package(输入框+下拉框填写或选择包名),unique(填写内容不能与已有的java完整类名重名,或布局名重名,重复会自动在后面加个2,3...以此类推,nonempty(不能为空,输入框不能不填内容)
  • default:默认值,默认值会根据type类型自动填充到输入框或决定勾选框勾选状态
  • suggest:建议值,会覆盖默认值,配合变量或函数动态修改输入框内容或勾选框勾选状态
  • thumb标签:插件模板效果缩略图
  • global标签:引用全局变量文件
  • execute标签:执行模板代码配置文件,这里是个文件引用

3. globals.xml.ftl

声明一些全局变量的文件。查看文件:

<?xml version="1.0"?>
<globals>
    <global id="hasNoActionBar" type="boolean" value="false" />
    <global id="parentActivityClass" value="" />
    <global id="simpleLayoutName" value="${layoutName}" />
    <global id="excludeMenu" type="boolean" value="true" />
    <global id="generateActivityTitle" type="boolean" value="false" />
    <#include "../common/common_globals.xml.ftl" />
</globals>

可以看到其同template.xml内的parameter类似,内部有标签,分别定义id,type和默认值。

4. recipe.xml.ftl

用于执行模板代码的配置文件。查看文件:

<?xml version="1.0"?>
<#import "root://activities/common/kotlin_macros.ftl" as kt>
<recipe>
    <#include "../common/recipe_manifest.xml.ftl" />
    <@kt.addAllKotlinDependencies />

<#if generateLayout>
    <#include "../common/recipe_simple.xml.ftl" />
    <open file="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml" />
</#if>

    <instantiate from="root/src/app_package/SimpleActivity.${ktOrJavaExt}.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${activityClass}.${ktOrJavaExt}" />
    <open file="${escapeXmlAttribute(srcOut)}/${activityClass}.${ktOrJavaExt}" />

</recipe>

简单介绍下:

  • <#include/>标签,引入某文件内容
  • 打开文件标签,这个要看执行时机,如果文件还没生成,则可能会报错,所以该标签会放在生成目标文件代码后。
  • 标签,实例化,即执行模板代码。
  • from,执行指定某目录下的模板代码文件,上文提到模板文件是在root目录下
  • to,执行后的结果内容输出文件,像这样的${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml由函数,变量拼接文件名(包扩路径)的表达式,我们不需要了解它是怎么实现的,但根据你的开发经验一定能看得懂它会生成什么样的文件名(包括路径),我好像说了一句废话 T^T

5. template_blank_activity.png

没什么好介绍的,模板效果缩略图文件,上文提到用thumb标签使其显示于面板上。

说好的简单介绍,结果还是BB了那么多

玩转Android Studio自定义模板插件-MVP模板为例_乘月网_专注于移动互联网_android开发_html开发_java开发_linux运维_php开发_web开发

下面开始折腾“MVPActivity”插件

分析

上文已经给出代码示意图,我们需要自动生成YModuleActivity,IYModule和YModulePresenter三个java类,那么:

1. 需要各自三个类的源码模板

比如模板名分别为:MVPActivity.java.ftl,MVPInterface.java.ftl和MVPPresenter.java.ftl,由上文可知,文件于root->src->app_package目录下。

2. 需要各自三个类的类名变量和一个公共拼接变量

对于这样的代码:

public class YModuleActivity extends BaseActivity<IYModule.Presenter> implements IYModule.View {

    @Override
    public void setPresenter(IYModule.Presenter presenter) {
        // Bind presenter
        if (presenter == null) {
            presenter = new YModulePresenter(this);
        }
    }
}

需要转换成动态化模板成这样:

public class ${activityClass} extends BaseActivity<${mvpInterface}.Presenter> implements ${mvpInterface}.View {

    @Override
    public void setPresenter(${mvpInterface}.Presenter presenter) {
        // Bind presenter
        if (presenter == null) {
            presenter = new ${presenterClass}(this);
        }
    }
}

对,是这三个变量:activityClass,mvpInterface,presenterClass,上文查看template.xml文件中,已经看到“activityClass”已定义过,只要再添加另两个就好了。

心细的同学可能注意到YModuleActivity,IYModule和YModulePresenter都有个YModule,可以提取变量为commonClassName。

3. 需要一个时间变量(可选)

对于类的注释,比如这样:

/**
 * <pre>
 *     @author : www.icheny.cn
 *     @e-mail : ausboyue@gmail.com
 *     @time   : 2018.08.25
 *     @desc   : ${commonClassName} Activity.
 *     @version: 1.0.1
 * </pre>
 */

注释很多都可以共用或稍加修改便可,但是创建的时间(time)是动态的,那么需要变量:createTime来替换。

制作

1. 三个类的源码模板

MVPActivity.java.ftl

package ${packageName};

<#if generateLayout>
import cn.icheny.plugin.mvp.demo.R;
</#if>
import cn.icheny.plugin.mvp.demo.module.base.BaseActivity;

/**
 * <pre>
 *     @author : www.icheny.cn
 *     @e-mail : ausboyue@gmail.com
 *     @time   : ${createTime}
 *     @desc   : ${commonClassName} Activity.
 *     @version: 1.0.1
 * </pre>
 */
public class ${activityClass} extends BaseActivity<${mvpInterface}.Presenter> implements ${mvpInterface}.View {

    @Override
    protected void initData() {

    }

    @Override
    protected void initViews() {

    }

    @Override
    protected int layoutId() {
        return R.layout.${layoutName};
    }

    @Override
    public void showData() {

    }

    @Override
    public void setPresenter(${mvpInterface}.Presenter presenter) {
        // Bind presenter
        if (presenter == null) {
            presenter = new ${presenterClass}(this);
        }
    }
}

MVPInterface.java.ftl

package ${packageName};

import cn.icheny.plugin.mvp.demo.module.base.IBasePresenter;
import cn.icheny.plugin.mvp.demo.module.base.IBaseView;

/**
 * <pre>
 *     @author : www.icheny.cn
 *     @e-mail : ausboyue@gmail.com
 *     @time   : ${createTime}
 *     @desc   :  MVP --> ${commonClassName} VP --> View,Presenter. Child View And Child Presenter Interface For This Module.
 *     @version: 1.0.1
 * </pre>
 */
public interface ${mvpInterface} {

    interface View extends IBaseView<Presenter> {
        /**
         * show data
         */
        void showData();

    }

    interface Presenter extends IBasePresenter {
        /**
         * load data
         */
        void loadData();
    }
}

MVPPresenter.java.ftl

package ${packageName};

/**
 * <pre>
 *     @author : www.icheny.cn
 *     @e-mail : ausboyue@gmail.com
 *     @time   : ${createTime}
 *     @desc   :  MVP --> ${commonClassName} P --> Presenter. Child Presenter Implements Class For This Module.
 *     @version: 1.0.1
 * </pre>
 */
public class ${presenterClass} implements ${mvpInterface}.Presenter {

    private final ${mvpInterface}.View view;

    /**
     * Constructor
     *
     * @param view
     */
    public ${presenterClass}(${mvpInterface}.View view) {
        this.view = view;
    }

    @Override
    public void loadData() {

    }

    @Override
    public void doRefresh() {

    }
}

2. 定义UI面板,template.xml

......
    <parameter
        id="createTime"
        name="Create Time"
        type="string"
        default="2018.08.25"
        help="The time that will show on class annotation." />

    <parameter
        id="commonClassName"
        name="Common Class Name"
        type="string"
        default="Main"
        help="The string ,Other class will use for their name." /> 

    <parameter
        id="activityClass"
        name="Activity Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${commonClassName}Activity"
        default="MainActivity"
        help="The name of the activity class to create" />

    <parameter
        id="mvpInterface"
        name="MVP Interface Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="I${commonClassName}"
        default="IMain"
        help="The name of the mvp interface to create" />  

    <parameter
        id="presenterClass"
        name="Presenter Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${commonClassName}Presenter"
        default="MainPresenter"
        help="The name of the mvp presenter class to create" />    
</template>
.......

限于文章篇幅,这里只贴添加和修改的代码。

3. 定义执行模板代码配置文件,recipe.xml.ftl

......
    <instantiate from="root/src/app_package/MVPActivity.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />

    <open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />

    <instantiate from="root/src/app_package/MVPInterface.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${mvpInterface}.java" />

    <instantiate from="root/src/app_package/MVPPresenter.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${presenterClass}.java" />
......

同样,这里只贴添加和修改的代码。

完成以上,重启AS,就可以愉快地玩耍了。

至此,本文结束。Demo源码后续文章更新发布,敬请关注。

2018年09月3日更新:Demo源码:Github:AndroidStudioPluginForMVP

© 版权声明
THE END
喜欢就支持以下吧
点赞0 分享
评论 共4条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容