Android Studio Module&页面 自定义模板

起因

最近项目在搞组件化,基本改造已完成(解耦什么的真的是体力活呀!因为项目比较庞大,所以老代码决定还是放着不动,有时间有人力再搞,新的大功能以组件化的方式开发),但是创建新的组件模块要拷贝很多相似的配置和代码,鉴于IDEA强大的模板功能,决定把这块给自动化了。

符合我们要求的自定义模板分两类:

1. Module 模板

位于<AS安装目录>/plugins/android/lib/templates/gradle-project下,创建Application或者Module,类型只有固定的几类,categoryApplicationformfactor为:

  • Mobile : 移动端应用
  • Wear : 可穿戴设备的应用
  • Car : 车载应用
  • TV : 电视应用
  • Things : 物联网应用

一开始,自定义模板的时候使用的Mobile,不生效,发现Mobile只认一个;按照JavaLibrary的写法,去掉formfactor,发现也只认一个;后来改成Things才可以。
Module模板在创建的时候,无法像页面模板那样,在Create Dialog中添加自定义参数,让用户选填,这个有点坑。

2. 页面模板

位于templates/activities下,在module中创建特定的文件或目录,这种模板可以在Create Dialog中添加自定义的参数。

目录结构

templateName
    ├--- template.xml         模板入口配置文件
    ├--- globals.xml          可供模板使用的全局预设变量
    ├--- recipe.xml           操作清单
    └--- <其他文件/目录>        模板源文件

1. template.xml

这是当前模板的入口配置文件。下面稍微介绍下一些重要的tag和字段。

<template> tag:

  • format 模板格式版本号,和templates目录中其他模板一样就好,当前是5,如果大于android plugin定义的CURRENT_FORMAT,该模板将会被跳过。
  • revision 模板版本号,如果有重名的模板(模板文件不仅可以放在<AS安装目录>/plugins/android/lib/templates下,还可以放在<用户主目录>/.android/templates目录下),取版本号最高的那个,如果相等,取修改时间最新的那个
  • namedescription属性标明了该模板显示的名称和描述。

<category> tag的value属性,标明该模板对应的分类:

  • Application:创建project或者module
    这种模板一般放在templates/gradle-project下
    通过File -> New -> New Project… / New Module… 进行创建
  • Activity:创建Activity的
    这种模板一般放在templates/activities下
    通过右击module -> New -> Activity 进行创建
  • XmlFragmentServiceFolderOther等:创建其他文件的模板
    这种模板一般放在templates/other下
    通过右击module -> New -> 选择对应的分类进行创建

<formfactor> tag的value属性,标明模板的二级分类,参见一、/ 1.

<thumbs> & <thumb> 指定该模板的图标,图标文件一般和template.xml同级

<parameter> tag标明当前模板自定义的参数
如果是project & module类的模板(categoryApplication),这些参数都是定死的,创建模板的界面也是定死的,我们无法自定义添加。但是其他的category类型(包括自定义category),这些参数都会在UI上展现,可以让用户输入。tag中的属性简介:

  • id:模板中引用时用到的参数名
  • name:参数显示的名称
  • type:参数类型,boolean、string等
  • default:参数的默认值

<globals> tag的file属性,指定了全局配置文件(一般为globals.xml.ftl),模板文件中可用的全局参数都可以写到这个文件中。

<execute> tag的file属性,指定了模板的创建清单(一般为recipe.xml.ftl),这个文件中注明了生成哪些文件、目录,以及怎么生成。

2. globals.xml.ftl

该文件定义了模板中可用的全局参数,系统预定义的参数可以参看这个链接
<global> tag定义了一个全局参数:

  • id:模板中引用时用到的参数名
  • type: 数据类型(可选)
  • value:参数值
    这里可以通过${name}的方式,引用系统预定义的参数,而且,系统还预定义一些方法可供使用,具体参见这个链接

3. recipe.xml.ftl

这个文件中通过tag操作符的方式指明了需要生成哪些文件,以及怎么生成,可用的tag如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mkdir at="somewhere" />
创建 somewhere 目录
<instantiate from="A.ftl" to="...B" />
解析A.ftl模板并生成为工程中的...B文件
<copy from="A" to="…B" />
将A文件拷贝为工程中的…B文件,纯拷贝(不做模板解析)
<append from="A" to="…B" />
将A文件追加到工程中的…B文件最后(不做模板解析)
<merge from="A.ftl" to="…B" />
解析A.ftl模板并合并到工程中的…B文件,只有xml和gradle文件才能用<merge>
<open file="…B" />
在AS中打开工程中的…B文件

以下是gradle相关的tag:

1
2
3
4
5
6
7
8
<apply plugin="…" />
应用某个gradle插件
<classpath mavenUrl="…" />
添加某个mavenUrl到classpath
<dependency mavenUrl="…" />
添加某个依赖

这个文件中也可以使用系统预定义的参数方法以及globals.xml.ftl中定义的参数、FreeMarker的语法。

