Android Studio自定义模板之MVPActivity

前言

Android开发中经常需要创建Activity。一般情况下,咱们都是"New"->Java Class/Activity。但是Android Studio自带的Activity模板都比较简单,未必符合我们所需的模板样式。例如在MVP框架下,需创建Activity、Present、Contract、Model等文件,并关联关系,初始化、author、统一的网络请求写法。所以我们需要自定义模板,帮我们省去这一系列重复操作。

举个栗子,创建自定义MVP模板(本文较长,赶时间的小伙伴可以到底部下载源码)


演示MVP.gif

图解1 配置创建信息

MVP Name为红框标记部分统一命名(Activity、Layout、Contract、Model、Present)。由Generate MVP File选择框决定是否在生成activity的同时生成Contract、Model和Presenter等Java类。


创建MVPActivity

图解2 文件结构

点击Finish完成创建后,可以看到自动生成的文件(如下图)


自动生成的文件

这里我们主要看自动生成的HomePageActivity。其他仨请看开头Gif,这里就不一一截图了


HomeMvpActivity

以上内容均为自动生成,使用过MVP的小伙伴是不是很熟悉?是不是很方便?妈妈再也不用担心我的CV大法导致代码区域乱七八糟了。

PS:每个项目MVP有或多或少差异,大家可以根据项目实际编写合适的模板。

附上Activity分区注释

    /*-----------------------静态Activity启动方法区-------------------*/
    /*-----------------------常量声明区-------------------------------*/
    /*-----------------------UI控件成员变量声明区---------------------*/
    /*-----------------------普通成员变量声明区-----------------------*/
    /*-----------------------初始化相关方法区-------------------------*/
    /*-----------------------生命周期回调方法区(除onCreate()方法外)- */
    /*-----------------------事件响应方法区---------------------------*/
    /*-----------------------重载的逻辑方法区-------------------------*/
    /*-----------------------普通逻辑方法区---------------------------*/
    /*-----------------------内部接口声明区---------------------------*/
    /*-----------------------内部类声明区-----------------------------*/

如何自定义模板

1.熟悉安卓模板

在Android Studio的安装目录下 \plugins\android\lib\templates\activities(同Mac)保存着系统自带的activity模板和我们自定义的模板

\plugins\android\lib\templates\activities

在编写自定义模板前,需要熟悉下模板的结构和组成,这里我们从最简单的模板EmptyActivity入手
EmptyActivity目录

src:代码文件,生成对应的文件模板
globals.xml.ftl:Java类库,主要用于提供参数。可存储全局变量以供其他模板文件统一引用
recipe.xml.ftl:用于组合生成我们实际需要的代码文件和布局文件等。
template.xml:相当于Android中的布局文件,用于图形化提供参数,布局等。
template_blank_activity.png:模板照片

自定义模板通常都是复制已有,然后再修改修改

globals.xml.ftl

存储一些全局变量

存储全局变量。每一个变量定义,由id 唯一标识,type 类型,value 实际值组成

recipe.xml.ftl

recipe.xml.ftl

即便不懂freemarker引擎也不影响,也能依样画葫芦,这里就简单讲下用到的标签及含义:

  • instantiate 将 ftl->java文件,中间会通过一个步骤,将ftl中的变量都换成对应的值,完整的流程是 ftl->freemarker process -> java 。
  • open 用于转换完成时在项目中对应的包下生成对应文件,例如Activity
  • if if指令,这里if判断是否生成layout文件
    • <#if >
      <#elseif >
      <#else >
      </#if >

template.xml

template.xml

template.xml

对应用户输入的模板界面,
<parameter>获取用户输入参数

  • id 唯一标识,通过该属性,获取输入值,也可用于其他文件查找引用
  • name 标签名称,类似label展示给用户看
  • type 输入值类型
  • constraints 填写值的约束
  • suggest 建议值,例如填写Activity Name,给出一个建议值
  • help 底部提示语言,对应图形化界面操作
    New EmptyActivity

几个文件的整体的关系类似下图:

关系图

图片来源:http://www.slideshare.net/murphonic/custom-android-code-templates-15537501


2.定义MVPActivity模板

了解完以上简单介绍,基本足够我们写自(zhào)定(yàng)义(huà)模(hú)板(lū)

2.1创建MVPActivity文件夹

一般都是复制现有的(例如EmptyActivity,或者文章末尾的源码),再改改。


目录\plugins\android\lib\templates\activities

添加MVP所对应文件


MVPActivity目录

2.2编写MVP相关文件

activity_mvp.xml.ftl

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="${packageName}.${activityClass}">

</RelativeLayout>

IContract.java.ftl

package ${packageName};

