本文主要介绍了Android studio中的模板开发技术,其中live和file template比较简单,不详细介绍,主要篇幅集中在Android studio的project 模板和module模板介绍上。

live template

  1. psfi
  2. null
  3. nn
  4. logt, logm, logr
  5. 自定义
  6. provider
  7. addTest
  8. addDemo

file template

  1. 单例模板 Singleton
  2. 自定义测试 TestActivity TestView
  3. 自定义 dagger 模板
  4. 自定义 MVP 模板
  5. 快速生成 file template

project template & module template

一、freeMarker 简介

基本语法

  • String Built-ins for strings

  • ${value} ${function(value)} 计算并输出value值

  • 流程判断 <#if>、<#elseif>、<#else>、</#if>

    <#if condition>
    ...
    <#elseif condition2>
    ...
    <#elseif condition3>
    ...
    <#else>
    ...
    </#if>
  • list 遍历

    <#list sequence as item>
    Part repeated for each item
    </#list>
  • 赋值 <#assign>

    <#assign themeName=theme.name!'AppTheme'>

实战

看一个来自于NewAndroidProject中的AndroidManifest.xml.ftl模板

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity android:name=".${activityClass}"
android:label="@string/title_${activityToLayout(activityClass)}">
<#if parentActivityClass != "">
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value="${parentActivityClass}" />
</#if>
<#if isLauncher>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</#if>
</activity>
</application>
</manifest>

在这个模板中可以看到基本的语法使用,对freemarker的语法了解到这个程度就够用了。

param function directive
${activityClass} activityToLayout() <#if isLauncher>
${parentActivityClass} </#if>
isLauncher
*activityToLayout 是内建于Android studio idea中的函数,还有其他有用的内建函数将在后面介绍。

二、 分析Android studio中的自带模板

/Applications/Android Studio.app/Contents/plugins/android/lib/templates

➜  templates ls -la
-rw-r--r--@ 1 zhou admin 10695 Sep 16 13:49 NOTICE
drwxr-xr-x@ 18 zhou admin 612 Sep 16 13:49 activities <-重点
-rw-r--r--@ 1 zhou admin 310 Sep 16 13:49 build.gradle
drwxr-xr-x@ 6 zhou admin 204 Sep 16 13:49 eclipse
drwxr-xr-x@ 3 zhou admin 102 Sep 16 13:49 gradle
drwxr-xr-x@ 10 zhou admin 340 Sep 16 13:49 gradle-projects <-重点
drwxr-xr-x@ 28 zhou admin 952 Sep 16 13:49 other

先看一个简单的module模板 EmptyActivity

/Applications/Android Studio.app/Contents/plugins/android/lib/templates/activities

➜  activities ls -la
....
drwxr-xr-x@ 9 zhou admin 306 Sep 16 13:49 BasicActivity
drwxr-xr-x@ 7 zhou admin 238 Sep 16 13:49 EmptyActivity
drwxr-xr-x@ 7 zhou admin 238 Sep 16 13:49 FullscreenActivity

...
➜  tree EmptyActivity
.
├── globals.xml.ftl
├── recipe.xml.ftl
├── root
│   └── src
│   └── app_package
│   └── SimpleActivity.java.ftl
├── template.xml
└── template_blank_activity.png

<template.xml>分析

template.xml是一个模板的 Metadata文件,维护了模板的输入参数、模板名称、thumbs图标等信息

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

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

<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="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>

<parameter>

<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" />

type : string、enum、boolean、separator

constraints : 参数校验

constraints 含义
nonempty 非空
apilevel 最小支持API
package 合法的java包名
class 合法的class名
activity 合法的fully-qualified activity名(com.example.myapp.MyActivity)
layout、string、drawable、 id 资源名合法[a-z][_]
unique 唯一
exists 必须存在

parameter对应创建工程模板时的form

<category>

<category value="Activity" />
  • Applications
  • Activities
  • UI Components

