android studio 自定義模板

由于項目用上了 mvp 架構,基本上一個頁面就至少需要新創建6個類,分別是 model view presenter 的接口以及其對應的實現類,再加上使用 dagger 的話就要更多了,所以這時候 android studio 的自定義模板就派上用場了,可以節省很多編寫模板代碼的重復性工作

那么該如何入手呢?相信大部分用過 as 的人以及使用過一些自帶的模板樣式了,這些自帶的模板就是最好的參照目標了,廢話不多說,先看看它的結構

1.模板結構

這里參照的是 empty activity

Empty Activity

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

模板的結構

這里簡單做個總結:

  • template:主要是給生成頁面提供一些需要用戶傳入的參數
  • global.xml.ftl:主要提供一些全局參數
  • recipe.xml.ftl:主要用于生成實際需要的代碼,資源文件等
  • root文件夾:包含 project 中一系列屬性文件的模板

root 底下還有一些相關文件介紹

  • build.gradle.ftl:project 的 build.gradle 模板,如果需要添加 maven 庫的地址,就在這里添加
  • gradle.properties.ftl:project 的 gradle.properties 的模板,如果需要添加工程的一些公用屬性(版本號\版本名\簽名信息\私有 maven 庫的 group 和 id 信息等)就在這里面修改
  • local.properties.ftl:project 的 local.properties.ftl 模板,里面指定 SDK的路徑,如果設置好環境變量,創建工程的時候就動態生成指定的路徑,不需要手動修改
  • project_ignore:project 的.gitingore 模板,里面可以增刪版本管理需要過濾的文件夾\文件
  • settings.gradle.ftl:project 的 settings.gradle 模板,里面可以指定真個工程需要編譯的 module,這個建議不要修改,可以在工程中手動修改

1.1首先是 template.xml 文件,打開后的主要內容如下

<?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" />
<!-- 省略了若干個 parameter,和上面那個差不多 -->
<!-- 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 屬性,對應新建 Activity 時顯示的名字
2.<category>對應 New 的類別為 Activity

頁面和屬于對照

現在來詳解 parameter標簽 的屬性

  • id:唯一表示,最終通過這個屬性來獲取輸入值(分為input 和 checkbox)
  • name:相當于 hint 了
  • type:屬性的類型,分為 String 和 Boolean
  • constraints:填寫值的約束
  • suggest:建議值
  • default:默認值
  • visibility:是否顯示(一般就是根據其他類型為 checkbox 的 parameter 來確定了),例如上圖的 layoutname,只有 generateLayout 為 true 時才顯示
generateLayout 為 false 時不顯示 Layout Name
generateLayout 為 true 時顯示 Layout Name
  • help:鼠標懸浮在該 parameter 時顯示的幫助提示


    help 屬性的效果

然后是 thumbs 標簽,也沒啥,就是個縮略圖罷了

thumbs

最后還有兩個標簽,引用了外部文件,也是下面要講的內容
<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的值}

最后可以看到還引用了另外一個 ftl,這也說明了這個文件里定義的屬性同時也可以被其他模板引用

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>

跳過兩個 include 引入的 ftl,先介紹能看到的標簽

  • open:在代碼生成后,打開指定的文件,這里寫了兩個 open,所以創建了一個 activity 后,就會把 activity 的 java 文件和layout.xml 同時打開
  • instantiate:就是把模板轉換成實際目標文件的一個操作了,from 指定的是模板文件,to 指定的是生成文件,后面再詳細介紹

然后可以看到前面還 include 了兩個 ftl,實際上代表的就是 menifest 和 layout 的相關操作,下面是 recipe_manifest.xml.ftl 的內容

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

這里又看到一個新的標簽merge,字面意義就是合并,也就是把模板文件合并到項目中已經存在的對應文件中,這里是合并了 AndroidManifest.xml 和 string.xml

