Android Template学习笔记

Android Studio的Template,大致可以从复杂程度上分为三种:Live Template,File Template和Activity Template(或Project Template)。

Live Template

用几个字母+tab展开成一段代码,然后在需要的位置填上自定义内容。

设置入口:Settings->Editor->Live Templates

右侧点击+号之后填写下面的信息,代码中的$name$表示这段代码中可以自定义的部分,在第一个$name$处输入,所有的$name$占位部分都会同时变化。第一处占位符输入完成按enter后会自动跳转到第二处占位符输入下一个变量。
代码和缩写字母定义完毕后,一定要选左下角的生效语言范围才会生效,右下角是设置触发的按键(一般是tab

File Template

以模板生成文件
设置入口:Settings->Editor->File Templates

点击+创建新模板。编写模板会使用一些占位符语法,在创建文件时自动转换成代码。

预定义的变量

预定义的变量可以直接在文件模板中使用:

  • ${PACKAGE_NAME}:新文件被创建的包名
  • ${NAME}:文件名
  • ${USER}:当前用户系统登录的用户名
  • ${DATE}:当前系统日期
  • ${TIME}:当前系统时间
  • ${YEAR}:当前年份
  • ${MONTH}:当前月份
  • ${MONTH_NAME_SHORT}:月份前三个字母。Example: Jan, Feb, etc.
  • ${MONTH_NAME_FULL}:月份全称。 Example: January, February, etc.
  • ${DAY}:current day of the month
  • ${DAY_NAME_SHORT}:first 3 letters of the current day name. Example: Mon, Tue, etc.
  • ${DAY_NAME_FULL}:full name of the current day. Example: Monday, Tuesday, etc.
  • ${HOUR}:当前小时
  • ${MINUTE}:当前分钟
  • ${PROJECT_NAME}:project名

include其他模板

使用#parse可以在模板中包括进其他模板的内容,例如通用的文件创建人和创建时间信息,可以放在一个header文件中,在第二个Includestab中定义:

代码如下

1
2
3
4
/**
*
* Created by ${USER} on ${DATE}.
*/

在使用时就可以在其他模板中使用#parse("Kt File Header.kt")引入这个header,生成文件时自动转换成这样的注释代码。

自定义变量

除了以上预定义的变量,用${xxx}的格式自定义一些变量名,在创建文件时会同时提示填写这些变量:

1
2
3
4
5
6
7
8
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end

import androidx.room.Entity

#parse("Kt File Header.kt")
@Entity(tableName = "${tableName}s")
data class ${NAME}(var ${props}: String): BaseModel()

在这段代码中自定义了tableName和props两个变量,在新建文件时就会要求填写:

最后生成的代码:

1
2
3
4
5
6
7
8
9
10
package info.zhufree.windwhite.bean

import androidx.room.Entity

/**
*
* Created by zhufree on 2019/4/26.
*/
@Entity(tableName = "tests")
data class TestModel(var title: String) : BaseModel()

以上两种模板在Jetbrain家的其他IDE中也可以使用。

ActivityTemplate

最后一种也是最复杂的模板,可以直接生成多个配套文件,典型的例子就是新建Activity,会直接生成一个Activity文件+layout文件,如果是复杂的带Fragment的,带ViewModel的,则会自动生成Fragment以及ViewModel文件等:

除了Activity,也可以直接创建Project等等:

这种模板不能直接在设置中定义,需要自己编写,放在AndroidStudio的模板文件夹中,重启Studio即可使用。编写模板需要用到Free Marker语言
文件夹路径:
MacOS:/Applications/Android Studio.app/Contents/plugins/android/lib
Windows:[Android Studio安装目录]/plugins/android/lib/templates

模板文件夹中不同文件的作用:

template.xml

这个文件可以看做定义Activity模板的清单文件,列出在创建Activity时需要填的所有变量,对应这个页面:

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<?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" />
<!--适用于手机,相对的还有平板,TV,可穿戴设备等-->
<formfactor value="Mobile" />

<!--parameter标签定义需要填的参数-->
<!--id 在后面引用时使用-->
<!--name 解释这个参数意义的名字-->
<!--type 填的数据类型-->
<!--constraints 填的内容限制(类名,独一,不可为空)-->
<!--suggest 在填写其他参数时可以根据其他参数的变化自动填充(根据layoutName进行下划线转驼峰命名生成)-->
<!--default 默认值-->
<!--help 显示在左下角的提示文字-->
<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" />

<!--boolean类型显示的是一个勾选框+Name-->
<parameter
id="generateLayout"
name="Generate Layout File"
type="boolean"
default="true"
help="If true, a layout file will be generated" />

<!--visibility 可见性,这里意思是勾选了上面的generateLayout才会显示填写布局文件名这一行-->
<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" />

<parameter
id="includeInstantAppUrl"
name="Associate a URL with this Activity"
type="boolean"
default="false"
visibility="isInstantApp!false"
help="If true, this activity will be associated with URL, improving discovery of your Instant App" />

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

<!-- 128x128 thumbnails relative to template.xml -->
<!-- 显示在左边的缩略图 -->
<thumbs>
<!-- default thumbnail is required -->
<thumb>template_blank_activity.png</thumb>
</thumbs>

<!-- 声明一些全局定义的global变量 -->
<globals file="globals.xml.ftl" />
<!-- 执行recipe.xml.ftl文件来生成目标文件 -->
<execute file="recipe.xml.ftl" />
</template>

在这个页面用户填写的变量值,将和globals.xml.ftl中预定义的全局变量值一起作为可用的变量在后面编写模板代码的过程中使用。

recipe.xml

recipe文件中定义以哪个文件为模板来生成哪个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0"?>
<#import "root://activities/common/kotlin_macros.ftl" as kt>
<recipe>
<#include "../common/recipe_manifest.xml.ftl" />
<@kt.addAllKotlinDependencies />
<!-- 根据generateLayout判断是否要生成layout文件 -->
<#if generateLayout>
<!-- 在recipe_simple中处理生成layout文件的逻辑,和下面的instantiate一样 -->
<#include "../common/recipe_simple.xml.ftl" />
<!-- 文件创建完之后用open打开 -->
<open file="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml" />
</#if>
<!-- from 模板文件位置 to 目标文件位置 -->
<instantiate from="root/src/app_package/SimpleActivity.${ktOrJavaExt}.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.${ktOrJavaExt}" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.${ktOrJavaExt}" />
</recipe>

以上这些文件和png图片都在模板文件夹的根目录下,对于较复杂的模板,root文件夹下分为srcres文件夹,以及清单文件模板AndroidManifest.xml.ftl等等,和真实的项目结构类似,src文件夹中app_package则代表包名路径,之后是activity等类文件的模板,一般有.java.ftl.kt.ftl两种后缀的,分别对应生成.java文件和.kt文件。
res文件夹中则可能会有layout/menu/values等文件夹,放置布局,菜单,资源值等类型的模板文件。

Activity.ftl

src文件夹下的ActivityFragment等类的模板文件,同理可添加Adapter之类的模板。
以最简单的Activity+kotlin语言为例:

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
// 声明包名
package ${escapeKotlinIdentifiers(packageName)}
// 导入一些必要的包,这里的${superClassFqcn}在common_global.xml.ftl中定义,根据是否是appCompatActivity,是否useAndroidX导入了不同的Activity
import ${superClassFqcn}
import android.os.Bundle
// 判断是否导入布局控件
<#if (includeCppSupport!false) && generateLayout>
import kotlinx.android.synthetic.main.${layoutName}.*
</#if>

// 定义activity类名,继承父类
class ${activityClass} : ${superClass}() {

// 自动生成onCreate方法
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 如果生成了layout文件,调用setContentView方法
<#if generateLayout>
setContentView(R.layout.${layoutName})
<#include "../../../../common/jni_code_usage.kt.ftl">
<#elseif includeCppSupport!false>

// Example of a call to a native method
android.util.Log.d("${activityClass}", stringFromJNI())
</#if>
}
<#include "../../../../common/jni_code_snippet.kt.ftl">
}

