公司有自己的一套 MVP
架构,每次创建新的 MVP Activity
或 MVP Fragment
时,都需要写相对应的 Presenter
和 Contract
,想到 AS
内嵌的 Activity、Fragment、AIDL
等模板,便捷好用。就想自己将这个 MVP
整成一个模板,这样就能节省不少写模板代码的时间了。
FreeMarker Template Language(FTL)
FreeMarker
模板语言,是一台基于模板和要改变的数据,并用来生成输出文本的(HTML
网页、电子邮件、配置文件、源代码等 )
的通用的工具,意味着要准备数据在真实编程语言中来显示, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
FTL 基本语法
- 标签
FTL
标签与 HTML
标签有相似之处,但是不是从属关系。FTL
标签都是以 #
开头的。用户自定义 FTL
标签需要使用 @
符号替代 #
。
<#include "xxx.tfl"/>
- 注释
<#-- 这是注释 -->
- if 、 else 指令
<#if 变量名>
......
<#elseif 变量名>
......
<#else>
......
</#if>
例如:
<#if generateKotlin>
<instantiate from="root/src/app_package/SimpleActivity.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<#else>
<instantiate from="root/src/app_package/SimpleActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
</#if>
- 访问变量值
${变量名} 例如:${packageName}
- 引入其他文件
<#include "xxx.ftl"/>
例如:
<#include "../common/common_globals.xml.ftl" />
-
list
变量
<#list variables as loopVariable>
repeatThis
</#list>
例:
<#list fruits as fruit>
<li>${fruit}
</#list>
模板格式
按照惯例,模板目录结构中由 FreeMarker
处理的任何文件都应具有 .ftl
文件扩展名。因此,如果你的一个源文件是 MyActivity.java
,并且它包含 FreeMarker
指令,那么它应该被命名为 MyActivity.java.ftl
。
目录结构
模板是包含许多XML和FreeMarker文件的目录。只有两个必填文件是template.xml和recipe.xml.ftl。模板源文件(PNG文件,模板化Java和XML文件等)属于root/子目录。下面是模板的示例目录结构:
imageroot
存放我们的代码模板文件和资源文件
globals.xml.ftl
定义全局变量
recipe.xml.ftl
配置需要应用的模板路径和生成的文件的路径
template.xml
定义模板参数。
template.xml
每个模板目录必须包含一个 template.xml
文件。此 XML
文件包含有关模板的元数据,包括 IDE
将作为用户选项显示的名称,描述,类别和用户可见参数。XML
文件还指示 recipe.xml.ftl
的名称,以及 globals.xml
文件
下面是一个 template.xml
文件示例:
<?xml version="1.0"?>
<template
format="5"
revision="5"
name="MVP Activity"
minApi="9"
minBuildApi="14"
description="Creates a new MVP activity">
<category value="Activity" />
<formfactor value="Mobile" /> # 如同我们在创建module时所显示的类型,如:Wear、TV等。
<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" />
<thumbs>
<!-- default thumbnail is required -->
# 可选,用于创建模板时,在左边显示名为template_blank_activity的预览图片
<thumb>template_blank_activity.png</thumb>
</thumbs>
# 可选,将工程定义的全局变量包含进来
<globals file="globals.xml.ftl" />
# 开始执行模板渲染
<execute file="recipe.xml.ftl" />
</template>
image
以下是 template.xml
支持的标签列表:
-
format
此模板遵循的模板格式版本 -
revision
此模板的整数版本(您可以在更新模板时递增),可选 -
name
模板名称。在AS
操作File
-->New
-->Activity
可找到对应的Activity
-
description
模板的描述。见图 minApi
模板所需的最小 API
值,IDE
将确保在实例化模板之前,目标工程的 minSdkVersion
不低于这个值,可选
minBuildApi
此模板所需的最小构建目标 API
。在实例化模板之前,IDE
将确保目标项目的目标是大于或等于此值的 API
级别。这可确保模板可以安全地使用较新的 API(
可选择由运行时 API
级别检查保护 )
,而不会将编译时错误引入目标项目,可选
<dependency>
表示模板要求目标项目中存在给定库。如果不存在,IDE
将向项目添加依赖项。
name : 库的名称。目前接受的值有:
1、android-support-v4
2、android-support-v13
revision : 此模板所需的库的最低版本。
例如:
<dependency name =“android-support-v4”revision =“8”/>
<category>
模板类型。此元素是可选的
value : 模板类型。应该是以下值之一:
1、Applications
2、Activities
3、UI Components
例 :
<category value =“Activities”/>
<parameter>
用户可自定义的模板参数
id : 表示此变量的标识符在 FreeMarker 文件中作为全局变量提供。如果标识符是 foo,则参数值将在 FreeMarker 文件中通过 ${foo} 可得到
name : 模板参数的显示名称。假设 <category value = "Activities" />,则在 AS 中通过 File --> New --> Activity 可找到对应的 Activity
type : 参数的数据类型。要么string,boolean,enum,或 separator
help : 操作 <parameter/> 时,底部显示的提示语
default : 参数的默认值
suggest : 建议值
visibility : 根据其他 View 的 ${id} 定义该 View 是应该可见还是消失
constraints : 属性值约束
android:inputType : text|textEmailAddress|number|textPassword
type
定义了实际的视图属性,分别有
EditText、Spinner
和CheckBox
类型
string : 表示对应的实际视图是一个EditText
enum : 表示对应的实际视图是一个Spinner
boolean : 表示对应的实际视图是一个 CheckBox
constraints
可选属性。强加于参数值的约束。可以使用组合约束 |。有效的约束类型有:
class : 该值应表示有效的Java类名称,例如(Activity、Fragment、Presenter、Model、Utility等类名)
nonempty : 该属性字段不能为 null 或 empty。此约束仅在指定其他约束时才有意义,例如layout,这意味着该值不应表示现有布局资源名称
unique : 这个确保包中不会存在相同的名称。(也就是说,假设项目中已经存在 MainActivity,则通过显示 Main2Activity 等简单建议,避免使用重复的 Activity 名称)
apilevel : 数字化的 API 级别
package : 有效的 java 类名
layout : 有效的 layout 名称
drawable : 有效的 drawable 名称
string : 有效的 string
id : 有效 id 资源名称
exists : 值必须已经存在; 此约束仅在指定其他约束时才有意义,例如layout,这意味着该值应表示现有布局资源名称
suggest
可选的。表示自动建议参数值的
FreeMarker
表达式(“
动态默认值”)
。当用户修改其他参数值时,如果此参数的值未从其默认值更改,则该值将更改为此表达式的结果。这似乎是循环的,因为参数是可以与suggest
相互对照的值,但这些表达式仅针对未编辑的值进行更新,因此这种方法允许用户编辑任一参数值,而另一个将自动更新为合理的默认值属性的内置方法 :
1、suggest="${layoutToActivity(layoutName)}"
layoutName="activity_main" --> MainActivity
layoutName="main" --> MainActivity
2、suggest="${activityToLayout(activityClass)}"
activityClass=“MainActivity” --> activity_main
activityClass=“Main” --> activity_main
3、suggest="${underscoreToCamelCase(classToResource(activityClass))}Adapter" //首字母是大写
activityClass=“MainActivity” --> MainAdapter
activityClass=“Main” --> MainAdapter
4、suggest="item_${classToResource(activityClass)}" //首字母变成了小写
activityClass=“MainActivity” --> item_main
activityClass=“Main” --> item_main
classToResource(activityClass)
:这句话的意思是,当我们在创建该模板后,在 activityClass
对应的文本框中输入某个值,比如:test
,它会直接在 layoutName
对应的文本框中显示,即:test
,所以以完整的语句 suggest="${classToResource(activityClass)}_activity"
而言,此时 layoutName
对应的文本框中显示的应该是 test_activity
。
<option>
类型的参数enum,表示值的选择.
id : 如果选择此选项,则设置的参数值
minApi : 可选的。选择此选项时所需的最低API级别。minSdkVersion在实例化模板之前,IDE将确保目标项目的值不低于此值
[text] : 此元素的文本内容表示选择的显示值
<parameter
id="navType"
name="Navigation Type"
type="enum"
default="none">
<option id="none" default="true">None</option>
<option id="tabs" minApi="11">Tabs</option>
<option id="pager" minApi="11">Swipe Views</option>
<option id="dropdown" minApi="11">Dropdown</option>
</parameter>
<thumb>
表示模板的缩略图。<thumb>
元素应包含在 <thumbs>
元素内。此元素的文本内容表示缩略图的路径。如果此元素具有多个属性,则它们将被视为参数值的选择器。例如,如果有两个缩略图:
<thumbs>
<thumb>template.png</thumb>
<thumb navType="tabs">template_tabs.png</thumb>
</thumbs>
如果值 navType
模板参数是 tabs
,name
模板“预览”缩略图将显示template_tabs.png
,否则显示 template.png
。
global.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>
这个文件用于定义一些全局变量。
-
<global>
定义一个全局变量 -
id
: 变量名称 -
type
: 变量类型 -
value
: 默认值
访问这些变量的方法:${
变量id}
。例如:
${hasNoActionBar};
recipe.xml.ftl
recipe.xml.ftl
包含从此模板生成代码时应执行的各个指令。例如,您可以复制某些文件或目录( copy
指令 )
,可选地通过 FreeMarker
运行源文件( instantiate
指令)
,并在生成代码( open
指令 )
后要求 ADT
在 Eclipse
中打开文件。
注意: recipe.xml.ftl
的名称可自定义,但必须在 template.xml
声明。但按照惯例,最好称之为 recipe.xml.ftl
。
注意:全局变量 globals.xml.ftl
可用于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>
<#if generateKotlin>
<instantiate from="root/src/app_package/SimpleActivity.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<#else>
<instantiate from="root/src/app_package/SimpleActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
</#if>
</recipe>
该文件用于定义如何生成文件和代码。
<copy>
用于从 root
文件夹复制文件到目标文件
唯一必需的参数是 from
指定要在 root/
目录下复制的源文件的位置。如果需要,将自动创建所有必需的祖先目录。
默认目标位置是输出目录根目录下的相同路径(即目标项目的位置)。如果提供了可选 to
参数,则指定输出目录。请注意,如果 from
路径以 ...
结尾 .ftl
,则会自动删除它。例如 <instantiate from="res/values/strings.xml.ftl" />
是足够的; 这将创建一个名为的文件 strings.xml
,而不是 strings.xml.ftl
。
此参数以递归方式工作,因此如果 from
是目录,则以递归方式复制该目录。
<instantiate>
将 .ftl
文件转化成为 .java
或 .kt
文件
<merge>
用于合并文件,如将模板文件的 string.xml
合并到我们项目的 string.xml
<open>
在代码生成后打开指定文件,例如:当我们创建一个 Activity
时,AS
会自动打开 Activity
以及布局文件
<#include>
导入另一个 ftl
文件
额外模板功能
FreeMarker
几个重要函数 :
string activityToLayout(string)
作用:
此函数将类似
activity calss
的标识符字符串(
例如FooActivity)
转换为对应的资源标识符字符串,例如activity_foo
。参数:
activityClass,活动类名称,例如FooActivity重新格式化。
string camelCaseToUnderscore(string)
作用 :
此函数将
camel-case
标识符字符串(
例如FooBar)
转换为其对应的下划线分隔标识符字符串,例如foo_bar
。参数 :
camelStr
,驼峰式字符串,例如FooBar
转换为下划线分隔的字符串。
string escapeXmlAttribute(string)
作用 :
此函数用来转义字符串,例如
Android's
,它可以用作XML
属性值:Android's
。特别是,它将转义'
,"
,<
和&
。参数 :
str
,要转义的字符串。
string escapeXmlText(string)
作用 :
此函数用来转义字符串,例如
A & B's
可以将其用作XML
文本。这意味着它将转义<
和>
,但不像escapeXmlAttribute
它将不会转义'
和"
。在前面的示例中,它将转义字符串为A & B\s
。请注意,如果您想要使用XML
文本作为<string>
资源的值,您应该考虑使用escapeXmlString
,因为它执行额外的所需的字符串资源转义参数 :
str
,转义为正确XML文本的字符串。
string escapeXmlString(string)
作用 :
此函数用来转义字符串,例如
A & B's
,它适合作为XML
文本插入字符串资源文件中,例如A & B\s
。除了转义<
和&
之类的XML
字符外,它还执行其他Android
特定的转义,例如使用反斜杠转义撇号,等等参数 :
str
,例如,Activity's Title
以转义为适当的资源XML
值。
string extractLetters(string)
作用 :
此函数从字符串中提取所有字母,有效删除任何标点符号和空白字符。
参数 :
str
,从中提取字母的字符串。
string classToResource(string)
作用 :
此函数将
Android
类名称(
例如FooActivity
或FooFragment)
转换为相应的资源标识符字符串,例如foo
,删除activity
或fragment
后缀。目前删除的后缀列在下面:
Activity
Fragment
Provider
Service
参数 :
className
,类名,例如FooActivity
重新格式化为下划线分隔的字符串,后缀已删除。
string layoutToActivity(string)
作用 :
此函数将资源标识符字符串
(
例如activity_foo)
转换为对应的Java
类标识符字符串,例如FooActivity
。参数 :
resourceName
,资源名称,例如activity_foo
重新格式化。
string slashedPackageName(string)
作用 :
此函数将完整
Java
包名称转换为其对应的目录路径。例如,如果给定的参数是com.example.foo
,则返回值为com/example/foo
。参数 :
packageName
,要重新格式化的包名称,例如com.example.foo
。
string underscoreToCamelCase(string)
作用 :
此函数将下划线分隔的字符串
(
例如foo_bar)
转换为其对应的驼峰字符串,例如FooBar
。参数 :
underStr
,下划线分隔的字符串,例如foo_bar
转换为驼峰字符串。
工具元数据
创建活动布局时,请确保在布局的根视图中包含活动名称作为 tools
命名空间的一部分,如以下示例所示
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/hello_world"
android:padding="@dimen/padding_medium"
tools:context=".${activityClass}" />
我们在布局中使用此属性来维护到用于布局的活动活动的映射。(
是的,可以有多个,但是此属性显示您要编辑布局的活动上下文。例如,它将用于查找主题注册(
这是每个活动而不是每个布局 ) )
在清单文件中,我们将来会将其用于其他功能 - 例如预览操作栏,这也需要我们知道活动上下文。
具体参考
网友评论