# 基于FreeMaker的Android Studio模板
* **背景**:
由于我们项目中的基础架构设计是基于MVP模式的(MVP和MVPVM),它的缺点之一就是在新建页面时需要创建大量接口和实现等等文件,这种重复的工作有时候会令人烦躁,开发的体验十分不好。为此,我们可以通过使用模板来帮助创建这些有规律可循的文件,并且进一步规范我们的代码(类、文件命名、方法流程等等)。同时,在稳定的业务迭代过程中,把常规的业务场景(譬如列表、详情)抽象成对应的模板,也可以大大减少我们开发中的重复代码编写,是一个很不错的解决办法。
* **方案**:
模板的工作主要是创建文件,写入一些内置的代码。除了IDE插件之外,还有不少模板语言可以用来做这个事情。Android Studio本身就已经内置了两种模板创建方案,分别是:
* 基于[Apache Velocity](http://velocity.apache.org/engine/devel/user-guide.html#Velocity_Template_Language_VTL:_An_Introduction)模板语言的**File and Code Templates**
入口在File--New--Edit File Templates...菜单,其中已经内置了大量语言的模板支持,譬如C++,Html,Java,Kotlin等等。甚至可以仔细区分到文件头部、方法实现等部分的模板,但是内置的编辑器无法提供一次创建多个文件的功能,所以暂时不选这种方案。它仍然可以帮我们做很多事。
* 基于[FreeMaker](https://freemarker.apache.org/)的Android模板
功能入口也是在File--New下面(如下图),可以看到Android Studio已经内置了不少代码模板,Activity、Fragment、UI、Service等等,而且可以一次创建多个文件,这就是我们想要的。看了一下发现Android Studio并没有提供内置的编辑器。[官方指南](https://developer.android.google.cn/studio/projects/templates)的介绍也只限于使用,搜索一番后发现了模板的文件目录,通过复制和修改这些模板文件,就可以达成创建新模板的目标,下面是一些简要介绍。
<img src="https://developer.android.google.cn/studio/images/projects/templates-menu_2-2-beta_2x.png" width="232" />
## Android ADT Template
### 名称介绍
基于freemaker模板引擎的这套方案叫Android ADT Template,起码曾经是叫这个名字,这是我从一个[古老的网站](https://www.i-programmer.info/professional-programmer/resources-and-tools/6845-android-adt-template-format-document.html)上看到的。当时它还是在Eclipse的adt插件的一部分,现在已经是Android Studio内置的android support pulsgin插件的功能。下面的介绍基本来自于上述网站。
### 目录结构
模板的文件位于%Android Studio%\plugins\android\lib\templates下,一套模板实际上是一系列文件组成的,包括必要的文件 `template.xml` 和 `recipe.xml.ftl`,以及模板代码文件等等。一个常规的模板目录应该是这样的:
* MVP Activity (模板根目录)
* [template.xml](#template) -- 模板描述文件,输入参数设置等等,下面详细介绍
* [recipe.xml.ftl](#recipe) -- 要生成、合成的文件说明,路径、来源模板等等
* [global.xml.ftl](#global) -- 全局参数定义
* template.png -- IDE中显示的模板图片
* root/ -- 代码模板根目录
* AndroidManifest.xml.ftl
* res/ -- 对应生成的资源文件目录
* ...
* src/ -- 对应生成的代码模板目录
* app_package/
* MVPActivity.java.ftl
* ...
#### <span id="template">template.xml</span>
`template.xml`是一套模板必不可少的定义文件,它定义了模板的名称、分组、描述、适用范围和用户输入等等,同时指定模板的生成脚本`recipe.xml.ftl`和全局参数文件`global.xml.ftl`。没有它,AS就无法识别当前目录是一个模板,也就无法为模板创建入口等等。下面是一个`template.xml`的示例:
<?xml version="1.0"?>
<template
format="5"
revision="5"
name="MVP List Fragment"
minApi="9"
minBuildApi="14"
description="创建一个列表Fragment和相关文件">
<category value="Healthmall" />
<formfactor value="Mobile" />
<parameter
id="entity"
name="列表实体名称"
type="string"
constraints="class|unique|nonempty"
default="Product"
help="生成的viewmodel的名称" />
<parameter
id="adapterName"
name="Adapter名称"
type="string"
constraints="class|unique|nonempty"
suggest="${entity}Adapter"
default="ProductAdapter"
help="生成的Adapter的名称" />
...
<!-- 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>
其中:
* **<template\>**
模板根目录:
* name : 模板名称,窗口中显示在选择分类右侧
* minApi : 最低支持的API版本,低于此版本无法创建模板
* minBuildApi : 最低构建版本,同上
* description : 模板描述
* **<category\>**
模板分类,如上图,用于区分模板类型。
* **<parameter\>**
用户定义的输入参数:
* id : 参数的标识,在FreeMaker文件`ftl`模板中可以通过语法`${id}`获得对应的输入值,这个定义是全局的
* name : 参数名称
* type : 参数类型,如果是string,在窗口中是一个输入框,如果是boolean,则是一个选择的box
* constraints : 约束
* suggest : 建议值,可以通过表达式,获得其他输入参数进行拼接。例如示例中的`adapterName`,它的值会跟随`entity`的输入而变化为对应的`___Adapter`
* defalut : 默认值
* help : 提示,描述信息
* **<thumbs\>**
封面图片
* **<globals\>**
全局参数定义文件指定
* **<execute\>**
脚本执行文件指定,即`recipe.xml.ftl`文件
#### <span id="recipe">recipe.xml.flt</span>
此文件配置了模板执行的具体指令操作,譬如是复制一份文件到目标位置,或者是使用模板生成目标代码等等。此文件可以识别global.xml.ftl或者template.xml定义的一些参数,从而进行一些分支操作,譬如区分是否生成kotlin代码或者是java代码。以下是具体的指令描述:
* **<copy\>**
copy指令会把`from`属性中的文件全部拷贝到你项目中,如果你不指定`to`属性,那么默认会复制到与当前文件目录在项目对应的路径,其中root文件夹对应的是当前module的根目录。注意,与instantiate指令不同的是,copy指令复制的文件不会经过FreeMaker处理,复制模板ftl文件到项目中,也同样还是ftl文件。
* **<instantiate\>**
跟copy指令类似,不过ftl文件会经过FreeMaker,所有表达式会进行求值,并且生成的文件不会带.ftl。
* **<open\>**
在IDE中打开目标文件。
* **<merge\>**
把当前定义的模板文件的内容合并到项目中已存在的指定文件里面,常见的就是AndroidManifest.xml的文件合并和strings.xml的合并。
#### <span id="global">global.xml.ftl</span>
全局参数定义文件,定义的参数可以在脚本文件中识别,但在template.xml没法使用。
#### root\
root文件夹,代表着项目中的根目录,存放着要生成的资源、代码模板文件等等。其中的文件路径都代表着实际项目里面对应的位置。有一点不同的是,我们可以使用`src/app_package/`这一约定字符串来替代可变的实际包名。
### 封装的函数
FreeMaker本身已经提供了大量的内置的模板函数(详见[此处](https://freemarker.apache.org/docs/ref.html))(内置的这些方法无法用于`template.xml`中,没有找到原因,但插件额外提供的几个方法可以),用于操作字符、时间、数字等等,也提供了大量的指令譬如inclub、if else、function等等,使模板可以完成更加复杂的功能。
除此之外,插件还提供了几个更适用于Android世界的字符串操作方法,这里我只列一下,具体作用可以看对应的链接介绍:
* [activityToLayout](https://www.i-programmer.info/professional-programmer/resources-and-tools/6845-android-adt-template-format-document.html#toc_activitytolayout)
* camelCaseToUnderscore
* escapeXmlAttribute
* escapeXmlText
* escapeXmlString
* extractLetters
* ...
### 使用方式
添加一个新的模板并且使用的步骤:
1. 把模板文件夹放到%Android Studio%\plugins\android\lib\templates下;
2. 重启Android Studio;
3. 右击新建文件即可找到xin添加的模板。
## 我们项目中的模板
上面是当前的模板方案的一些技术性介绍。通过以上介绍,再参照现存的模板,相信各位都可以把一个切实的模板的想法实现下来。接下来的事情,就是如何抽象业务,形成一个通用的模板。这些在实际业务迭代中大家自然感受到,我就不展开去讨论了。
由于我们项目中有MVP和MVPVM两种基础架构,所以我们的模板会尽量覆盖到这两种架构,同时我们的项目也是Java和Kotlin混编的项目,所以单个模板需要覆盖的面比较大。目前我们已经有了部分模板,下面将会介绍这些模板的作用,以及我们未来的一些计划。也希望大家有自己的想法的话,可以实现出来并且补充。
* **MVP Activity**
创建一个继承自`AbstractMvpActivity`的空白页面,并根据用户选择生成对应的MVP接口和实现,和对应的布局资源文件。创建时的输入项是这样的:
![](https://s22.postimg.cc/vrfa3qmzl/template4.png)
与自带模板不同的地方,就是多出了*标题、是否需要下拉刷新、是否需要实现Presenter* 等选/输入项,**如果下面模板再此出现,则不再另作介绍**:
* 标题:输入项,显示在`AppToolbar`标题栏上面的标题
* 是否需要下拉刷新
可选项。但无论是否勾选,生成的layout布局文件都会带有我们项目的加载过程控件`LoadingView`,保证在页面数据请求的时候有我们特有的加载动画(在Presenter中可以通过`view.startLoading()`,`view.stopLoading()来进行动画的开启和停止`)。取消勾选的话,只是生成的模板代码中会禁用掉下拉刷新功能。
* 是否需要实现Presenter
可选项。有时候只是需要创建一个简单的静态页面,就没有必要再创建MVP相关的接口类。这种情况下,取消勾选此选项,则不生成对应的MVP接口。
模板会根据选择的语言(Java/Kotlin)来生成对应的代码,一般情况下会有以下文件(以Kotlin为例):
* `XxxActivity.kt`
* `XxxConstract.kt`
* `XxxPresenter.kt`
* `activity_xxx.xml`
并且会在`AndroidManifest.xml`中自动注册。创建的文件会根据自己的功能划分到目前我们项目的各个功能package下面,例如presenter,view,model等等。
* **MVP Fragment**
与MVP Activity类似,只不过生成的页面是继承自`AbstractMvpFragment`,并且在生成的布局文件中没有标题栏,所以创建时也没有标题栏输入框。
* **MVP List Activity**
创建一个能够分页展示的列表数据展示页及相关文件。跟上面的模板不一样的就是,这里生成的代码几乎已经拥有一个完整的业务流程(请求加载列表--成功转换接口数据为页面数据--展示到页面),除了真实的数据接口。这个模板代表大部分目前我们项目里面的列表页展示业务,所以在大家需要创建一个常规的分页列表页面时可以首先考虑到使用此模板。里面会生成包括adapter、viewmodel等各式代码文件,也会生成一些接口方法及实现代码,大家可以把这些代码理解为一种**代码规范**,包括各种命名和使用流程等等。希望大家即使脱离模板自己手写代码时,也尽量参照这些代码来进行编写。
下面是创建此模板的用户输入,参照了自带的List Fragment模板:
![](https://s22.postimg.cc/mc8licc41/template5.png)
* 列表实体名称:
业务的实体名称,也是所有代码文件的基础名称。例如一个商品列表页,业务实体名称为Product(产品),于是生成的会有接口数据ProductInfo,页面数据ProductVM,页面适配器ProductAdapter等等文件,也会生成`getProductList()`,`showProductList()`等相关方法。但不会有一个具体的类叫做Product.java,所以这只是一个抽象的名称。
* Adapter名称:
适配器名称,默认根据列表实体名称进行更改,也可以自定义。
* item布局名称:
生成的adapter所需要的item布局。
下面是生成的文件列表(以Kotlin为例):
* `XxxListActivity.kt`
* `XxxAdapter.kt`
* `XxxListConstract.kt`
* `XxxListPresenter.kt`
* `XxxInfo.kt` : 接口数据bean
* `XxxVM.kt` : 页面数据viewmodel
* `activity_xxx_list.xml`
* `item_layout_xxx.xml`
额外的一些业务代码是这样的:
接口:
class ProductListContract {
abstract class Presenter(view: View) : BasePresent<View>(view) {
/**
* 获取列表
*/
abstract fun getProductList(refresh: Boolean)
}
interface View : BaseView {
/**
* 显示列表数据并停止加载动画
*
* @param refresh 是否刷新
* @param list 列表的视图模型
*/
fun showProductList(refresh: Boolean, list: List<ProductVM>?)
}
}
Presenter实现:
class ProductListPresenter(view: ProductListContract.View, var dataSource: DataSource) : ProductListContract.Presenter(view) {
var page = 1
override fun onStart() {
view.startLoading(false)
getProductList(true)
}
override fun getProductList(refresh: Boolean) {
// 如果是刷新页面,则page下标重置
if (refresh) {
page = 1
}
// 数据请求
dataSource.getProductList(page, ConstantValue.PAGE_COUNT, object : NetCallBackListener<List<ProductInfo>>() {
override fun onFinish() {
}
override fun onSuccess(msg: String?, list: List<ProductInfo>?) {
// 接口数据转换为页面数据
view.showProductList(refresh, list?.map { ProductVM.convertToViewModel(it) })
page++
}
override fun onFailure(msg: String?) {
// 失败后停止动画,并且根据状态显示对应的提示。列表页中成功后动画自动停止,可以不单独操作。
view.stopLoading(LoadingView.FAILURE, msg)
}
})
}
}
页面实现:
class ProductListActivity : AbstractMvpActivity<ProductListContract.Presenter>(), ProductListContract.View {
private lateinit var adapter: ProductAdapter
override fun setContentView() = R.layout.activity_product_list
override fun bindViews(bundle: Bundle?) {
setToolbarDefaultBackAction(toolbar)
initRefreshableList()
}
// dataSource 是接口,model 层
override fun newPresent(): ProductListContract.Presenter {
return ProductListPresenter(this, DataSourceImpl(application))
}
override fun showProductList(refresh: Boolean, list: List<ProductVM>?) {
if (refresh) {
adapter.replace(list)
} else {
adapter.addAll(list)
}
}
private fun initRefreshableList() {
val list = loading_view
adapter = ProductAdapter(mContext, R.layout.item_layout_product, null)
adapter.setOnItemClickListener(object : BaseLoadMoreViewAdapter.OnItemClickListener<ProductVM?>() {
override fun onItemClick(holder: BaseRecViewHolder?, data: ProductVM?, position: Int) {
// click item
}
})
list.setAdapter(adapter)
list.setOnLoadMoreListener {
// 加载下一页
presenter.getProductList(false)
}
list.setOnRefreshListener {
// 下拉刷新
presenter.getProductList(true)
}
}
}
* **MVP List Fragment**
大体与MVP List Activity一致,只是主体页面实现换成Fragment。
## 计划中的模板
以上的是已经开发完成,大家正在使用的一些模板。目前还有一系列计划中的模板,包括:
* 上述模板的MVPVM架构对应的实现
* 带有头部的(可选悬停)列表模板
* CoordinatorLayout+AppBarLayout+ViewPage+Fragment模板
欢迎有想法的同学可以帮忙实现它们(做起来还是挺累的),或者开发一些额外的业务模板
**一切为了偷懒!**