在Android Studio中自定义代码模板

概述

我们在使用Android Studio创建Activity、Fragment等等的时候,都会使用Android Studio提供的模板来简化我们创建的,使用模板时,我们只要做简单的配置,Android就能为我们生成相应的代码,所以使用模板可以提高开发的效率,接下来我们将学习如何去自定义一个符合自己项目框架的模板。

介绍

Android Studio模板的安装路径:<Android Studio安装目录>/plugins/android/lib/templates

as_template_dir.png
as_template.png

模板文件结构

Android Studio中已有的Empty Activity模板:

empty_activity_template.png

模板组成结构:

  • template.xml:定义模板参数
  • globals.xml.ftl:定义全局变量
  • recipe.xml.ftl:配置要引用的模板路径和生成的文件的路径
  • root文件:存放模板文件和资源文件
  • 效果缩略图

模板变量处理流程:

template_variable_dataflow.png

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
        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>
template.png

说明

  • <template>中的name对应新建Activity时显示的名字
  • <category>对应New的类别为Activity
  • <parameter>对应界面上蓝色框的一个项,
    • id:唯一表示,最终通过该属性值,获取用户界面上的输入值
    • name:界面上Label提示语
    • type:输入值类型
    • constraints:值约束
    • suggest:建议值,比如填写ActivityName的时候,会给出LayoutName的建议值
    • help:底部显示的提示语

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>

这个文件用于定义一些全局变量。

说明

<global>:表示一个全局变量

  • id:变量名
  • type:变量类型
  • value:默认值

访问变量:${变量id}

recipe.xml.ftl

<#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>:导入另一个ftl文件
  • <open>:在代码生成后打开指定文件,例如,当我们创建一个Activity后,AS会自动打开Activity及布局文件。
  • <instantiate>:将.ftl文件转成.java.kt文件。
  • <copy>:用于从root文件夹中复制文件到目标目录。
  • <merge>:用于合并文件,如将模板的strings.xml合并到我们项目中的strings.xml

Freemarker语法

AS 中模板的定义使用的是Freemarker的语法。

语法

  • ${变量名}:访问变量值

  • <#if 变量名>:条件判断

  • <#include "xx.ftl">:引入其他模板文件

实例:EmptyActivity\root\src\app_package\SimpleActivity.java.ftl

package ${packageName};

import ${superClassFqcn};
import android.os.Bundle;
<#if (includeCppSupport!false) && generateLayout>
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});
       <#include "../../../../common/jni_code_usage.java.ftl">
<#elseif includeCppSupport!false>

        // Example of a call to a native method
        android.util.Log.d("${activityClass}", stringFromJNI());
</#if>
    }
<#include "../../../../common/jni_code_snippet.java.ftl">
}

从模板到代码的流程:

code_generation_process.png

自定义MVP模板

在Google给出的MVP Sample中,每创建一个页面,需要创建:

XxActivityXxFragmentXxContractXxPresenter四个文件,步骤繁琐,且AS目前没有提供相应的模板,所以接下来将自定义一个MVP的模板,来简化这些繁琐的操作。

template.xml

<?xml version="1.0"?>
<template
    format="5"
    revision="5"
    name="Page"
    minApi="9"
    minBuildApi="14"
    description="Creates a new MVP page">

    <category value="MVP" />
    <formfactor value="Mobile" />

    <parameter
        id="activityClass"
        name="Activity Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${layoutToActivity(activityLayout)}"
        default="MainActivity"
        help="The name of the activity class to create" />

    <parameter
        id="activityLayout"
        name="Activity Layout Name"
        type="string"
        constraints="layout|unique|nonempty"
        suggest="${activityToLayout(activityClass)}"
        default="activity_main"
        help="The name of the layout to create for the activity" />

    <parameter
        id="fragmentClass"
        name="Fragment Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${underscoreToCamelCase(classToResource(activityClass))}Fragment"
        default="MainFragment"
        help="The name of the fragment class to create" />

    <parameter
        id="fragmentLayout"
        name="Fragment Layout Name"
        type="string"
        constraints="layout|unique|nonempty"
        suggest="fragment_${classToResource(fragmentClass)}"
        default="fragment_main"
        help="The name of the layout to create for the fragment" />

    <parameter
        id="contractClass"
        name="Contract Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${underscoreToCamelCase(classToResource(fragmentClass))}Contract"
        default="MainViewModel"
        help="The name of the contract class to create" />

    <parameter
        id="presenterClass"
        name="Presenter Name"
        type="string"
        constraints="class|unique|nonempty"
        suggest="${underscoreToCamelCase(classToResource(fragmentClass))}Presenter"
        default="MainViewModel"
        help="The name of the presenter class to create" />

    <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="packageName"
        name="Package name"
        type="string"
        constraints="package"
        default="com.mycompany.myapp" />

    <parameter
        id="pagePackage"
        name="Page package path"
        type="string"
        constraints="package"
        suggest="ui.${classToResource(fragmentClass)?replace('_', '')}"
        default="ui.main"
        help="The package path for the page." />

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

globals.xml.ftl

<?xml version="1.0"?>
<globals>
    <global id="hasNoActionBar" type="boolean" value="false" />
    <global id="parentActivityClass" value="" />
    <global id="simpleLayoutName" value="${activityLayout}" />
    <global id="excludeMenu" type="boolean" value="true" />
    <global id="generateActivityTitle" type="boolean" value="false" />
    <#include "../../activities/common/common_globals.xml.ftl" />
</globals>

recipe.xml.ftl