recipe中還有一個比較常見的標簽,這個模板里沒看到
<copy from="root/res/drawable-hdpi" to="${escapeXmlAttribute(resOut)}/drawable-hdpi" />

  • copy :從root中copy文件到我們的目標目錄,比如我們的模板Activity需要使用一些圖標,那么可能就需要使用copy標簽將這些圖標拷貝到我們的項目對應文件夾。

2.代碼生成的過程

模板里的文件基本都是 ftl 結尾的, 這里首先需要要解釋一下 ftl 的概念

ftl是FreeMarker Template Language的縮寫,它是簡單的,專用的語言, 不是 像PHP那樣成熟的編程語言。 那就意味著要準備數據在真實編程語言中來顯示,比如數據庫查詢和業務運算, 之后模板顯示已經準備好的數據。在模板中,你可以專注于如何展現數據, 而在模板之外可以專注于要展示什么數據。

而AS中的這些模板是就是通過這個FreeMarker模板引擎創建的

FreeMarker 是一款 模板引擎: 即一種基于模板和要改變的數據, 并用來生成輸出文本(HTML網頁,電子郵件,配置文件,源代碼等)的通用工具。 它不是面向最終用戶的,而是一個Java類庫,是一款程序員可以嵌入他們所開發產品的組件。

代碼生成的簡單示意圖

3.簡單的 freemarker 語法

  • 1.插入屬性

    定義好一個屬性,在模板文件中使用${定義好的屬性名稱},即可完成替換

  • 2.if 語法
    例如前面在 recipe.xml.ftl 里看到的, 這個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"> }

可以看到模板中有很多變量需要替換后才能生成為最終需要的代碼,這些變量一般來自 globals.xml.ftl 中預先定義好的變量以及 template 中需要用戶輸入的變量,經過 recipe.xml.ftl 中的instantiate標簽指定生成的路徑即可完成這個過程

4.具體示例

最近的項目用到 mvp 和 dagger(這里就不細談 dagger 的用法了),所以每個頁面要多寫很多接口以及實現類,下面是項目的分包:

包結構
  • contract:定義了一個頁面的 presenter 和 view 的接口,放在contract 里是為了方便查看
  • di.component.presenter:用于往目標類注入 presenter
  • di.module:提供presenter所依賴的組件
  • model.event:model的接口
  • model.impl:model 的實現類
  • presenter:contract 的 presenter 實現類

4.1 模板代碼分析

下面分析一下,編寫一個具體業務要生成哪些模板代碼,例如要做一個登錄的業務

4.1.1 mvp 的部分
  • 首先要定義的是Contract, 包含整個業務邏輯與頁面顯示
    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 的實現,一般就是讓 activity 或者 fragment 實現 LoginContract.View 了,這部分不是模板能解決的,就不寫了**

  • 然后是 Presenter 的實現,當然實現 LoginContract.Presenter 即可
    //項目有Presenter 的基類的話模板里還需要添加繼承
    public class LoginPresenter extends BasePresenter implement LoginContract.Presenter {
    // 接口定義的方法,也不是模板能解決的
    public void loginIn(String account, String password) {
    // 具體的實現代碼
    }
    }

4.1.2 Presenter的部分

這里presenter 的具體實現實際上也是包含很多重復代碼的

  • 首先,Presenter 里肯定需要持有 上面定義的Contract.View 的引用,這樣才能再邏輯處理結束后回調 View 層代碼

  • 然后,Presenter 也需要持有 Model層的引用去處理數據,一般 Model 層也是需要定義接口的,所以又多了兩個類: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) {
          // 具體代碼實現
      }
    }
    

修改后的 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) { // 具體的實現代碼 }
}

由于一般的業務都是要通過請求或者本地數據庫來處理的,所以這里抽取父類 BaseModel,項目里使用了 GreenDao 和 Retrofit,所以 BaseModel依賴于DaoMaster.DevOpenHelper和 Retrofit 兩個對象

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) {
       // 具體代碼實現 
    }
}