superClassFqcn的定义(common_global.xml.ftl):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<#if !appCompat>
<global id="superClass" type="string" value="Activity"/>
<global id="superClassFqcn" type="string" value="android.app.Activity"/>
<global id="Support" value="" />
<global id="actionBarClassFqcn" type = "string" value="android.app.ActionBar" />
<global id="kotlinActionBar" type="string" value="actionBar" />
<global id="kotlinFragmentManager" type="string" value="fragmentManager" />
<#elseif appCompatActivity>
<global id="superClass" type="string" value="AppCompatActivity"/>
<global id="superClassFqcn" type="string" value="${getMaterialComponentName('android.support.v7.app.AppCompatActivity', useAndroidX)}"/>
<global id="Support" value="Support" />
<global id="actionBarClassFqcn" type = "string" value="${getMaterialComponentName('android.support.v7.app.ActionBar', useAndroidX)}" />
<global id="kotlinActionBar" type="string" value="supportActionBar" />
<global id="kotlinFragmentManager" type="string" value="supportFragmentManager" />
<#else>
<global id="superClass" type="string" value="ActionBarActivity"/>
<global id="superClassFqcn" type="string" value="${getMaterialComponentName('android.support.v7.app.ActionBarActivity', useAndroidX)}"/>
<global id="Support" value="Support" />
<global id="actionBarClassFqcn" type = "string" value="${getMaterialComponentName('android.support.v7.app.ActionBar', useAndroidX)}" />
<global id="kotlinActionBar" type="string" value="supportActionBar" />
<global id="kotlinFragmentManager" type="string" value="supportFragmentManager" />
</#if>

layout.xml.ftl

作为模板的布局文件在root/res/layout文件夹下,以一个最简单的simple.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
<?xml version="1.0" encoding="utf-8"?>
<!-- 判断根布局用哪个版本的ConstraintLayout -->
<${getMaterialComponentName('android.support.constraint.ConstraintLayout', useAndroidX)}
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
<#if hasAppBar && appBarLayoutName??>
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/${appBarLayoutName}"
</#if>
<!--如果有appbar,需要设置behavior-->
tools:context="${packageName}.${activityClass}">
<!-- 添加一个示例的TextView -->
<#if isNewProject!false>
<TextView
<#if includeCppSupport!false>
android:id="@+id/sample_text"
</#if>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</#if>
</${getMaterialComponentName('android.support.constraint.ConstraintLayout', useAndroidX)}>

总体来说,这一类模板结构可能比较复杂,但原理都是根据预定义的变量和自定义的变量,加上事先编写好的模板代码,通过不同的条件判断,填充变量名等操作最终生成目标文件代码。
编写模板文件需要了解一些常用的预定义变量和free maker语法等,可以参考上面的官方手册。
使用ActivityProject层级的模板需要一定的时间成本,属于磨刀不误砍柴工,一旦完成能够节省很多复制粘贴再修改的工作,但需要平衡好模板化和自定义化的程度,编写符合自己需求同时也具有足够复用价值的模板代码。