使用命令列建立Android Studio專案

無心.發表於2019-03-17

前言:前段時間,公司要開發一個移動開發平臺,有一個功能是在網頁端建立Android專案,填入專案名和包名,要能夠在後臺生成一個Android Studio目錄的工程,然後提供給使用者。接到這個需求的時候我是一臉懵逼的,開發者要這玩意有啥用啊,我用Android Studio建立工程不是更方便?然而領導說了要做,那就做唄。於是有了以下思路:

在後臺放一個標準的Android Studio工程,通過前臺傳遞過來的專案名和包名去修改專案中用到包名的檔案,替換成使用者輸入的包名就ok了。哇,多麼簡單,想好方案後彙報給領導。

領導說:“你這個確實可以這個功能,但是有沒有逼格更高一點的方法?
我:


在這裡插入圖片描述

好吧,領導發話了還能咋辦?原來的方法被擱下了,額,猜想出以下兩種高逼格的方式:

  1. 通過控制Android Studio開發工具去生成專案,當然不是手動操作,是通過某種方式,比如命令的方式去操作開發工具,進而生成專案。
  2. 通過一行命令的方式,命令中傳入包名、專案名,動態建立工程中所需要的各種檔案,進而生成完整的工程目錄。

好吧,最後的結果是:第一種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目錄下:


在這裡插入圖片描述

這麼一看,我們只需要:

  1. 建立一個libs目錄和src目錄。
  2. 用模板生成文字檔案。

然後src中也一樣,有資料夾就建立資料夾,需要建立檔案就用模板檔案生成。依次遞進資料夾,直到該目錄下只剩檔案為止。看似很難,其實很簡單啦。舉個例子,比如src目錄下有如下路徑:

androidTest/java/com/suning/demo

為啥是這個路徑,因為我們的包名是com.suning.demo。前面androidTest/java是固定的。兩者拼接就是最終的檔案目錄,直到ExampleInstrumentedTest.java類


在這裡插入圖片描述

這個檔案需要替換的就這麼兩處。所以對於androidTest/java這個目錄下,我們只需要:

  1. 根據包名遞進建立目錄com/suning/demo。
  2. 模板檔案替換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檔案的:

  1. 首先獲取模板檔案構造成Template物件。
  2. 一個輸入流指向要生成的AndroidManifest.xml檔案。
  3. 把需要傳遞的引數通過map集合存放。
  4. 以map和輸入流為引數,輸出最終生成的檔案。
  5. 關閉流。

再看下AndroidManifest.xml.ftl是如何獲取傳遞進來的引數的:


在這裡插入圖片描述

packageName就是我們map集合中的一個鍵,${packageName}即可取到這個鍵所對應的值。模板檔案的建立都同理。至於在命令列中如何獲取我們輸入的專案名和包名,並傳遞給java類去操作模板檔案,這又涉及到shell 命令的語法了。懂得忽略,不懂的私下百度,這裡因為篇幅的原因不去詳細介紹了。然後來總結下生成一個工程我們需要怎麼做:

  1. 根據命令列傳遞的專案名新建工程的根目錄。通過指令碼執行gradle init命令生成根目錄下gradle相關的東西。
  2. 編寫好操作各個模板檔案的java類(負責去操作所有需要通過模板生成的檔案,需要Freemarker.jar的支援)。
  3. 一份標準的資原始檔目錄備用。
  4. 各個模板檔案。

看下我的目錄:


在這裡插入圖片描述
其中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 !"



大體步驟如下:

  1. 通過傳遞的專案名建立專案根目錄。
  2. 執行gradle init命令,生成根目錄下gradle相關的目錄和資料夾。
  3. 建立對應的資料夾,並拷貝資原始檔。
  4. 通過java類去輸出所有需要生成的工程檔案,如build.gradle、AndroidManifest.xml、MainActivity.java等等。。。。
  5. 刪除編譯後的操作類的位元組碼檔案。

好吧,還有個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檔案中,省去使用者自行配置的時間,要說倒也算是優點功能,不過有沒有人用還是個未知數,畢竟作為開發者,我還是更相信開發工具生成的專案,完-。-

相關文章