玩转Android Studio自定义模板插件-MVP模板为例
得益于Android Studio强大的拓展功能,我们可以开发出适于自己项目的插件以满足快速高效的开发需求,本文以MVP模板插件为例
在日常开发中,新建一个如名为ZModuleActivity的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注册]
像这样 -->
一定程度上为我们省下了很多创建文件,写一些基础代码的时间,但是这远远没有达到我们想要的效果。一个成熟的项目开发会有着自己的一套代码架构,层次和模板,这就意味着每次新建Activity都要修改些代码以沿用项目框架。
如MVP设计模式代码结构可能如下,以YModuleActivity
为例:
graph TD
A[MVP]-->B[YModuleActivity]
A[MVP]-->C[IYModule]
A[MVP]-->D[YModulePresenter]
代码可能像这样 -->
AndroidManifest就不贴了,不是本文重点。
每新建一个Activity都要做大量无意义的基础代码工作,不知道你有没有这样的感觉:好烦啊,总是重复这些工作有意思吗?能不能直接帮我们生成好啊?有的人在感叹之余默默地把之前已经做好的(如本文 的YModule
系列代码:YModuleActivity,IYModule,YModulePresenter)相关文件给copy过来改文件名,改类名,改代码,改注释,删冗余代码,最后变成类似YModule系列文件和代码这样。嗯,的确有省了不少事,但毕竟还是一连串的事儿。
既然有了困扰,就成了需求,既然是需求,就得满足需求。好在AS允许我们开发这样的代码模板插件,那么正式进入今天的主题。
打开AS安装目录(本文为“android-studio”),依次打开:android-studio》plugins》android》lib》templates》activities 结果如下:
看到目录名是否觉得眼熟?如果你还反应过来,没关系,看下图:
没错,这里就是传说中新建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布局中的标签用法。
这里稍微提一下:EmptyActivity的root目录下只有源代码模板目录src,因为模板比较简单,其直接引用了EmptyActivity同级目录common的root目录下资源模板目录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类似,内部有
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了那么多
下面开始折腾“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
暂无评论内容