<?xml version="1.0"?>
<recipe>

    <#--  生成mainfest配置  -->
    <merge from="root/AndroidManifest.xml.ftl"
           to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />

    <#--  生成布局文件  -->
    <instantiate from="root/res/layout/activity.xml.ftl"
                   to="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(activityLayout)}.xml" />
    <instantiate from="root/res/layout/fragment.xml.ftl"
                   to="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(fragmentLayout)}.xml" />

    <#--  生成.java文件  -->
    <instantiate from="root/src/app_package/Activity.${ktOrJavaExt}.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${pagePackage?replace('.', '/')}/${activityClass}.${ktOrJavaExt}" />
    <instantiate from="root/src/app_package/Fragment.${ktOrJavaExt}.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${pagePackage?replace('.', '/')}/${fragmentClass}.${ktOrJavaExt}" />    
    <instantiate from="root/src/app_package/Contract.${ktOrJavaExt}.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${pagePackage?replace('.', '/')}/${contractClass}.${ktOrJavaExt}" />
    <instantiate from="root/src/app_package/Presenter.${ktOrJavaExt}.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${pagePackage?replace('.', '/')}/${presenterClass}.${ktOrJavaExt}" />   

    <#--  打开文件.java文件  -->
    <open file="${escapeXmlAttribute(srcOut)}/${pagePackage?replace('.', '/')}/${activityClass}.${ktOrJavaExt}" />
    <open file="${escapeXmlAttribute(srcOut)}/${pagePackage?replace('.', '/')}/${fragmentClass}.${ktOrJavaExt}" />
    <open file="${escapeXmlAttribute(srcOut)}/${pagePackage?replace('.', '/')}/${contractClass}.${ktOrJavaExt}" />
    <open file="${escapeXmlAttribute(srcOut)}/${pagePackage?replace('.', '/')}/${presenterClass}.${ktOrJavaExt}" />

    <#--  打开布局文件  -->
    <open file="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(activityLayout)}.xml" />
    <open file="${escapeXmlAttribute(resOut)}/layout/${escapeXmlAttribute(fragmentLayout)}.xml" />

</recipe>

Activity.java.ftl

package ${packageName}.${pagePackage};

import ${superClassFqcn};
import android.os.Bundle;
import com.github.xch168.mvp.util.ActivityUtil;

import ${packageName}.R;

public class ${activityClass} extends ${superClass} {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.${activityLayout});

        ${fragmentClass} fragment = (${fragmentClass}) getSupportFragmentManager().findFragmentById(R.id.contentFrame);

        if (fragment == null) {
            fragment = ${fragmentClass}.newInstance();

            ActivityUtil.addFragmentToActivity(getSupportFragmentManager(), fragment, R.id.contentFrame);
        }

        new ${presenterClass}(fragment);

    }

}

Fragment.java.ftl

package ${packageName}.${pagePackage};

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import ${packageName}.R;

public class ${fragmentClass} extends Fragment implements ${contractClass}.View {

    private ${contractClass}.Presenter mPresenter;

    public static ${fragmentClass} newInstance() {
        Bundle arguments = new Bundle();
        arguments.putString("", "");
        ${fragmentClass} fragment = new ${fragmentClass}();
        fragment.setArguments(arguments);
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.${fragmentLayout}, container, false);


        return root;
    }

    @Override
    public void onResume() {
        super.onResume();
        mPresenter.start();
    }

    @Override
    public void setPresenter(@NonNull ${contractClass}.Presenter presenter) {
        mPresenter = presenter;
    }
}

Contract.java.ftl

package ${packageName}.${pagePackage};

import com.github.xch168.mvp.ui.BasePresenter;
import com.github.xch168.mvp.ui.BaseView;

public interface ${contractClass} {

    interface View extends BaseView<Presenter> {

    }

    interface Presenter extends BasePresenter {
        
    }  
}

Presenter.java.ftl

package ${packageName}.${pagePackage};

public class ${presenterClass} implements ${contractClass}.Presenter {

    private final ${contractClass}.View mView;

    public ${presenterClass}(${contractClass}.View view) {

        mView = view;

        mView.setPresenter(this);
    }

    @Override
    public void start() {
        
    }
}

AndroidManifest.xml.ftl

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="${packageName}">

    <application>
        <activity android:name="${packageName}.${pagePackage}.${activityClass}"
            <#if generateActivityTitle!true>
                <#if isNewProject>
                    android:label="@string/app_name"
                <#else>
                    android:label="@string/title_${activityToLayout(activityClass)}"
                </#if>
            </#if>
            <#if hasNoActionBar>
                android:theme="@style/${themeNameNoActionBar}"
            <#elseif (requireTheme!false) && !hasApplicationTheme && appCompat>
                android:theme="@style/${themeName}"
            </#if>
            <#if buildApi gte 16 && parentActivityClass != "">
                android:parentActivityName="${parentActivityClass}"
            </#if>>
            <#if parentActivityClass != "">
                <meta-data android:name="android.support.PARENT_ACTIVITY"
                    android:value="${parentActivityClass}" />
            </#if>
        </activity>
    </application>
</manifest>

使用MVP模板

将模板文件复制到<Android Studio安装目录>/plugins/android/lib/templates/{userName}/MVP目录下,然后重启Android Studio。

Step1:新建一个MVP页面

use_mvp_templage.jpg

Step2:配置参数

fill_mvp_template.png

Step3:点击Finish,将自动生成相关代码及资源文件

gen_mvp_code.png

参考链接

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

推荐阅读更多精彩内容