举个🌰

gradle-project/DemoTemplate
    ├--- template.xml
    ├--- globals.xml
    ├--- recipe.xml
    ├--- mobile-module.png //tempalte的缩略图,从别的template里拷贝一个就好
    └--- root
           ├--- AndroidManifest.xml.ftl
           ├--- build.properties.ftl
           ├--- settings.gradle.ftl
           └--- res
                  └--- values
                         └--- strings.xml.ftl

template.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<?xml version="1.0"?>
<template
format="5"
revision="2"
name="Demo Module Template"
description="Create a Demo Template Module.">
<!-- Module 级别的模板 -->
<category value="Application" />
<!-- 类别为 Things -->
<formfactor value="Things" />
<!-- Create Dialog 上显示的预览图 -->
<thumbs>
<thumb>mobile-module.png</thumb>
</thumbs>
<!-- 以下都是一些默认参数 -->
<parameter
id="packageName"
name="Package name"
type="string"
constraints="app_package|nonempty"
default="com.your.myapp" />
<parameter
id="appTitle"
name="Module Name"
type="string"
constraints="nonempty"
default="module_" />
<parameter
id="minApi"
name="Minimum API level"
type="string"
constraints="apilevel"
default="8" />
<!--
Usually the same as minApi, but when minApi is a code name this will be the corresponding
API level
-->
<parameter
id="minApiLevel"
name="Minimum API level"
type="string"
constraints="apilevel"
default="8" />
<parameter
id="targetApi"
name="Target API level"
type="string"
constraints="apilevel"
default="19" />
<!--
Usually the same as targetApi, but when targeting a preview platform this is the code name instead
-->
<parameter
id="targetApiString"
name="Target API"
type="string"
constraints="apilevel"
default="19" />
<parameter
id="buildApi"
name="Build API level"
type="string"
constraints="apilevel"
default="19" />
<!--
Usually the same as buildApi, but when targeting a preview platform this is the code name instead
-->
<parameter
id="buildApiString"
name="Build API level"
type="string"
constraints="apilevel"
default="19" />
<parameter
id="makeIgnore"
name="Create .gitignore file"
type="boolean"
default="true" />
<globals file="globals.xml.ftl" />
<execute file="recipe.xml.ftl" />
</template>

globals.xml.ftl

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?>
<globals>
<!-- 项目根目录 -->
<global id="topOut" value="." />
<!-- Module根目录 -->
<global id="projectOut" value="." />
<global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
<global id="manifestOut" value="${manifestDir}" />
<global id="resOut" value="${resDir}" />
</globals>

recipe.xml.ftl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0"?>
<recipe>
<!-- 在Module的根目录下,创建 libs 和 drawable 目录 -->
<mkdir at="${escapeXmlAttribute(projectOut)}/libs" />
<mkdir at="${escapeXmlAttribute(resOut)}/drawable" />
<!-- 合并 settings.gradle -->
<merge from="root/settings.gradle.ftl"
to="${escapeXmlAttribute(topOut)}/settings.gradle" />
<!-- 根据当前template的root/build.gradle.ftl,初始化Module的build.grale -->
<instantiate from="root/build.gradle.ftl"
to="${escapeXmlAttribute(projectOut)}/build.gradle" />
<!-- 初始化AndroidManifest.xml -->
<instantiate from="root/AndroidManifest.xml.ftl"
to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<!-- 初始化strings.xml -->
<instantiate from="root/res/values/strings.xml.ftl"
to="${escapeXmlAttribute(resOut)}/values/strings.xml" />
<#if !createActivity>
<mkdir at="${escapeXmlAttribute(srcOut)}" />
</#if>
<#if makeIgnore>
<copy from="root://gradle-projects/common/gitignore"
to="${escapeXmlAttribute(projectOut)}/.gitignore" />
</#if>
<#include "root://gradle-projects/common/proguard_recipe.xml.ftl"/>
</recipe>

root/AndroidManifest.xml.ftl

1
2
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="${packageName}" />

root/build.gradle.ftl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apply plugin: 'com.android.library'
android {
compileSdkVersion <#if buildApiString?matches("^\\d+$")>${buildApiString}<#else>'${buildApiString}'</#if>
<#if explicitBuildToolsVersion!false>buildToolsVersion "${buildToolsVersion}"</#if>
defaultConfig {
minSdkVersion <#if minApi?matches("^\\d+$")>${minApi}<#else>'${minApi}'</#if>
targetSdkVersion <#if targetApiString?matches("^\\d+$")>${targetApiString}<#else>'${targetApiString}'</#if>
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

root/settings.gradle.ftl

1
include ':${projectName}'

root/res/values/string.xml.ftl

1
2
3
<resources>
<string name="app_name">${escapeXmlString(appTitle)}</string>
</resources>

设置好以后,重启Studio,然后 File -> New —> New Module.. 应该就能弹出创建的对话框了

参考