本文主要介绍了Android studio中的模板开发技术,其中live和file template比较简单,不详细介绍,主要篇幅集中在Android studio的project 模板和module模板介绍上。
live template
psfi
null
nn
logt, logm, logr
自定义
provider
addTest
addDemo
file template
单例模板 Singleton
自定义测试 TestActivity TestView
自定义 dagger 模板
自定义 MVP 模板
快速生成 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 >
实战
看一个来自于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" /> <thumbs > <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
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(); 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); } IPath to = getTargetPath(toPath); merge(freemarker, paramMap, fromPath, to); } else if (name.equals(TAG_OPEN)) { String relativePath = attributes.getValue(ATTR_FILE); if (relativePath != null && !relativePath.isEmpty()) { mOpen.add(relativePath); } } else if (!name.equals("recipe" )) { 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
<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
七、参考资料