<globals>、<execute>

  • globals 指定全局属性定义文件
  • execute 指定指令文件
    后面会详细介绍

<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>

这里include了 common_globals.xml.ftl

<globals>
...
<#assign themeName=theme.name!'AppTheme'>
<#assign themeNameNoActionBar=theme.nameNoActionBar!'AppTheme.NoActionBar'>
<#assign appCompat=backwardsCompatibility!(theme.isAppCompat)!false>
<#assign appCompatActivity=appCompat && (buildApi gte 22)>

<global id="themeName" type="string" value="${themeName}" />
...
<global id="appCompat" type="boolean" value="${appCompat?string}" />
...

<global id="manifestOut" value="${manifestDir}" />
<global id="buildVersion" value="${buildApi}" />
...
<global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
<global id="resOut" value="${resDir}" />
...
</globals>

可以看到这个文件很简单定义了一些在freemarker处理过程中需要用到的全局变量。

<recipe.xml.ftl>分析

<?xml version="1.0"?>
<recipe>
<merge from="root/AndroidManifest.xml.ftl"
to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<merge from="root/res/values/manifest_strings.xml.ftl"
to="${escapeXmlAttribute(resOut)}/values/strings.xml" />

<#if generateLayout>
<#if appCompat && !(hasDependency('com.android.support:appcompat-v7'))>
<dependency mavenUrl="com.android.support:appcompat-v7:${buildApi}.+"/>
</#if>
<instantiate from="root/res/layout/simple.xml.ftl"
to="${escapeXmlAttribute(resOut)}/layout/${simpleLayoutName}.xml" />

<#if (isNewProject!false) && !(excludeMenu!false)>
<#include "recipe_simple_menu.xml.ftl" />
</#if>
<#include "recipe_simple_dimens.xml" />

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

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

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

<dependency>

<dependency mavenUrl="com.android.support:appcompat-v7:${buildApi}.+"/>
添加dependency 到 module build.gradle文件中,这种方法适合添加少量的依赖。

<copy>

<copy from="{src}" to="{dest}" />
将文件从src复制到dest,如果目标目录不存在则会自动创建目录。

<instantiate>

<instantiate from="{src}" to="{dest}" />
同copy,不同的是,instantiate 会转义模板中的${value}

<merge>

<merge from="{src}" to="{dest}" />
源文件于目标文件合并(只支持xml和build.gradle文件)

<open>

<open file="{dest}" />
在IDE中打开目标文件

root目录

├── root
│   └── src
│   └── app_package
│   └── SimpleActivity.java.ftl

这里的 app_package 等价于
app_package = src/com/mogujie/....
这个目录下存储了模板源文件(*.ftl)

IDEA 内建函数

函数名 功能
activityToLayout FooActivity => activity_foo
layoutToActivity activity_foo => FooActivity
camelCaseToUnderscore FooBar => foo_bar
underscoreToCamelCase foo_bar => FooBar
escapeXmlAttribute “<,>” => encode("<,>")
slashedPackageName com.example.foo =>  com/example/foo

三、 Android studio如何处理Freemarked文件呢

上述4个命令不是Freemarker的内建命令标签,Android studio通过一个叫做TemplateHandler的类另外处理模板。

TemplateHandler.java

TemplateHandler.java