到這里登錄業務的 P層和 M 層代碼基本就寫完了,一共需要 LoginContract/ LoginPresenter/LoginModel/LoginModelImpl四個文件

4.1.3 dagger 的部分

首先這里說一下 dagger 的好處,簡單來說,dagger 就是將目標類與其依賴的對象的實例化過程隔離開來,例如這里的 LoginPresenter,一般在 activity 或者 fragment 中實例化

public class LoginActivity extends Activity implement LoginContract.View {
  private LoginPresenter mPresenter;

  public void onCreate(Bundle saveInstanceState) {
    super.onCrate(saveInstanceState);
    // 省略DaoMaster.DevOpenHelper 和 Retrofit 的實例化
    ....
    mPresenter = new LoginPresenter(this, new LoginModelImpl(helper, retrofit));
  }

  void showLoginSuccess(UserInfo info){...} 
  void showLoginFailed(Throwable e){...}
}

實際上寫這種 new 的代碼是很 low 的,萬一 LoginPresenter 的構造函數被修改了,就需要修改 LoginActivity 的代碼,如果這個 LoginPresenter 到處都是的話,那就悲催了...

所以dagger就是為了解決這個問題而存在的,dagger 是一種依賴注入, 此處 LoginActivity 依賴于 LoginPresenter, dagger 可以把 LoginPresenter 的實例化放在一個獨立的模塊中去執行,而 LoginActivity 不必關心也不知曉 Presnter 的實例化過程,這樣上面的問題就迎刃而解了.至于 dagger 的用法這里就忽略了

接下來講使用 dagger 所需要創建的類
mPresenter = new LoginPresenter(this, new LoginModelImpl(helper, retrofit));
從這句代碼就可以看出 LoginPresenter依賴于兩個對象,一個是 View 接口,另一個是 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) { // 具體的實現代碼 }
}

這里給 LoginPresenter 的構造函數添加 @Inject 注解,這樣 dagger 就能判斷這是一個可用依賴注入實例化的目標

接下來,LoginPresenter 又有進一步的依賴,由于傳入的參數都是接口,是不可能用 @Inject 標注在構造函數的了,所以這里又需要 dagger 中的Module提供實現類的對象,本著 m 層和 v 層分離的原則,這里就需要兩個 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 的實例化 
    ....
    return new LoginModelImpl(helper, retrofit);
  }
}

最后就是 Component 注入器了

@Component(dependencies = {LoginModelModule.class, LoginViewModule.class})
public interface LoginPresenterComponent {
  void inject(LoginActivity activity);
}

到這里 dagger 部分的代碼也就完了,下面開始編寫自定義模板,這里列舉一下所有需要的模板代碼

  • Contract 類
package ${packageName}.contract;

public interface ${businessName}Contract {
  interface View {}
  interface Presenter{}
}
  • Presneter 實現類
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 實現類
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 的實例化  
    .... 
    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實例的注入器
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);
}

模板代碼準備好之后就可以開始制作模板了
首先復制整個 Empty Activity模板(推薦復制過來再修改的方式)

由于這個模板不涉及 activity 和manifest.xml 以及 layout,所以先刪掉相關的標簽
先從template.xml開始,刪掉沒用的 parameter,留下一個 packageName,然后添加一個業務名稱,有這兩個就夠了

接著是設置 globals,設置一個 srcOut
<global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />

最后是配置 recipe.xml.ftl, 根據自己想要的包修改一下路徑即可, 只是簡單的復制工作而已了

<?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.模板一旦有錯,as 跑起來就跪了,窗口關不掉只能強行關閉 as 再開過
  • 2.前面提到要留下這個 packageName 本來想做成固定路徑的,但不是報錯就是路徑不對.另外不指定這個 id 就沒辦法弄到當前的路徑和包,不知道是為啥
  • 3.這個是網上搜索的時候看到的,貌似自定義的模板會造成as 升級失敗,如果遇到,把這份模板剪切出來,升級結束后再復制回去即可
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容