丨版權說明 : 《玩轉Android Studio自定義模板插件-MVP模板爲例》於當前CSDN博客和乘月網屬同一原創,轉載請說明出處,謝謝。
得益於Android Studio強大的拓展功能,我們可以開發出適於自己項目的插件以滿足快速高效的開發需求,本文以MVP模板插件爲例
在日常開發中,新建一個如名爲ZModuleActivity的Activity:
新建後Android Studio(下文簡稱:AS)會依據我們編輯和勾選的一些要求生成具有一定代碼的.java和.xml的Activity和Layout文件,以及會幫我們在AndroidManifest.xml註冊好這個新建的Activity。
大體這樣:
像這樣 –>
一定程度上爲我們省下了很多創建文件,寫一些基礎代碼的時間,但是這遠遠沒有達到我們想要的效果。一個成熟的項目開發會有着自己的一套代碼架構,層次和模板,這就意味着每次新建Activity都要修改些代碼以沿用項目框架。
如MVP設計模式代碼結構可能如下,以YModuleActivity
爲例:
代碼可能像這樣 –>
AndroidManifest就不貼了,不是本文重點。
每新建一個Activity都要做大量無意義的基礎代碼工作,不知道你有沒有這樣的感覺:好煩啊,總是重複這些工作有意思嗎?能不能直接幫我們生成好啊?有的人在感嘆之餘默默地把之前已經做好的(如本文 的YModule
系列代碼:YModuleActivity,IYModule,YModulePresenter)相關文件給copy過來改文件名,改類名,改代碼,改註釋,刪冗餘代碼,最後變成類似YModule系列文件和代碼這樣。嗯,的確有省了不少事,但畢竟還是一連串的事兒。
既然有了困擾,就成了需求,既然是需求,就得滿足需求。好在AS允許我們開發這樣的代碼模板插件,那麼正式進入今天的主題。
打開AS安裝目錄(本文爲“android-studio”),依次打開:android-studio》plugins》android》lib》templates》activities 結果如下:
看到目錄名是否覺得眼熟?如果你還反應過來,沒關係,看下圖:
沒錯,這裏就是傳說中新建Activity的模板插件集中營,每個目錄即爲一個插件。
知道這些就好辦了,對EmptyActivity這個目錄進行令人窒息的Ctrl C
and Ctrl V
操作, 然後對這個副本重命名(本文爲“MVPActivity”)。你可能會問爲什麼一定是EmptyActivity?我可沒有強調,只是顧名思義,空蕩蕩的模板可以省去刪除一些冗餘的模板代碼操作,是吧?
打開MVPActivity目錄,你可看到如下文件結構:
下面先簡單介紹這幾個文件,然後開始寫模板
1. root目錄
用於存放源代碼模板和資源模板的.ftl文件,我們可以一路展開到最後一個目錄,可以看到文件SimpleActivity.java.ftl(如果AS版本較高的話還有SimpleActivity.kt.ftl,顧名思義,分別是java版和kotlin版的模板,本文僅專注java)。查看文件:
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">
}
很明顯是Activity的模板嘛,一看便懂,確認過眼神。
${xxx}
爲引用值,由前臺面板(如上文新建Activity的插件視圖面板)填寫或勾選的結果值賦值,xxx
可能爲變量,也可能爲函數返回值。<#if xxx!bool值>yyy</#if>
爲if判斷語句,與常見語言判斷語法規則並無區別,只是寫法的區別。<#include "xxx"
>導入某某文件的內容。類似於Android佈局中的標籤用法。
這裏稍微提一下:EmptyActivity的root目錄下只有源代碼模板目錄src,因爲模板比較簡單,其直接引用了EmptyActivity同級目錄common的root目錄下資源模板目錄res,下文會有提及,本文只講源代碼模板,不做深究。
2. template.xml
定義插件視圖面板的外觀,佈局,類似咱Android的layout.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="instantAppActivityHost"
name="Instant App URL Host"
type="string"
suggest="${companyDomain}"
default="instantapp.example.com"
visibility="isInstantApp!false"
help="The domain to use in the Instant App route for this activity"/>
<parameter
id="instantAppActivityRouteType"
name="Instant App URL Route Type"
type="enum"
default="pathPattern"
visibility="isInstantApp!false"
help="The type of route to use in the Instant App route for this activity" >
<option id="path">Path</option>
<option id="pathPrefix">Path Prefix</option>
<option id="pathPattern">Path Pattern</option>
</parameter>
<parameter
id="instantAppActivityRoute"
name="Instant App URL Route"
type="string"
default="/.*"
visibility="isInstantApp!false"
help="The route to use in the Instant App route for this activity"/>
<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="backwardsCompatibility"
name="Backwards Compatibility (AppCompat)"
type="boolean"
default="true"
help="If false, this activity base class will be Activity instead of AppCompatActivity" />
<parameter
id="packageName"
name="Package name"
type="string"
constraints="package"
default="com.mycompany.myapp" />
<!-- 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>
對照着上文的【Empty Activity(插件)的視圖面板】示意圖或者自己新建個Empty Activity呼出面板,差不多能看懂或猜測到一大半吧?
下面是對parameter模塊補充介紹:
- parameter標籤:表示一個參數模塊,爲變量的賦值而服務的視圖塊
- id:變量名,上文提到引用值
${xxx}
中的變量 - type:變量的數據類型,同java數據類型差不多,這裏常見的是string和boolean類型,boolean型模板面板以勾選框的形式顯示
- constraints:約束,主要有class(輸入框填寫需要創建的java類名),layout(輸入框填寫需要創建的佈局名),package(輸入框+下拉框填寫或選擇包名),unique(填寫內容不能與已有的java完整類名重名,或佈局名重名,重複會自動在後面加個2,3…以此類推,nonempty(不能爲空,輸入框不能不填內容)
- default:默認值,默認值會根據type類型自動填充到輸入框或決定勾選框勾選狀態
- suggest:建議值,會覆蓋默認值,配合變量或函數動態修改輸入框內容或勾選框勾選狀態
- thumb標籤:插件模板效果縮略圖
- global標籤:引用全局變量文件
- execute標籤:執行模板代碼配置文件,這裏是個文件引用
3. 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>
可以看到其同template.xml內的parameter類似,內部有標籤,分別定義id,type和默認值。
4. recipe.xml.ftl
用於執行模板代碼的配置文件。查看文件:
<?xml version="1.0"?>
<#import "root://activities/common/kotlin_macros.ftl" as kt>
<recipe>
<#include "../common/recipe_manifest.xml.ftl" />
<@kt.addAllKotlinDependencies />
<#if generateLayout>
<#include "../common/recipe_simple.xml.ftl" />
<open file="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml" />
</#if>
<instantiate from="root/src/app_package/SimpleActivity.${ktOrJavaExt}.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.${ktOrJavaExt}" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.${ktOrJavaExt}" />
</recipe>
簡單介紹下:
- <#include/>標籤,引入某文件內容
- 打開文件標籤,這個要看執行時機,如果文件還沒生成,則可能會報錯,所以該標籤會放在生成目標文件代碼後。
- 標籤,實例化,即執行模板代碼。
- from,執行指定某目錄下的模板代碼文件,上文提到模板文件是在root目錄下
- to,執行後的結果內容輸出文件,像這樣的
${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml
由函數,變量拼接文件名(包擴路徑)的表達式,我們不需要了解它是怎麼實現的,但根據你的開發經驗一定能看得懂它會生成什麼樣的文件名(包括路徑),我好像說了一句廢話 T^T
5. template_blank_activity.png
沒什麼好介紹的,模板效果縮略圖文件,上文提到用thumb標籤使其顯示於面板上。
說好的簡單介紹,結果還是BB了那麼多
下面開始折騰“MVPActivity”插件
分析
上文已經給出代碼示意圖,我們需要自動生成YModuleActivity,IYModule和YModulePresenter三個java類,那麼:
1. 需要各自三個類的源碼模板
比如模板名分別爲:MVPActivity.java.ftl,MVPInterface.java.ftl和MVPPresenter.java.ftl,由上文可知,文件於root->src->app_package目錄下。
2. 需要各自三個類的類名變量和一個公共拼接變量
對於這樣的代碼:
public class YModuleActivity extends BaseActivity<IYModule.Presenter> implements IYModule.View {
@Override
public void setPresenter(IYModule.Presenter presenter) {
// Bind presenter
if (presenter == null) {
presenter = new YModulePresenter(this);
}
}
}
需要轉換成動態化模板成這樣:
public class ${activityClass} extends BaseActivity<${mvpInterface}.Presenter> implements ${mvpInterface}.View {
@Override
public void setPresenter(${mvpInterface}.Presenter presenter) {
// Bind presenter
if (presenter == null) {
presenter = new ${presenterClass}(this);
}
}
}
對,是這三個變量:activityClass,mvpInterface,presenterClass,上文查看template.xml文件中,已經看到“activityClass”已定義過,只要再添加另兩個就好了。
心細的同學可能注意到YModuleActivity,IYModule和YModulePresenter都有個YModule
,可以提取變量爲commonClassName。
3. 需要一個時間變量(可選)
對於類的註釋,比如這樣:
/**
* <pre>
* @author : www.icheny.cn
* @e-mail : [email protected]
* @time : 2018.08.25
* @desc : ${commonClassName} Activity.
* @version: 1.0.1
* </pre>
*/
註釋很多都可以共用或稍加修改便可,但是創建的時間(time)是動態的,那麼需要變量:createTime來替換。
製作
1. 三個類的源碼模板
MVPActivity.java.ftl
package ${packageName};
<#if generateLayout>
import cn.icheny.plugin.mvp.demo.R;
</#if>
import cn.icheny.plugin.mvp.demo.module.base.BaseActivity;
/**
* <pre>
* @author : www.icheny.cn
* @e-mail : [email protected]
* @time : ${createTime}
* @desc : ${commonClassName} Activity.
* @version: 1.0.1
* </pre>
*/
public class ${activityClass} extends BaseActivity<${mvpInterface}.Presenter> implements ${mvpInterface}.View {
@Override
protected void initData() {
}
@Override
protected void initViews() {
}
@Override
protected int layoutId() {
return R.layout.${layoutName};
}
@Override
public void showData() {
}
@Override
public void setPresenter(${mvpInterface}.Presenter presenter) {
// Bind presenter
if (presenter == null) {
presenter = new ${presenterClass}(this);
}
}
}
MVPInterface.java.ftl
package ${packageName};
import cn.icheny.plugin.mvp.demo.module.base.IBasePresenter;
import cn.icheny.plugin.mvp.demo.module.base.IBaseView;
/**
* <pre>
* @author : www.icheny.cn
* @e-mail : [email protected]
* @time : ${createTime}
* @desc : MVP --> ${commonClassName} VP --> View,Presenter. Child View And Child Presenter Interface For This Module.
* @version: 1.0.1
* </pre>
*/
public interface ${mvpInterface} {
interface View extends IBaseView<Presenter> {
/**
* show data
*/
void showData();
}
interface Presenter extends IBasePresenter {
/**
* load data
*/
void loadData();
}
}
MVPPresenter.java.ftl
package ${packageName};
/**
* <pre>
* @author : www.icheny.cn
* @e-mail : [email protected]
* @time : ${createTime}
* @desc : MVP --> ${commonClassName} P --> Presenter. Child Presenter Implements Class For This Module.
* @version: 1.0.1
* </pre>
*/
public class ${presenterClass} implements ${mvpInterface}.Presenter {
private final ${mvpInterface}.View view;
/**
* Constructor
*
* @param view
*/
public ${presenterClass}(${mvpInterface}.View view) {
this.view = view;
}
@Override
public void loadData() {
}
@Override
public void doRefresh() {
}
}
2. 定義UI面板,template.xml
......
<parameter
id="createTime"
name="Create Time"
type="string"
default="2018.08.25"
help="The time that will show on class annotation." />
<parameter
id="commonClassName"
name="Common Class Name"
type="string"
default="Main"
help="The string ,Other class will use for their name." />
<parameter
id="activityClass"
name="Activity Name"
type="string"
constraints="class|unique|nonempty"
suggest="${commonClassName}Activity"
default="MainActivity"
help="The name of the activity class to create" />
<parameter
id="mvpInterface"
name="MVP Interface Name"
type="string"
constraints="class|unique|nonempty"
suggest="I${commonClassName}"
default="IMain"
help="The name of the mvp interface to create" />
<parameter
id="presenterClass"
name="Presenter Name"
type="string"
constraints="class|unique|nonempty"
suggest="${commonClassName}Presenter"
default="MainPresenter"
help="The name of the mvp presenter class to create" />
.......
限於文章篇幅,這裏只貼添加和修改的代碼。
3. 定義執行模板代碼配置文件,recipe.xml.ftl
......
<instantiate from="root/src/app_package/MVPActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<instantiate from="root/src/app_package/MVPInterface.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${mvpInterface}.java" />
<instantiate from="root/src/app_package/MVPPresenter.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${presenterClass}.java" />
......
同樣,這裏只貼添加和修改的代碼。
完成以上,重啓AS,就可以愉快地玩耍了。
至此,本文結束。Demo源碼後續文章更新發布,敬請關注。
2018年09月3日更新:Demo源碼:Github:AndroidStudioPluginForMVP