由于項(xiàng)目用上了 mvp 架構(gòu),基本上一個(gè)頁(yè)面就至少需要新創(chuàng)建6個(gè)類(lèi),分別是 model view presenter 的接口以及其對(duì)應(yīng)的實(shí)現(xiàn)類(lèi),再加上使用 dagger 的話就要更多了,所以這時(shí)候 android studio 的自定義模板就派上用場(chǎng)了,可以節(jié)省很多編寫(xiě)模板代碼的重復(fù)性工作
那么該如何入手呢?相信大部分用過(guò) as 的人以及使用過(guò)一些自帶的模板樣式了,這些自帶的模板就是最好的參照目標(biāo)了,廢話不多說(shuō),先看看它的結(jié)構(gòu)
1.模板結(jié)構(gòu)
這里參照的是 empty activity

它的位置就在 as的安裝目錄(mac的話右鍵as應(yīng)用-> 顯示包內(nèi)容 -> content 里就是了)/plugins/android/lib/templates/activities,

這里簡(jiǎn)單做個(gè)總結(jié):
- template:主要是給生成頁(yè)面提供一些需要用戶傳入的參數(shù)
- global.xml.ftl:主要提供一些全局參數(shù)
- recipe.xml.ftl:主要用于生成實(shí)際需要的代碼,資源文件等
- root文件夾:包含 project 中一系列屬性文件的模板
root 底下還有一些相關(guān)文件介紹
- build.gradle.ftl:project 的 build.gradle 模板,如果需要添加 maven 庫(kù)的地址,就在這里添加
- gradle.properties.ftl:project 的 gradle.properties 的模板,如果需要添加工程的一些公用屬性(版本號(hào)\版本名\簽名信息\私有 maven 庫(kù)的 group 和 id 信息等)就在這里面修改
- local.properties.ftl:project 的 local.properties.ftl 模板,里面指定 SDK的路徑,如果設(shè)置好環(huán)境變量,創(chuàng)建工程的時(shí)候就動(dòng)態(tài)生成指定的路徑,不需要手動(dòng)修改
- project_ignore:project 的.gitingore 模板,里面可以增刪版本管理需要過(guò)濾的文件夾\文件
- settings.gradle.ftl:project 的 settings.gradle 模板,里面可以指定真?zhèn)€工程需要編譯的 module,這個(gè)建議不要修改,可以在工程中手動(dòng)修改
1.1首先是 template.xml 文件,打開(kāi)后的主要內(nèi)容如下
<?xml version="1.0"?>
<template
format="5"
revision="5"
name="Empty Activity"
minApi="9"
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" />
<!-- 省略了若干個(gè) parameter,和上面那個(gè)差不多 -->
<!-- 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>
其中
1.<template>的 name 屬性,對(duì)應(yīng)新建 Activity 時(shí)顯示的名字
2.<category>對(duì)應(yīng) New 的類(lèi)別為 Activity

現(xiàn)在來(lái)詳解 parameter標(biāo)簽 的屬性
- id:唯一表示,最終通過(guò)這個(gè)屬性來(lái)獲取輸入值(分為input 和 checkbox)
- name:相當(dāng)于 hint 了
- type:屬性的類(lèi)型,分為 String 和 Boolean
- constraints:填寫(xiě)值的約束
- suggest:建議值
- default:默認(rèn)值
- visibility:是否顯示(一般就是根據(jù)其他類(lèi)型為 checkbox 的 parameter 來(lái)確定了),例如上圖的 layoutname,只有 generateLayout 為 true 時(shí)才顯示


-
help:鼠標(biāo)懸浮在該 parameter 時(shí)顯示的幫助提示
help 屬性的效果
然后是 thumbs 標(biāo)簽,也沒(méi)啥,就是個(gè)縮略圖罷了