/** Executes the given recipe file: copying, merging, instantiating, opening files etc */
private void execute(
final Configuration freemarker,
String file,
final Map<String, Object> paramMap) {
try {
mLoader.setTemplateFile(new File(mRootPath, file));
Template freemarkerTemplate = freemarker.getTemplate(file);

StringWriter out = new StringWriter();
freemarkerTemplate.process(paramMap, out);
out.flush();
String xml = out.toString();

// Parse and execute the resulting instruction list.
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

saxParser.parse(new ByteArrayInputStream(xml.getBytes()),
new DefaultHandler() {
@Override
public void startElement(String uri, String localName, String name,
Attributes attributes)
throws SAXException {
if (mNoToAll) {
return;
}

try {
boolean instantiate = TAG_INSTANTIATE.equals(name);
if (TAG_COPY.equals(name) || instantiate) {
String fromPath = attributes.getValue(ATTR_FROM);
String toPath = attributes.getValue(ATTR_TO);
if (toPath == null || toPath.isEmpty()) {
toPath = attributes.getValue(ATTR_FROM);
toPath = AdtUtils.stripSuffix(toPath, DOT_FTL);
}
IPath to = getTargetPath(toPath);
if (instantiate) {
instantiate(freemarker, paramMap, fromPath, to);
} else {
copyTemplateResource(fromPath, to);
}
} else if (TAG_MERGE.equals(name)) {
String fromPath = attributes.getValue(ATTR_FROM);
String toPath = attributes.getValue(ATTR_TO);
if (toPath == null || toPath.isEmpty()) {
toPath = attributes.getValue(ATTR_FROM);
toPath = AdtUtils.stripSuffix(toPath, DOT_FTL);
}
// Resources in template.xml are located within root/
IPath to = getTargetPath(toPath);
merge(freemarker, paramMap, fromPath, to);
} else if (name.equals(TAG_OPEN)) {
// The relative path here is within the output directory:
String relativePath = attributes.getValue(ATTR_FILE);
if (relativePath != null && !relativePath.isEmpty()) {
mOpen.add(relativePath);
}
} else if (!name.equals("recipe")) { //$NON-NLS-1$
System.err.println("WARNING: Unknown template directive " + name);
}
} catch (Exception e) {
sMostRecentException = e;
AdtPlugin.log(e, null);
}
}
});

} catch (Exception e) {
sMostRecentException = e;
AdtPlugin.log(e, null);
}
}

全部的代码在这里可以查看,这里不详细描述。
android/src/com/android/tools/idea/templates

模板实例化过程

四、当我们新建一个工程时,Android studio为我们干了啥

NewAndroidProject

/Applications/Android Studio.app/Contents/plugins/android/lib/templates/gradle-projects/NewAndroidProject

├── globals.xml.ftl
├── recipe.xml.ftl
├── root
│   ├── build.gradle.ftl
│   ├── gradle.properties.ftl
│   ├── local.properties.ftl
│   ├── project_ignore
│   └── settings.gradle.ftl
├── template.xml
└── template_new_project.png

cat recipe.xml

<?xml version="1.0"?>
<recipe>
<instantiate from="root/build.gradle.ftl"
to="${escapeXmlAttribute(topOut)}/build.gradle" />

<#if makeIgnore>
<copy from="root/project_ignore"
to="${escapeXmlAttribute(topOut)}/.gitignore" />
</#if>

<instantiate from="root/settings.gradle.ftl"
to="${escapeXmlAttribute(topOut)}/settings.gradle" />

<instantiate from="root/gradle.properties.ftl"
to="${escapeXmlAttribute(topOut)}/gradle.properties" />

<copy from="../../gradle/wrapper"
to="${escapeXmlAttribute(topOut)}/" />

<#if sdkDir??>
<instantiate from="root/local.properties.ftl"
to="${escapeXmlAttribute(topOut)}/local.properties" />
</#if>
</recipe>

NewAndroidModule

/Applications/Android Studio.app/Contents/plugins/android/lib/templates/gradle-projects/NewAndroidModule

├── globals.xml.ftl
├── recipe.xml.ftl
├── root
│   ├── AndroidManifest.xml.ftl
│   ├── build.gradle.ftl
│   ├── module_ignore
│   ├── proguard-rules.txt.ftl
│   ├── res
│   │   ├── mipmap-anydpi
│   │   ├── mipmap-hdpi
│   │   ├── mipmap-mdpi
│   │   ├── mipmap-xhdpi
│   │   ├── mipmap-xxhdpi
│   │   ├── mipmap-xxxhdpi
│   │   └── values
│   │   ├── colors.xml
│   │   ├── strings.xml.ftl
│   │   └── styles.xml.ftl
│   ├── settings.gradle.ftl
│   └── test
│   └── app_package
│   ├── ApplicationTest.java.ftl
│   └── ExampleUnitTest.java.ftl
├── template.xml
└── template_new_project.png