import com.xx.mvp.base.BaseModel;
import com.xx.mvp.base.BasePresenter;
import com.xx.mvp.base.BaseView;
import rx.Observable;
<#assign aDateTime = .now>
/**
 * Model: {@link ${modelName}} View:{@link ${activityClass}} Presenter:{@link ${presenterName}}
 * @Author: ${author}
 * @Description: 
 * @Date: Create in ${aDateTime}
 * @Modified By:
 */
public interface ${contractName} {

   interface Model extends BaseModel {
        /**
         * 更新数据
         *
         * @return
         */
        Observable<Object> update();

    }

    interface View extends BaseView {

    }

    interface Presenter extends BasePresenter<Model, View> {

        /**
         * 更新数据
         */
        void update();
    }
    
}

Model.java.ftl

package ${packageName};

import com.xx.bean.base.BaseResult;
import com.xx.http.rx.TransformUtils;
import com.xx.mvp.base.BaseModelImpl;
import retrofit2.http.POST;
import rx.Observable;
<#assign aDateTime = .now>
/**
 * Present: {@link ${presenterName}}
 * @Author: ${author}
 * @Description: 
 * @Date: Create in  ${aDateTime}
 * @Modified By:
 */
public class ${modelName} extends BaseModelImpl<${modelName}.Service> implements ${contractName}.Model {
    public ${modelName}() {super(${modelName}.Service.class);}

    @Override
    public Observable<Object> update() {
        return getRequestService()
                .update()
                .compose(TransformUtils.defaultSchedulers());
    }

    public interface Service {
        /**
         * 更新数据
         *
         * @return
         */
        @POST("pos-web/updateAllData")
        Observable<BaseResult<Object>> update();
    }
}

MVPActivity.java.ftl

package ${packageName};

import ${superClassFqcn};
import android.os.Bundle;
<#if includeCppSupport!false>
import android.widget.TextView;
</#if>
<#assign aDateTime = .now>
/**
<#if generateMVP>
 * Model: {@link ${modelName}} Presenter:{@link ${presenterName}}
</#if>
 * @Author: ${author}
 * @Description: 
 * @Date: Create in ${aDateTime}
 * @Modified By:
 */
<#if generateMVP>
public class ${activityClass} extends BaseActivity<${contractName}.Model, ${contractName}.View, ${contractName}.Presenter> implements ${contractName}.View {
<#else>
public class ${activityClass} extends BaseActivity{
</#if>

    /*-----------------------静态Activity启动方法区-------------------*/

    public static void startActivity(Activity activity) {
        Intent intent = new Intent(activity, ${activityClass}.class);
        activity.startActivity(intent);
    }
    
    /*-----------------------常量声明区-------------------------------*/
    /*-----------------------UI控件成员变量声明区---------------------*/
    /*-----------------------普通成员变量声明区-----------------------*/
    /*-----------------------初始化相关方法区-------------------------*/

    @Override
    public int getContentView() {
        return R.layout.${layoutName};
    }

    @Override
    public void initView(Bundle savedInstanceState, View titleLayout) {

    }

    @Override
    public void initData(Intent intent) {
        getPresenter().update();
    }
  

<#if generateMVP>
    @Override
    public ${contractName}.Model createModel() {
        return new ${modelName}();
    }

    @Override
    public ${contractName}.View createView() {
        return this;
    }

    @Override
    public ${contractName}.Presenter createPresenter() {
        return new ${presenterName}();
    }
<#else>
    @Override
    public BasePresenter createPresenter() {
        return null;
    }
</#if>

    /*-----------------------生命周期回调方法区(除onCreate()方法外)-*/
    /*-----------------------事件响应方法区---------------------------*/
    /*-----------------------重载的逻辑方法区-------------------------*/
    /*-----------------------普通逻辑方法区---------------------------*/
    /*-----------------------内部接口声明区---------------------------*/
    /*-----------------------内部类声明区-----------------------------*/
   
}

Presenter.java.ftl

package ${packageName};

import android.os.Bundle;
import com.xx.http.rx.BaseSubscribe;
import com.xx.mvp.base.BasePresenterImpl;
import rx.Subscription;

<#assign aDateTime = .now>
/**
 * Model: {@link ${modelName}} View:{@link ${activityClass}}
 * @Author: ${author}
 * @Description: 
 * @Date: Create in ${aDateTime}
 * @Modified By:
 */
class ${presenterName} extends BasePresenterImpl<${contractName}.Model, ${contractName}.View>
        implements ${contractName}.Presenter {

    @Override
    public void onCreate(Bundle savedInstanceState) {

    }
    
    /**
     * 更新数据
     */
    @Override
    public void update() {
        Subscription subscribe = getModel().update()
                .subscribe(new BaseSubscribe<Object>(this) {
                    @Override
                    protected void onSuccess(Object bean) {
                    }
                });
        addSubscribeRequest(subscribe);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

}

2.3编写配置文件

template.xml 定义界面

<?xml version="1.0"?>
<template
    format="5"
    revision="5"
    name="MVPActivity"
    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="activityName"
        name="MVP Name"
        type="string"
        constraints="nonempty"
        default="MVP"
        help="The name of the MVP class to create" />
    <parameter
        id="activityClass"
        name="MVPActivity Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${activityName}Activity"
        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(activityName)}"
        default="activity_main"
        visibility="generateLayout"
        help="The name of the layout to create for the activity" />