最后還有兩個(gè)標(biāo)簽,引用了外部文件,也是下面要講的內(nèi)容
<globals file="globals.xml.ftl" />
<execute file="recipe.xml.ftl" />
1.2 globals.xml.ftl
<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>
里面定義的是一些全局變量,方便其他文件可以引用這里的值,引用的方式是&{id的值}
最后可以看到還引用了另外一個(gè) ftl,這也說(shuō)明了這個(gè)文件里定義的屬性同時(shí)也可以被其他模板引用
1.3 recipe.xml.ftl
<?xml version="1.0"?>
<recipe>
<#include "../common/recipe_manifest.xml.ftl" />
<#if generateLayout>
<#include "../common/recipe_simple.xml.ftl" />
<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>
跳過(guò)兩個(gè) include 引入的 ftl,先介紹能看到的標(biāo)簽
- open:在代碼生成后,打開(kāi)指定的文件,這里寫(xiě)了兩個(gè) open,所以創(chuàng)建了一個(gè) activity 后,就會(huì)把 activity 的 java 文件和layout.xml 同時(shí)打開(kāi)
- instantiate:就是把模板轉(zhuǎn)換成實(shí)際目標(biāo)文件的一個(gè)操作了,from 指定的是模板文件,to 指定的是生成文件,后面再詳細(xì)介紹
然后可以看到前面還 include 了兩個(gè) ftl,實(shí)際上代表的就是 menifest 和 layout 的相關(guān)操作,下面是 recipe_manifest.xml.ftl 的內(nèi)容
<recipe folder="root://activities/common">
<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" />
</recipe>
這里又看到一個(gè)新的標(biāo)簽merge,字面意義就是合并,也就是把模板文件合并到項(xiàng)目中已經(jīng)存在的對(duì)應(yīng)文件中,這里是合并了 AndroidManifest.xml 和 string.xml
recipe中還有一個(gè)比較常見(jiàn)的標(biāo)簽,這個(gè)模板里沒(méi)看到
<copy from="root/res/drawable-hdpi" to="${escapeXmlAttribute(resOut)}/drawable-hdpi" />
- copy :從root中copy文件到我們的目標(biāo)目錄,比如我們的模板Activity需要使用一些圖標(biāo),那么可能就需要使用copy標(biāo)簽將這些圖標(biāo)拷貝到我們的項(xiàng)目對(duì)應(yīng)文件夾。
2.代碼生成的過(guò)程
模板里的文件基本都是 ftl 結(jié)尾的, 這里首先需要要解釋一下 ftl 的概念
ftl是FreeMarker Template Language的縮寫(xiě),它是簡(jiǎn)單的,專用的語(yǔ)言, 不是 像PHP那樣成熟的編程語(yǔ)言。 那就意味著要準(zhǔn)備數(shù)據(jù)在真實(shí)編程語(yǔ)言中來(lái)顯示,比如數(shù)據(jù)庫(kù)查詢和業(yè)務(wù)運(yùn)算, 之后模板顯示已經(jīng)準(zhǔn)備好的數(shù)據(jù)。在模板中,你可以專注于如何展現(xiàn)數(shù)據(jù), 而在模板之外可以專注于要展示什么數(shù)據(jù)。
而AS中的這些模板是就是通過(guò)這個(gè)FreeMarker模板引擎創(chuàng)建的
FreeMarker 是一款 模板引擎: 即一種基于模板和要改變的數(shù)據(jù), 并用來(lái)生成輸出文本(HTML網(wǎng)頁(yè),電子郵件,配置文件,源代碼等)的通用工具。 它不是面向最終用戶的,而是一個(gè)Java類(lèi)庫(kù),是一款程序員可以嵌入他們所開(kāi)發(fā)產(chǎn)品的組件。

3.簡(jiǎn)單的 freemarker 語(yǔ)法
- 1.插入屬性
定義好一個(gè)屬性,在模板文件中使用${定義好的屬性名稱},即可完成替換
- 2.if 語(yǔ)法
例如前面在 recipe.xml.ftl 里看到的, 這個(gè)generateLayout 是再 template 中定義的 boolean 的 parameter<#if generateLayout> <#include "../common/recipe_simple.xml.ftl" /> <open file="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml" /> </#if>
下面以 Empty Activity模板中的 SimpleActivity.java.ftl 為例子
package ${packageName}; import ${superClassFqcn}; import android.os.Bundle; <#if includeCppSupport!false> import android.widget.TextView; </#if> public class ${activityClass} extends ${superClass} { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); <#if generateLayout> setContentView(R.layout.${layoutName}); </#if> <#include "../../../../common/jni_code_usage.java.ftl"> } <#include "../../../../common/jni_code_snippet.java.ftl"> }
可以看到模板中有很多變量需要替換后才能生成為最終需要的代碼,這些變量一般來(lái)自 globals.xml.ftl 中預(yù)先定義好的變量以及 template 中需要用戶輸入的變量,經(jīng)過(guò) recipe.xml.ftl 中的instantiate標(biāo)簽指定生成的路徑即可完成這個(gè)過(guò)程
4.具體示例
最近的項(xiàng)目用到 mvp 和 dagger(這里就不細(xì)談 dagger 的用法了),所以每個(gè)頁(yè)面要多寫(xiě)很多接口以及實(shí)現(xiàn)類(lèi),下面是項(xiàng)目的分包:

- contract:定義了一個(gè)頁(yè)面的 presenter 和 view 的接口,放在contract 里是為了方便查看
- di.component.presenter:用于往目標(biāo)類(lèi)注入 presenter
- di.module:提供presenter所依賴的組件
- model.event:model的接口
- model.impl:model 的實(shí)現(xiàn)類(lèi)
- presenter:contract 的 presenter 實(shí)現(xiàn)類(lèi)
4.1 模板代碼分析
下面分析一下,編寫(xiě)一個(gè)具體業(yè)務(wù)要生成哪些模板代碼,例如要做一個(gè)登錄的業(yè)務(wù)
4.1.1 mvp 的部分
首先要定義的是Contract, 包含整個(gè)業(yè)務(wù)邏輯與頁(yè)面顯示
public interface LoginContract {
// Contract 中肯定是要包括 View 和 Presenter 的
interface View {
// 具體的方法,這部分不是模板能解決的
void showLoginSuccess(UserInfo info);
void showLoginFailed(Throwable e);
}
interface Presenter {
// 具體的方法,這部分不是模板能解決的
void loginIn(String account, String password);
}
}** View 的實(shí)現(xiàn),一般就是讓 activity 或者 fragment 實(shí)現(xiàn) LoginContract.View 了,這部分不是模板能解決的,就不寫(xiě)了**
然后是 Presenter 的實(shí)現(xiàn),當(dāng)然實(shí)現(xiàn) LoginContract.Presenter 即可
//項(xiàng)目有Presenter 的基類(lèi)的話模板里還需要添加繼承
public class LoginPresenter extends BasePresenter implement LoginContract.Presenter {
// 接口定義的方法,也不是模板能解決的
public void loginIn(String account, String password) {
// 具體的實(shí)現(xiàn)代碼
}
}
4.1.2 Presenter的部分
這里presenter 的具體實(shí)現(xiàn)實(shí)際上也是包含很多重復(fù)代碼的
首先,Presenter 里肯定需要持有 上面定義的Contract.View 的引用,這樣才能再邏輯處理結(jié)束后回調(diào) View 層代碼
-
然后,Presenter 也需要持有 Model層的引用去處理數(shù)據(jù),一般 Model 層也是需要定義接口的,所以又多了兩個(gè)類(lèi):LoginModel 和 LoginModelImpl
public interface LoginModel { void login(String account, String password); } public class LoginModel extends BaseModel implement LoginModel { public void login(String account, String password) { // 具體代碼實(shí)現(xiàn) } }
修改后的 Presenter 代碼為
public class LoginPresenter extends BasePresenter implement LoginContract.Presenter {
private LoginContract.View mView;
private LoginModel mModel;
public LoginPresenter(LoginContract.View view, LoginModel model) {
mView = view;
mModel = model;
}
// 接口定義的方法,也不是模板能解決的
public void loginIn(String account, String password) { // 具體的實(shí)現(xiàn)代碼 }
}
由于一般的業(yè)務(wù)都是要通過(guò)請(qǐng)求或者本地?cái)?shù)據(jù)庫(kù)來(lái)處理的,所以這里抽取父類(lèi) BaseModel,項(xiàng)目里使用了 GreenDao 和 Retrofit,所以 BaseModel依賴于DaoMaster.DevOpenHelper和 Retrofit 兩個(gè)對(duì)象
public class BaseModel {
protected final Retrofit retrofit;
protected final DaoMaster.DevOpenHelper dbOpenHelper;
public BaseModel(DaoMaster.DevOpenHelper helper, Retrofit retrofit) {
this.dbOpenHelper = helper;
this.retrofit = retrofit;
}
}
修改后的 LoginModelImpl的代碼為
public class LoginModelImpl extends BaseModel implement LoginModel {
public LoginModelImpl(DaoMaster.DevOpenHelper helper, Retrofit retrofit) {
super(helper, retrofit);
}
public void login(String account, String password) {
// 具體代碼實(shí)現(xiàn)
}
}
到這里登錄業(yè)務(wù)的 P層和 M 層代碼基本就寫(xiě)完了,一共需要 LoginContract/ LoginPresenter/LoginModel/LoginModelImpl四個(gè)文件
4.1.3 dagger 的部分
首先這里說(shuō)一下 dagger 的好處,簡(jiǎn)單來(lái)說(shuō),dagger 就是將目標(biāo)類(lèi)與其依賴的對(duì)象的實(shí)例化過(guò)程隔離開(kāi)來(lái),例如這里的 LoginPresenter,一般在 activity 或者 fragment 中實(shí)例化
public class LoginActivity extends Activity implement LoginContract.View {
private LoginPresenter mPresenter;
public void onCreate(Bundle saveInstanceState) {
super.onCrate(saveInstanceState);
// 省略DaoMaster.DevOpenHelper 和 Retrofit 的實(shí)例化
....
mPresenter = new LoginPresenter(this, new LoginModelImpl(helper, retrofit));
}
void showLoginSuccess(UserInfo info){...}
void showLoginFailed(Throwable e){...}
}
實(shí)際上寫(xiě)這種 new 的代碼是很 low 的,萬(wàn)一 LoginPresenter 的構(gòu)造函數(shù)被修改了,就需要修改 LoginActivity 的代碼,如果這個(gè) LoginPresenter 到處都是的話,那就悲催了...
所以dagger就是為了解決這個(gè)問(wèn)題而存在的,dagger 是一種依賴注入, 此處 LoginActivity 依賴于 LoginPresenter, dagger 可以把 LoginPresenter 的實(shí)例化放在一個(gè)獨(dú)立的模塊中去執(zhí)行,而 LoginActivity 不必關(guān)心也不知曉 Presnter 的實(shí)例化過(guò)程,這樣上面的問(wèn)題就迎刃而解了.至于 dagger 的用法這里就忽略了
接下來(lái)講使用 dagger 所需要?jiǎng)?chuàng)建的類(lèi)
mPresenter = new LoginPresenter(this, new LoginModelImpl(helper, retrofit));
從這句代碼就可以看出 LoginPresenter依賴于兩個(gè)對(duì)象,一個(gè)是 View 接口,另一個(gè)是 LoginModel 接口,修改 LogingPresenter:
public class LoginPresenter extends BasePresenter implement LoginContract.Presenter {
private LoginContract.View mView;
private LoginModel mModel;
@Inject
public LoginPresenter(LoginContract.View view, LoginModel model) {
mView = view;
mModel = model;
}
// 接口定義的方法,也不是模板能解決的
public void loginIn(String account, String password) { // 具體的實(shí)現(xiàn)代碼 }
}
這里給 LoginPresenter 的構(gòu)造函數(shù)添加 @Inject 注解,這樣 dagger 就能判斷這是一個(gè)可用依賴注入實(shí)例化的目標(biāo)
接下來(lái),LoginPresenter 又有進(jìn)一步的依賴,由于傳入的參數(shù)都是接口,是不可能用 @Inject 標(biāo)注在構(gòu)造函數(shù)的了,所以這里又需要 dagger 中的Module提供實(shí)現(xiàn)類(lèi)的對(duì)象,本著 m 層和 v 層分離的原則,這里就需要兩個(gè) Module
@Module
public class LoginViewModule {
LoginContract.View view;
public LoginViewModule(LoginContract.View view) {
this.view = view;
}
@Provide
public LoginContract.View provideLoginView() {
return view;
}
}
@Module
public class LoginModelModule {
Context context;
public LoginModelModule(Context context) {
this.context = context;
}
@Provide
public LoginModel provideLoginModel() {
// 省略DaoMaster.DevOpenHelper 和 Retrofit 的實(shí)例化
....
return new LoginModelImpl(helper, retrofit);
}
}
最后就是 Component 注入器了
@Component(dependencies = {LoginModelModule.class, LoginViewModule.class})
public interface LoginPresenterComponent {
void inject(LoginActivity activity);
}
到這里 dagger 部分的代碼也就完了,下面開(kāi)始編寫(xiě)自定義模板,這里列舉一下所有需要的模板代碼
- Contract 類(lèi)
package ${packageName}.contract;
public interface ${businessName}Contract {
interface View {}
interface Presenter{}
}
- Presneter 實(shí)現(xiàn)類(lèi)
package ${modulePackageName}.presenter;
import ${modulePackageName}.contract.${businessName}Contract;
import ${modulePackageName}.model.event.${businessName}Model;
import ${parentPresenterPackage}.${basePresenterClassName};
import javax.inject.Inject;
public class ${businessName}Presenter extends ${basePresenterClassName} implements ${businessName}Contract.Presenter {
private ${businessName}Contract.View view;
private ${businessName}Model model;
@Inject
public ${businessName}Presenter(${businessName}Contract.View view, ${businessName}Model model) {
this.view = view;
this.model = model;
}
}
- Model 接口
package ${packageName}.model.event;
public interface ${businessName}Model {}
- Model 實(shí)現(xiàn)類(lèi)
package ${packageName}.model.impl;
import ${packageName}.model.event.${businessName}Model;
import ${daoPackage}.DaoMaster;
import ${projectPackage}.model.impl.${baseModelClassName};
import retrofit2.Retrofit;
public class ${businessName}ModelImpl extends ${baseModelClassName} implements ${businessName}Model {
public ${businessName}ModelImpl(DaoMaster.DevOpenHelper dataBaseHelper, Retrofit retrofit) {
super(dataBaseHelper, retrofit);
}
}
- Model 接口 的 Module
package ${packageName}.di.module.model;
import android.content.Context;
import ${daoPackage}.DaoMaster;
import ${packageName}.model.event.${businessName}Model;
import ${packageName}.model.impl.${businessName}ModelImpl;
import retrofit2.Retrofit;
import dagger.Module;
import dagger.Provides;
@Module
public class ${businessName}ModelModule {
Context context;
public ${businessName}ModelModule(Context context) {
this.context = context;
}
@Provides
public ${businessName}Model provide${businessName}Model() {
// 省略DaoMaster.DevOpenHelper 和 Retrofit 的實(shí)例化
....
return new ${businessName}ModelImpl(helper, retrofit);
}
}
- View層接口的 Module
package ${packageName}.di.module.view;
import ${packageName}.contract.${businessName}Contract;
import dagger.Module;
import dagger.Provides;
@Module
public class ${businessName}ViewModule {
${businessName}Contract.View view;
public ${businessName}ViewModule(${businessName}Contract.View view) {
this.view = view;
}
@Provide
public ${businessName}Contract.View provide${businessName}View() {
return view;
}
}
- Presenter實(shí)例的注入器
package ${packageName}.di.component.presenter;
import ${packageName}.di.module.view.${businessName}ViewModule;
import @{packageName}.di.module.model.${businessName}ModelModule;
import dagger.Component;
@Component(dependencies = {${businessName}ViewModule.class, ${businessName}ModelModule.class})
public interface ${businessName}PresenterComponent {
void inject(t target);
}
模板代碼準(zhǔn)備好之后就可以開(kāi)始制作模板了
首先復(fù)制整個(gè) Empty Activity模板(推薦復(fù)制過(guò)來(lái)再修改的方式)
由于這個(gè)模板不涉及 activity 和manifest.xml 以及 layout,所以先刪掉相關(guān)的標(biāo)簽
先從template.xml開(kāi)始,刪掉沒(méi)用的 parameter,留下一個(gè) packageName,然后添加一個(gè)業(yè)務(wù)名稱,有這兩個(gè)就夠了
接著是設(shè)置 globals,設(shè)置一個(gè) srcOut
<global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
最后是配置 recipe.xml.ftl, 根據(jù)自己想要的包修改一下路徑即可, 只是簡(jiǎn)單的復(fù)制工作而已了
<?xml version="1.0"?>
<recipe>
<instantiate from="root/src/app_package/BusinessContract.java.ftl"
to="${escapeXmlAttribute(srcOut)}/contract/${businessName}Contract.java" />
<instantiate from="root/src/app_package/BusinessModel.java.ftl"
to="${escapeXmlAttribute(srcOut)}/model/event/${businessName}Model.java" />
<instantiate from="root/src/app_package/BusinessModelImpl.java.ftl"
to="${escapeXmlAttribute(srcOut)}/model/impl/${businessName}ModelImpl.java" />
<instantiate from="root/src/app_package/BusinessModelModule.java.ftl"
to="${escapeXmlAttribute(srcOut)}/di/module/model/${businessName}ModelModule.java" />
<instantiate from="root/src/app_package/BusinessViewModule.java.ftl"
to="${escapeXmlAttribute(srcOut)}/di/module/view/${businessName}ViewModule.java"/>
<instantiate from="root/src/app_package/BusinessPresenter.java.ftl"
to="${escapeXmlAttribute(srcOut)}/presenter/${businessName}Presenter.java"/>
<instantiate from="root/src/app_package/BusinessPresenterComponent.java.ftl"
to="${escapeXmlAttribute(srcOut)}/di/component/presenter/${businessName}PresenterComponent.java"/>
</recipe>
5.遇到的一些坑
- 1.模板一旦有錯(cuò),as 跑起來(lái)就跪了,窗口關(guān)不掉只能強(qiáng)行關(guān)閉 as 再開(kāi)過(guò)
- 2.前面提到要留下這個(gè) packageName 本來(lái)想做成固定路徑的,但不是報(bào)錯(cuò)就是路徑不對(duì).另外不指定這個(gè) id 就沒(méi)辦法弄到當(dāng)前的路徑和包,不知道是為啥
- 3.這個(gè)是網(wǎng)上搜索的時(shí)候看到的,貌似自定義的模板會(huì)造成as 升級(jí)失敗,如果遇到,把這份模板剪切出來(lái),升級(jí)結(jié)束后再?gòu)?fù)制回去即可
