人类进步的根源是什么?是懒惰,是的,没有错,就是懒惰,正是当你想偷懒时,你才会去寻找更便捷的方法搞定一件事。写代码也是一样的,不想偷懒的程序猿不是好程序猿,下面我们来看看如何“偷懒”。
首先,声明一下,本文的作用纯属抛砖引玉,并不会太详细的介绍具体使用方法,仅仅介绍大概使用思路及踩坑日记。虽然本文以mvp为例,但是本文所讲的内容不局限于此,基本上所有的模板代码,你都可以生成模板,方便后面使用。
使用mvp模式开发安卓项目的人都知道,创建一个activity通常需要创建包含接口在内的5个类,写一两个界面还好,如果真是写完整个项目,光是创建这些类都让人心烦,那么有没有快捷的方法呢?当然是有的,最简单的方法就是使用studio 自带的file template。
下面来写一个简单的Preseter类模板:
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")
public class ${NAME}Presenter extends StyleActivityPresenter<I${NAME}View,I${NAME}Model>{
@Override
protected void initPresenter (Bundle savedInstanceState) {
}
}
使用效果:
public class TestPresenter extends StyleActivityPresenter<ITestView, ITestModel> {
@Override
protected void initPresenter (Bundle savedInstanceState) {
}
}
可以看出,使用上还是非常简单的,${NAME}就是你创建文件时输入的名字,其他的相信不用解释大家都看的懂。
上面的模板功能虽然已经能够很方便的让我创建一个类而不用去写过多的重复代码了,但是依然不够好用,因为上面说了,mvp模式通常包含5个类,还有布局文件,还有activity注册代码,这些可以说是每次创建activity的必须代码,而如果仅仅使用上面的file template功能,依然需要多次在不同包下创建文件,还有没有更偷懒的方法呢?当然有,就是studio强大的activity模板功能了。
其实这个功能,大家经常都在使用,只是很多人并没有注意罢了,就比如我们新建项目时:
这其实就是studio自带的activity模板,我们知道当我们选中某个类型的模板后,生成了项目之后,项目中就会有相应的java代码和布局,并且他会帮你在manifest注册好这个activity。
下面我们需要的也就是自定义这个功能,让他实现输入一个类名后在你指定的包下面自动生成5个mvp相关类和布局文件已经manifest注册。
首先,我们需要知道,系统自带的模板位置:XXX\android-studio\plugins\android\lib\templates\activities
这个目录下就是上面我们看到的所有activity模板的文件目录,先来简单介绍一下模板的目录下几个重要的文件及其作用,我们以LoginActivity这个模板为例:
root:这个目录下面放的是我们我们的代码模板,和file template代码类似,但是有一定区别。我喜欢叫他们模板输出原型。
globals.xml:这个文件是用来配置某些特殊属性的,比如是否是启动页面之类的属性。
recipe.xml:这个文件主要是配置需要生成哪些文件,用哪个模板生成,生成后要输出到哪个目录。
template.xml这个文件主要是用来定义我们的一些文件名和包名之类的变量属性,看看LoginActivity的配置界面效果,相信大家就懂这个文件的作用了:
template_login_activity.png:这个是上面图中那个界面示意图,通常不需要管它,当然你也可以放一张自己的图,替换掉。
下面说说怎么自定义自己的模板,(文章开始已经说了,本文并不会详细介绍如何进行自定义模板<我能说是自己也是才学这个东西吗?>,这里直接介绍我自己自定义时遇到的坑,和一些比较重要的注意事项):
- 首先,建议大家从最简单的模板开始尝试,不要一开始就完全以自己的mvp类去写,等熟悉了相关属性和规则后再去写mvp相关的模板,这是因为模板这个东西不是我们的项目代码,如果你配置错了,使用时虽然会有报错提示,但是并不准确,所以如果你一次性写太多东西的话,排查错误时很慢。
- 其次,建议直接先复制一份系统的模板代码比如(LoginActivity模板),然后在此基础上修改,不要自己去创建每个文件,理由和第一条类似,容易出错。
然后开始我们的模板创建之旅:
1.创建Demo项目用于测试
非常简单,只包含了一个默认的自动生成的MainActivity类
2.复制LoginActivity模板
在我们的对应目录XXX\android-studio\plugins\android\lib\templates\activities中复制LoginActivity文件夹并重命名为MVPTestActivity,接着进入我们复制的文件夹,打开template.xml这个文件,改掉name的值,最好是和你的目录名保持一致,我们这里就叫MVPTestActivity,其他几个属性可以按照自己的需要进行修改。
<template
format="5"
revision="6"
name="Login Activity"-->此属性改名为MVPTestActivity
description="Creates a new login activity, allowing users to optionally sign in with Google+ or enter an email address and password to log in to or register with your application."
minApi="8"
minBuildApi="14">
parameter,这个标签是我们配置界面上的字段,我这里只截图两个重要字段:
<parameter
id="activityClass"
name="Activity Name"
type="string"
constraints="class|unique|nonempty"
default="LoginActivity"
help="The name of the activity class to create" />
<parameter
id="layoutName"
name="Layout Name"
type="string"
constraints="layout|unique|nonempty"
suggest="${activityToLayout(activityClass)}"
default="activity_login"
help="The name of the layout to create for the activity" />
两张图一起看的话就很好理解了,id为activityClass的这个标签就是我们的类名,id为layoutName就是我们的布局名,id这个字段是我们在其他配置文件中引用name的查找依据,type自然就是类型,constraints是一些输入限制信息,default当然就是默认实现在配置界面上的值了,每个属性的具体用法,大家自己搜索一下相关博文,这里不是本文的重点。
我们这里先开始写最简单的,那么我们肯定是不需要Title之类的属性的,所以我们先把不需要显示或配置的字段注释掉。
<!-- <parameter
id="activityTitle"
name="Title"
type="string"
constraints="nonempty"
default="Sign in"
help="The name of the activity." /> -->
...
其他的一些字段我们也可以根据我们的需要稍作调整,这样这个文件基本就修改完成了。
recipe.xml,打开此文件,我们可以看到
<?xml version="1.0"?>
<#import "root://activities/common/kotlin_macros.ftl" as kt>
<recipe>
<#if appCompat && !(hasDependency('com.android.support:appcompat-v7'))>
<dependency mavenUrl="com.android.support:appcompat-v7:${buildApi}.+" />
</#if>
<#if (buildApi gte 22) && appCompat && !(hasDependency('com.android.support:design'))>
<dependency mavenUrl="com.android.support:design:${buildApi}.+" />
</#if>
<#include "../common/recipe_theme.xml.ftl" />
<merge from="root/AndroidManifest.xml.ftl"
to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<merge from="root/res/values/dimens.xml"
to="${escapeXmlAttribute(resOut)}/values/dimens.xml" />
<merge from="root/res/values/strings.xml.ftl"
to="${escapeXmlAttribute(resOut)}/values/strings.xml" />
<instantiate from="root/res/layout/activity_login.xml.ftl"
to="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml" />
<#if generateKotlin>
<@kt.addAllKotlinDependencies />
<instantiate from="root/src/app_package/LoginActivity.kt.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.kt" />
<#else>
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
</#if>
</recipe>
是不是看的一头雾水,其实不难,ifelse相信不用过多解释,我们重点讲讲其他几个比较重要的标签的作用。
- merge 顾名思义就是合并的意思,这个主要是用在资源文件或者manifest文件,因为我们通常是需要把我们新建的xml文件和项目中的进行合并,这里稍微讲解一下manifest的合并,因为其他的资源文件合并都很简单,就不说了。
打开MVPTestActivity\root目录下的manifest文件,主要代码如下:
<activity android:name=".${activityClass}"
<#if isNewProject>
android:label="@string/app_name"
<#else>
android:label="@string/title_${simpleName}"
</#if>
<#if hasNoActionBar>
android:theme="@style/${themeNameNoActionBar}"
<#elseif !(hasApplicationTheme!false)>
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>
<@manifestMacros.commonActivityBody />
</activity>
基本上不是很难理解,类似于这种${}代码,都是对其他文件中属性的引用,下面我们去掉我们不需要的属性,修改后:
<activity android:name=".${activityClass}"
<#if isNewProject>
android:label="@string/app_name"
</#if>
<#if hasNoActionBar>
android:theme="@style/${themeNameNoActionBar}"
<#elseif !(hasApplicationTheme!false)>
android:theme="@style/${themeName}"
</#if>>
<@manifestMacros.commonActivityBody />
</activity>
- instantiate 就是创建文件
- open file当然就是在我们生成好类后,打开相关类
这里重点说说from和to这两个属性,看名字大家应该都能猜出一点了,from就是指的我们的模板文件路径,to当然就是我们生成文件的路径,以简单的布局文件为例,可以看到一个instantiate标签内容如下:
<instantiate from="root/res/layout/activity_login.xml.ftl"
to="${escapeXmlAttribute(resOut)}/layout/${layoutName}.xml" />
其中root/res/layout/activity_login.xml.ftl这个路径,明显就是我们当前模板目录下面的文件,而to的属性,我们也可以理解下,${escapeXmlAttribute(resOut)}多半就是指的当前项目的res资源主目录,然后${layoutName}明显就是引用的template.xml中配置的,用户输入的layoutName这个String。
明白了这些属性后,我们做出如下修改:
- 删掉MVPTestActivity\root\res目录下的values文件夹
- 注释掉recipe.xml文件中两个关于资源合并的标签(不去掉会报空指针,因为我们已经删掉了资源模板)
- 最后修改MVPTestActivity\root\src\app_package目录下的LoginActivity.java文件(其实还需要同步修改LoginActivity.kt,不过因为我暂时没有使用kotlin,所以没做它的适配),这个文件内容太多,为了方便查看,我们简化成如下代码:
package ${packageName};
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
<#if applicationPackage??>
import ${applicationPackage}.R;
</#if>
/**
* A login screen that offers login via email/password.
*/
public class ${activityClass} extends AppCompatActivity {
@Override
protected void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.${layoutName});
}
}
好了,到此为止,其实我们已经完成了一些最简单的修改,下面我们来看看运行效果,保存每个文件后,重启studio(一开始写的时候千万不要想一次改完,不然多半会因为失败而放弃这个):
可以看到我们修改了模板介绍,修改了默认的activity名字,去掉了title和parent两个输入框,让我们点击完成试一下效果:
TestActivity和activity_test都是我们通过模板自动生成的文件,并且mainfest中已经注册了这个activity了,具体截图就不放了,大家可以自己尝试。
上面的步骤已经算是完成了模板的最基本使用了,但是离我们的要求还差点,因为我们想要的是生成多个文件,并且多个文件有可能不再同一个包下,那么我们怎么实现呢?
首先,假如我们的项目结构如下:
那么我现在需要的就是:
- 在presenter包下创建一个TestActivityPresenter类
- 在model包下创建一个TestActivityModel
- 在view包下创建一个TestActivityView
- 在port包下创建一个ITestActivityModel接口和ITestActivityView接口
- 并且TestActivityView应该实现ITestActivityView接口,TestActivityModel应该实现ITestActivityModel接口
- 生成对应布局文件,并且注册activity
最后一条,基本上不用再说了,我们开始的改动已经满足了,重点说说下面几条怎么实现,主要是说说怎么在不同目录下生成对应文件。
回到我们的recipe.xml文件中,我们注意看这里
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
这里其实就是用来生成我们刚刚那个TestActivity的配置代码,明显的我们想多生成几个文件的话,只需要多写几个这种标签就好,比如像下面这样:
<!--IView -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/I${activityClass}View.java" />
<!--View -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
<!--IModel -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/I${activityClass}Model.java" />
<!--Model -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}Model.java" />
<!--Presenter -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}Presenter.java" />
<open file="${escapeXmlAttribute(srcOut)}/${activityClass}.java" />
注意看to的值的最后一点,我采用拼接的方式规定了每个文件的名字格式,这样所有mvp下的类名都是符合一定命名规范的,当然不一定和我的一样,但是你一定要有自己的格式,不要随意取名字,这是基础,不过多解释原因。
这样我们生成的时候就会得到5个文件,但是还不够,因为他们现在都在同一个目录下,怎么让他们在自己的目录下生成呢,再来改改:
<!--IView -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/port/I${activityClass}View.java" />
<!--View -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/view/${activityClass}.java" />
<!--IModel -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/port/I${activityClass}Model.java" />
<!--Model -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/model/${activityClass}Model.java" />
<!--Presenter -->
<instantiate from="root/src/app_package/LoginActivity.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}Presenter.java" />
<open file="${escapeXmlAttribute(srcOut)}/presenter/${activityClass}.java" />
</#if>
是不是很简单?只需要在${escapeXmlAttribute(srcOut)}后面跟上具体的包路径就好了,那么我们还缺点什么?我们还缺模板源文件,因为上面from的文件都是LoginActivity这个文件,也就是说生成的每个类的代码都是相同的。
下面让我们来完成最后一步,编写模板代码了,这个就相对简单了,毕竟基本上都是java代码,难不倒大家的,我们在MVPTestActivity\root\src\app_package路径下把LoginActivity.java.ftl这个文件复制四份,kt那个文件不管,那个是适配kotlin的,如果你是用的Kotlin的话,也可以复制那个。
然后依次改名,效果如下:
每个类的代码如下:
MvpView.java
package ${packageName}.view;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
<#if applicationPackage??>
import ${applicationPackage}.R;
</#if>
import com.xujl.demo.port.I${activityClass}View;
import com.xujl.demo.presenter.${activityClass}Presenter;
class ${activityClass}View extends AppCompatActivity implements I${activityClass}View{
private ${activityClass}Presenter mPresenter;
@Override
protected void onCreate (Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.${layoutName});
mPresenter = new ${activityClass}Presenter(this);
}
}
MvpPresenter.java
package ${packageName}.presenter;
import com.xujl.demo.model.${activityClass}Model;
import com.xujl.demo.port.I${activityClass}Model;
import com.xujl.demo.port.I${activityClass}View;
public class ${activityClass}Presenter {
private I${activityClass}View mView;
private I${activityClass}Model mModel;
public ${activityClass}Presenter(I${activityClass}View view){
mView = view;
mModel = new ${activityClass}Model();
}
}
MvpModel.java
package ${packageName}.model;
import com.xujl.demo.port.I${activityClass}Model;
public class ${activityClass}Model implements I${activityClass}Model {
}
IMvpModel.java
package ${packageName}.port;
public interface I${activityClass}Model{
}
IMvpView.java
package ${packageName}.port;
public interface I${activityClass}View{
}
最后不要忘记修改recipe.xml中from的模板文件名和模板mainfest中的activity名字:
<!--IView -->
<instantiate from="root/src/app_package/IMvpView.java.ftl"
to="${escapeXmlAttribute(srcOut)}/port/I${activityClass}View.java" />
<!--View -->
<instantiate from="root/src/app_package/MvpView.java.ftl"
to="${escapeXmlAttribute(srcOut)}/view/${activityClass}.java" />
<!--IModel -->
<instantiate from="root/src/app_package/IMvpModel.java.ftl"
to="${escapeXmlAttribute(srcOut)}/port/I${activityClass}Model.java" />
<!--Model -->
<instantiate from="root/src/app_package/MvpModel.java.ftl"
to="${escapeXmlAttribute(srcOut)}/model/${activityClass}Model.java" />
<!--Presenter -->
<instantiate from="root/src/app_package/MvpPresenter.java.ftl"
to="${escapeXmlAttribute(srcOut)}/${activityClass}Presenter.java" />
----------------------------------------------------
<activity android:name=".view.${activityClass}View"
<#if isNewProject>
android:label="@string/app_name"
</#if>
<#if hasNoActionBar>
android:theme="@style/${themeNameNoActionBar}"
<#elseif !(hasApplicationTheme!false)>
android:theme="@style/${themeName}"
</#if>>
<@manifestMacros.commonActivityBody />
</activity>
最后来看看运行效果(右键点击的时候一定要在主包上面点击,不然生成路径可能会出错):
如果你不是在主包上点击的,也没关系,记得修改这里为主包路径就好:
至此我们就完成了一个完整的模板了,当然这里不论是包结构还是命名方式,还是mvp结构,都是我自己定义的,大家完全可以根据自己的项目实际情况的来写,我这样定义的结构,好处是只需要键入类名,其他都可以生成了,当然你也可以多设置几个包名字段,用来动态配置model,view,presenter,和接口的包路径。
最后附上整个MVPTestActivity的模板文件链接: https://pan.baidu.com/s/1skW4nTV 密码: kbve