# 基于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接口和實現,和對應的布局資源文件。創建時的輸入項是這樣的:

與自帶模板不同的地方,就是多出了*標題、是否需要下拉刷新、是否需要實現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模板:

* 列表實體名稱:
業務的實體名稱,也是所有代碼文件的基礎名稱。例如一個商品列表頁,業務實體名稱為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模板
歡迎有想法的同學可以幫忙實現它們(做起來還是挺累的),或者開發一些額外的業務模板
**一切為了偷懶!**