    <parameter
        id="generateMVP"
        name="Generate MVP File"
        type="boolean"
        default="true"
        help="If true, a mvp file will be generated" />

        <parameter
        id="contractName"
        name="MVP Contract"
        type="string"
        suggest="${activityName}Contract"
        help="The name of the contract to create for the activity" />
        <parameter
        id="modelName"
        name="MVP Model"
        type="string"
        suggest="${activityName}Model"
        help="The name of the model to create for the activity" />
        <parameter
        id="presenterName"
        name="MVP Presenter"
        type="string"
        suggest="${activityName}Presenter"
        help="The name of the presenter 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>

recipe.xml.ftl
注意其中$变量,引用temlplate.xml中各控件的变量id,方便创建的文件名字已命名好。

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>
    <instantiate from="root/res/layout/activity_mvp.xml.ftl"
                 to="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml"/>
</#if>

<#if generateMVP>

    <instantiate from="root/src/app_package/IContract.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${contractName}.java" />
    <open file="${escapeXmlAttribute(srcOut)}/${contractName}.java" />

     <instantiate from="root/src/app_package/Model.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${modelName}.java" />
    <open file="${escapeXmlAttribute(srcOut)}/${modelName}.java" />

     <instantiate from="root/src/app_package/Presenter.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${presenterName}.java" />
    <open file="${escapeXmlAttribute(srcOut)}/${presenterName}.java" />
    
</#if>


<#if generateKotlin>
  
    <instantiate from="root/src/app_package/MVPActivity.kt.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
    <open file="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<#else>
    <instantiate from="root/src/app_package/MVPActivity.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
    <open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />

</#if>

</recipe>

template.xml
想要让id activityName统一命名Activity、Present、Contract、Model、layout,需要通过suggest属性统一引用它的id

template.xml

<?xml version="1.0"?>
<template
    format="5"
    revision="5"
    name="MVPActivity"
    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="activityName"
        name="MVP Name"
        type="string"
        constraints="nonempty"
        default="MVP"
        help="The name of the MVP class to create" />
    <parameter
        id="activityClass"
        name="MVPActivity Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${activityName}Activity"
        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(activityName)}"
        default="activity_main"
        visibility="generateLayout"
        help="The name of the layout to create for the activity" />

    <parameter
        id="generateMVP"
        name="Generate MVP File"
        type="boolean"
        default="true"
        help="If true, a mvp file will be generated" />

        <parameter
        id="contractName"
        name="MVP Contract"
        type="string"
        suggest="${activityName}Contract"
        help="The name of the contract to create for the activity" />
        <parameter
        id="modelName"
        name="MVP Model"
        type="string"
        suggest="${activityName}Model"
        help="The name of the model to create for the activity" />
        <parameter
        id="presenterName"
        name="MVP Presenter"
        type="string"
        suggest="${activityName}Presenter"
        help="The name of the presenter 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>

2.4小结,集(tì)成(huàn)为自己项目MVP

以上自定义MVP模板已经全部结束。最后需要把MVPActivity挪移到Android Studio的安装目录下 \plugins\android\lib\templates\activities(同Mac),再重启Android Studio,大功告成!

1.java.ftl模板中使用变量。
例如自动生成的IContract.java文件中顶部含Header 信息,Present、Activity等信息(记得修改globals.xml.ftlauthor值;java import com.xx.等导包)。如图

IContract.java

IContract.java.ftl

template.xml

globals.xml.ftl

2.内联的顺序
内联的顺序在代码中是从上往下执行的,因此要想生成文件后焦点窗口定位在新的Activity内,则要把Activity的内联代码放在最下面,这样所有文件生成完毕后才会优先定位到Activity窗口
recipe.xml.ftl

PS:若编码的时候出现语法错误,那么在Android studio中点击finish生成Activity的时候会直接报错,查看点击log可以看到详细的报错位置,自己再进行修改就可以了。

附上模板源码:MVP模板下载地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,565评论 6 525
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,696评论 3 406
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,935评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,327评论 1 303
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,338评论 6 401
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,760评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,085评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,091评论 0 280
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,656评论 1 327
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,657评论 3 348
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,767评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,360评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,088评论 3 341
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,493评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,654评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,374评论 3 383
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,841评论 2 367