前言:前段时间,公司要开发一个移动开发平台,有一个功能是在网页端创建Android项目,填入项目名和包名,要能够在后台生成一个Android Studio目录的工程,然后提供给用户。接到这个需求的时候我是一脸懵逼的,开发者要这玩意有啥用啊,我用Android Studio创建工程不是更方便?然而领导说了要做,那就做呗。于是有了以下思路:
在后台放一个标准的Android Studio工程,通过前台传递过来的项目名和包名去修改项目中用到包名的文件,替换成用户输入的包名就ok了。哇,多么简单,想好方案后汇报给领导。
领导说:“你这个确实可以这个功能,但是有没有逼格更高一点的方法?
我:
好吧,领导发话了还能咋办?原来的方法被搁下了,额,猜想出以下两种高逼格的方式:
- 通过控制Android Studio开发工具去生成项目,当然不是手动操作,是通过某种方式,比如命令的方式去操作开发工具,进而生成项目。
- 通过一行命令的方式,命令中传入包名、项目名,动态创建工程中所需要的各种文件,进而生成完整的工程目录。
好吧,最后的结果是:第一种pass,Android Studio好像没有给我们提供这样一个操作它的工具;至于第二种:好像可行,但是想想要自己创建那么多文件和文件夹就头皮发麻,一度想要放弃-。-于是绝望的去百度搜索:如何用命令行生成Android项目,一看,哇,有好多博客呀,都是讲如何用命令去生成Android工程:
然后点进去一看,运行了命令:
android create project -n HelloWorld -t android-25 -p HelloWorld -k top.overcode.helloworld -a HelloWorld
其中,-n指定要创建的项目的名称,-t指定项目针对的Android的平台,-p指定该项目的保存路径,-k指定该项目的包名,-a选项指定Activity的名称。 然后生成了工程,目录结构如下:
这尼玛,是Eclipse结构的工程啊。。。。。。。。
好吧,搜索良久无果,决定看看Android Studio到底如何生成工程的。于是在As安装目录下闲逛了良久,无意中发现以下目录:
D:\Android\as\plugins\android\lib\templates
templates,模板,会不会有模板工程之类的东西呢?点进去的目录如下:
gradle-projects?Android Studio不就是gradle项目么?继续深入
NewAndroidProject,新的Android项目,似乎有点像了,继续
D:\Android\as\plugins\android\lib\templates\gradle-projects\NewAndroidProject\root
发现许多个.ftl结尾的文件,
点开build.gradle.ftl文件,发现长这样
咦,这不就是我们的根目录的build.gradle文件么?和项目中的一对比:
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
发现classpath 'com.android.tools.build:gradle:3.2.1’中的版本号似乎是从外界传递进来的。于是去搜索了.ftl文件的用途,说是模板文件,于是猜测这些文件是项目文件的模板文件,Android Studio在创建项目的时候,把我们输入的如包名、项目名等内容通过某种方式传递到模板文件中来。于是在搜索ftl的同时,有提到了FreeMarker,这又是啥呢?
嗯,大体意思就是用来改变模板中的数据的,是一个java类库,可以操作.ftl文件,向其中传入参数,.ftl文件可以获取到传递过来的参数,这样只需要模板文件,然后通过FreeMarker操作模板文件就可以生成Android项目中所需要的文件了,到此研究似乎有了一些进展。那Android Studio是不是也是通过这种方式呢?于是在Android Studio的安装目录下疯狂搜索:
至此可以确定,Android Studio是通过这种方式来生成工程中的项目文件的。即模板文件+FreeMarker模板引擎。
于是看下标准的Android项目的目录:
.gradle目录和gradle目录和目录结构层次比较深,好像不好去创建这么多的目录和文件,于是想到了gradle似乎有一个命令:
gradle init
前提是要配置gradle的环境变量哦,这里就不说怎么配置了,自行百度~执行完发现:
哇咔咔,根目录下的文件夹几乎都生成完了,只剩一个app目录了,这个没法通过命令去生成了。app目录下:
这么一看,我们只需要:
- 创建一个libs目录和src目录。
- 用模板生成文本文件。
然后src中也一样,有文件夹就创建文件夹,需要创建文件就用模板文件生成。依次递进文件夹,直到该目录下只剩文件为止。看似很难,其实很简单啦。举个例子,比如src目录下有如下路径:
androidTest/java/com/suning/demo
为啥是这个路径,因为我们的包名是com.suning.demo。前面androidTest/java是固定的。两者拼接就是最终的文件目录,直到ExampleInstrumentedTest.java类
这个文件需要替换的就这么两处。所以对于androidTest/java这个目录下,我们只需要:
- 根据包名递进创建目录com/suning/demo。
- 模板文件替换ExampleInstrumentedTest.java类中的包名。
然后AndroidManifest.xml、gradle文件中中需要替换的包名同理。文件和目录的创建都搞定了,还有资源文件,怎么办呢?这个有图片啥的,不可能手动创建,去一个标准的工程下复制整个res目录进来就行。好了,所需要的东西我们都有了,那么创建文件夹、生成文件、移动资源目录到特定的文件夹,我们要如何去弄呢?当然是脚本啦,在此之前我们要看下如何通过Freemarker去动态替换.ftl中的内容,需要Freemarker.jar,然后看如下代码:
/*
* 生成AndroidManifest文件
* */
public static void createAndroidManifest() throws Exception {
Template template = getTemplate("AndroidManifest.xml.ftl");
FileWriter fileWriter = new FileWriter(new File(PROJECT_ROOT+"/app/src/main/AndroidManifest.xml"));
Map<Object, Object> map = new HashMap<>();
map.put("packageName", PACKGE_NAME);
template.process(map, fileWriter);
fileWriter.close();
}
上面代码是用来生成AndroidManifest.xml文件的:
- 首先获取模板文件构造成Template对象。
- 一个输入流指向要生成的AndroidManifest.xml文件。
- 把需要传递的参数通过map集合存放。
- 以map和输入流为参数,输出最终生成的文件。
- 关闭流。
再看下AndroidManifest.xml.ftl是如何获取传递进来的参数的:
packageName就是我们map集合中的一个键,${packageName}即可取到这个键所对应的值。模板文件的创建都同理。至于在命令行中如何获取我们输入的项目名和包名,并传递给java类去操作模板文件,这又涉及到shell 命令的语法了。懂得忽略,不懂的私下百度,这里因为篇幅的原因不去详细介绍了。然后来总结下生成一个工程我们需要怎么做:
- 根据命令行传递的项目名新建工程的根目录。通过脚本执行gradle init命令生成根目录下gradle相关的东西。
- 编写好操作各个模板文件的java类(负责去操作所有需要通过模板生成的文件,需要Freemarker.jar的支持)。
- 一份标准的资源文件目录备用。
- 各个模板文件。
看下我的目录:
其中marker目录有以下模板文件:
这是一个工程中需要生成的所有文件,都需要通过模板去生成对应的文件。
build脚本的所有代码如下:
# @author Huxin 2019/2/2
echo "create project start..."
#check project name
#if empty
if [ -z "$1" ];then
echo "please input the project name,such as './bulid Test com.example '"
echo "create project fail !"
exit 0
fi
#if exist
if [ -d "$1" ]; then
echo "The project \"$1\" had exists, you must use another name"
echo "create project fail !"
exit 0
fi
#check project package name
if [ -z "$2" ];then
package="com.example.apps";
else
#package=$(echo $2 | tr '[A-Z]' '[a-z]' )
package=$2
fi
#get lowercase project name
var=$(echo $1 | tr '[A-Z]' '[a-z]' )
package_dir=$(echo $package | sed -e 's/\./\//g' )
#make project dir
mkdir $1
#into project root dir
cd $1
#init gradle
gradle init
if [ $? -eq 0 ]; then
#back tools root dir
cd ..
fi
#change code path
rm -rf $1/app/src/main/java/
mkdir -p $1/app/src/main/java/$package_dir
mkdir -p $1/app/src/test/java/$package_dir
mkdir -p $1/app/src/androidTest/java/$package_dir
cp -r res/ $1/app/src/main/res
sed -i "s/appName/$1/g" $1/app/src/main/res/values/strings.xml
#into project root dir
cd $1
echo "" >settings.gradle
echo "include ':app'" >settings.gradle
cd app
mkdir libs
#back project root dir
cd ..
cd ..
file=$4
# generate class
class=$(echo $file | awk -F '.' '{print $1}')
echo "start compile......."
# compile java class
javac -encoding UTF-8 -cp freemarker.jar Generate.java
if [ $? -eq 0 ]; then
echo "compile success,ready run..."
# run
java -cp ".;freemarker.jar" Generate "$1" "$2"
if [ $? -eq 0 ]; then
echo "run complete!"
else
echo "run error!"
fi
else
echo "compile error!"
fi
rm -f Generate.class
echo "All has been done !"
大体步骤如下:
- 通过传递的项目名建立项目根目录。
- 执行gradle init命令,生成根目录下gradle相关的目录和文件夹。
- 创建对应的文件夹,并拷贝资源文件。
- 通过java类去输出所有需要生成的工程文件,如build.gradle、AndroidManifest.xml、MainActivity.java等等。。。。
- 删除编译后的操作类的字节码文件。
好吧,还有个Generate.java文件,用来生成所有需要生成的文件,代码如下:
import java.io.File;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import freemarker.template.Configuration;
import freemarker.template.Template;
public class Generate {
//当前java文件所在的路径,即这个工具所在的根目录。
public final static String ROOT_PATH = Generate.class.getResource("").getPath();
//模板文件所在的路径。
public final static String FTL_PATH = ROOT_PATH+"/marker";
//项目根目录。
public static String PROJECT_ROOT = "";
//包名
public static String PACKGE_NAME = "";
public static void main(String[] args) throws Exception {
//传递进来三个参数,第一个是项目名,第二个是包名。
PROJECT_ROOT = args[0];
PACKGE_NAME = args[1];
createBuildGradle();
createAppBuildGradle(PROJECT_ROOT+"/app");
createAndroidManifest();
createIgnoreAndProguardFiles();
createTestFiles();
createMainActivity();
}
//生成module的build.gradle文件
private static void createAppBuildGradle(String appPath) throws Exception {
Template template = getTemplate("build.gradle1.ftl");
File file = new File(appPath+"/build.gradle");
if(!file.exists()) {
file.createNewFile();
}
FileWriter fileWriter = new FileWriter(file);
Map<Object, Object> map = new HashMap<>();
map.put("packageName", PACKGE_NAME);
template.process(map, fileWriter);
fileWriter.close();
}
/*
* 生成项目的build.gradle
* */
public static void createBuildGradle() throws Exception{
// 在模板文件目录中找到名称为name的文件
Template template = getTemplate("build.gradle.ftl");
FileWriter fileWriter = new FileWriter(new File(PROJECT_ROOT+"/build.gradle"));
Map<Object, Object> map = new HashMap<>();
map.put("gradlePluginVersion", "3.2.1");
template.process(map, fileWriter);
fileWriter.close();
}
/*
* 生成AndroidManifest文件
* */
public static void createAndroidManifest() throws Exception {
Template template = getTemplate("AndroidManifest.xml.ftl");
FileWriter fileWriter = new FileWriter(new File(PROJECT_ROOT+"/app/src/main/AndroidManifest.xml"));
Map<Object, Object> map = new HashMap<>();
map.put("packageName", PACKGE_NAME);
template.process(map, fileWriter);
fileWriter.close();
}
/*创建混淆文件和忽略文件*/
public static void createIgnoreAndProguardFiles() throws Exception{
//project下的忽略文件
Template template = getTemplate(".gitignore.ftl");
File projectIgnoreFile = new File(PROJECT_ROOT+"/.gitignore");
if(!projectIgnoreFile.exists()) {
projectIgnoreFile.createNewFile();
}
FileWriter fileWriter = new FileWriter(projectIgnoreFile);
Map<Object, Object> map = new HashMap<>();
template.process(map, fileWriter);
//module下的忽略文件
Template template1 = getTemplate(".gitignore1.ftl");
File moduleIgnoreFile = new File(PROJECT_ROOT+"/app/.gitignore");
if(!moduleIgnoreFile.exists()) {
moduleIgnoreFile.createNewFile();
}
FileWriter fileWriter1 = new FileWriter(moduleIgnoreFile);
Map<Object, Object> map1 = new HashMap<>();
template1.process(map1, fileWriter1);
fileWriter1.close();
//创建混淆文件
Template template2 = getTemplate("proguard-rules.pro.ftl");
File proguardFile = new File(PROJECT_ROOT+"/app/proguard-rules.pro");
if(!proguardFile.exists()) {
proguardFile.createNewFile();
}
FileWriter fileWriter2 = new FileWriter(proguardFile);
Map<Object, Object> map2 = new HashMap<>();
template2.process(map2, fileWriter2);
//关流。
fileWriter.close();
fileWriter1.close();
fileWriter2.close();
}
/*
* 创建Test文件
* */
public static void createTestFiles() throws Exception{
//创建ExampleUnitTest类
Template template = getTemplate("ExampleUnitTest.java.ftl");
String [] packages = PACKGE_NAME.split("\\.");
String testFilePath = PROJECT_ROOT+"/app/src/test/java";
for(int i=0;i<packages.length;i++) {
testFilePath = testFilePath+"/"+packages[i];
}
File testFile = new File(testFilePath+"/ExampleUnitTest.java");
if(!testFile.exists()) {
testFile.createNewFile();
}
FileWriter fileWriter = new FileWriter(testFile);
Map<Object, Object> map = new HashMap<>();
map.put("packageName", PACKGE_NAME);
template.process(map, fileWriter);
//创建ExampleInstrumentedTest类
Template template1 = getTemplate("ExampleInstrumentedTest.java.ftl");
String androidTestFilePath = PROJECT_ROOT+"/app/src/androidTest/java";
for(int i=0;i<packages.length;i++) {
androidTestFilePath = androidTestFilePath+"/"+packages[i];
}
File androidTestFile = new File(androidTestFilePath+"/ExampleInstrumentedTest.java");
if(!androidTestFile.exists()) {
androidTestFile.createNewFile();
}
FileWriter fileWriter1 = new FileWriter(androidTestFile);
Map<Object, Object> map1 = new HashMap<>();
map1.put("packageName", PACKGE_NAME);
template1.process(map1, fileWriter1);
fileWriter.close();
fileWriter1.close();
}
//创建MainActivity类。
public static void createMainActivity() throws Exception{
Template template = getTemplate("MainActivity.java.ftl");
String [] packages = PACKGE_NAME.split("\\.");
String mainActivityPath = PROJECT_ROOT+"/app/src/main/java";
for(int i=0;i<packages.length;i++) {
mainActivityPath = mainActivityPath+"/"+packages[i];
}
File mainActivityFile = new File(mainActivityPath+"/MainActivity.java");
if(!mainActivityFile.exists()) {
mainActivityFile.createNewFile();
}
FileWriter fileWriter = new FileWriter(mainActivityFile);
Map<Object, Object> map = new HashMap<>();
map.put("packageName", PACKGE_NAME);
template.process(map, fileWriter);
fileWriter.close();
}
public static Template getTemplate(String templateName) throws Exception{
Configuration configuration = new Configuration();
configuration.setDirectoryForTemplateLoading(new File(FTL_PATH));
Template template = configuration.getTemplate(templateName);
return template;
}
}
这个类是用来通过模板文件来生成工程中所有的文件的,是生成文件的核心类,当然需要Freemarker jar包的支持。到此我们通过命令生成Android Studio目录结构的Android工程就已经完成了。最终整个工具的目录如下:
说到底就是一个脚本,一个java类就可以了,主要是构建这样一个工具的思路。这个脚本可以在windows下执行,也可以在linux下执行。先看windows下,首先你需要一个shell 环境,但是windows下默认是没有shell环境的,你可以下载Git for Windows,下载地址:Git for Windows,下载完成,一路默认安装,安装完成就可以使用了,在桌面或任何文件目录中,点击右键菜单中会有Git Bash Here选项:
但是要使用这个工具,我们需要在tools文件夹下右击,选择Git Bash Here,如下:
然后执行如下命令:
./build Demo com.suning.demo
可以看到如下效果:
表示工程生成成功了。其中Demo是项目名,com.suning.demo是项目的包名。有两点需要注意:
1. tools文件夹所在的目录不要包含中文,可能会出现乱码。
2. 执行命令的时候一定要切换到tools目录下,无论是windows还是linux。
看下生成的工程:
这个项目是可以直接用Android Studio来运行的。当然你可以修改这个工具,改变某个文件想要改变的地方都可以,感兴趣的自己可以改一改,没啥难度。哦,对了,要在linux下运行,build文件要修改一处地方:
把其中的分号改成冒号就行:
java -cp ".:freemarker.jar" Generate "$1" "$2"
不然在linux下运行会报错。所有的代码都上传到Gihub上了,代码传送门。
总结:其实这个工具也挺鸡肋的,创建工程还是Android Studio来的方便,正常都不会用到,公司所开发的移动开发平台,要求网页端可以创建Android工程,并可以动态选择项目的依赖库(如Okhtp、Glide、Retrofit、RxJava等),然后生成工程的时候,远程调用命令,把所选依赖库的配置代码写入到gradle文件中,省去用户自行配置的时间,要说倒也算是优点功能,不过有没有人用还是个未知数,毕竟作为开发者,我还是更相信开发工具生成的项目,完-。-