cat recipe.xml

<?xml version="1.0"?>
<recipe>

<#if backwardsCompatibility!true>
<dependency mavenUrl="com.android.support:appcompat-v7:${buildApi}.+"/>
</#if>

<#if unitTestsSupported>
<dependency mavenUrl="junit:junit:4.12" gradleConfiguration="testCompile" />
</#if>

<#if !createActivity>
<mkdir at="${escapeXmlAttribute(srcOut)}" />
</#if>

<mkdir at="${escapeXmlAttribute(projectOut)}/libs" />

<merge from="root/settings.gradle.ftl"
to="${escapeXmlAttribute(topOut)}/settings.gradle" />
<instantiate from="root/build.gradle.ftl"
to="${escapeXmlAttribute(projectOut)}/build.gradle" />
<instantiate from="root/AndroidManifest.xml.ftl"
to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />

<mkdir at="${escapeXmlAttribute(resOut)}/drawable" />
<#if copyIcons && !isLibraryProject>
<copy from="root/res/mipmap-hdpi"
to="${escapeXmlAttribute(resOut)}/mipmap-hdpi" />
<copy from="root/res/mipmap-mdpi"
to="${escapeXmlAttribute(resOut)}/mipmap-mdpi" />
<copy from="root/res/mipmap-xhdpi"
to="${escapeXmlAttribute(resOut)}/mipmap-xhdpi" />
<copy from="root/res/mipmap-xxhdpi"
to="${escapeXmlAttribute(resOut)}/mipmap-xxhdpi" />
<copy from="root/res/mipmap-xxxhdpi"
to="${escapeXmlAttribute(resOut)}/mipmap-xxxhdpi" />
</#if>
<#if makeIgnore>
<copy from="root/module_ignore"
to="${escapeXmlAttribute(projectOut)}/.gitignore" />
</#if>
<#if enableProGuard>
<instantiate from="root/proguard-rules.txt.ftl"
to="${escapeXmlAttribute(projectOut)}/proguard-rules.pro" />
</#if>
<#if !(isLibraryProject??) || !isLibraryProject>
<instantiate from="root/res/values/styles.xml.ftl"
to="${escapeXmlAttribute(resOut)}/values/styles.xml" />
<#if buildApi gte 22>
<copy from="root/res/values/colors.xml"
to="${escapeXmlAttribute(resOut)}/values/colors.xml" />
</#if>
</#if>

<instantiate from="root/res/values/strings.xml.ftl"
to="${escapeXmlAttribute(resOut)}/values/strings.xml" />

<instantiate from="root/test/app_package/ExampleInstrumentedTest.java.ftl"
to="${escapeXmlAttribute(testOut)}/ExampleInstrumentedTest.java" />

<#if unitTestsSupported>
<instantiate from="root/test/app_package/ExampleUnitTest.java.ftl"
to="${escapeXmlAttribute(unitTestOut)}/ExampleUnitTest.java" />
</#if>
<#if includeCppSupport!false>
<instantiate from="root/CMakeLists.txt.ftl"
to="${escapeXmlAttribute(projectOut)}/CMakeLists.txt" />

<mkdir at="${nativeSrcOut}" />
<instantiate from="root/native-lib.cpp.ftl" to="${nativeSrcOut}/native-lib.cpp" />
</#if>

</recipe>

五、 build.gradle 合并问题

apply 合并失败

期望结果

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

实际结果

apply plugin: 'com.neenbedankt.android-apt' plugin: 'com.android.application'

只支持compile命令的build.gradle合并

  • dependencies 中,apt 引用代码消失
  • provide 不支持 merge

tools 命名空间合并后消失问题

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<!--合并时被吞掉-->
package="com.mogujie.myapplication">
...
</manifest>

六、壳工程template

to be continue

七、参考资料