簡(jiǎn)介
在gradle中配置開發(fā)時(shí)的所有環(huán)境,你只需要很少的代碼就能實(shí)現(xiàn)環(huán)境動(dòng)態(tài)切換的功能。而在打生產(chǎn)包時(shí)你只需要在gradle中修改release的值為true就能將非生產(chǎn)環(huán)境剔除(不會(huì)將非生產(chǎn)環(huán)境打包到Apk中),從而保證非生產(chǎn)環(huán)境不會(huì)泄漏。在gradle中配置完成后只需要clean一下就會(huì)在BuildConfig的目錄中生成一個(gè)EnvConfig的類,你只需要通過EnvConfig.getEnv()方法就能獲取到所有你在gradle中配置的值,而此時(shí)你不需要關(guān)心自己當(dāng)前處于哪個(gè)環(huán)境。切換環(huán)境時(shí)你只需要調(diào)用EnvConfig.setEnv(Type type)方法切換當(dāng)前環(huán)境即可。
GitHub
如果你想get源碼,請(qǐng)點(diǎn)擊https://github.com/kelinZhou/EnvironmentPlugin。
體驗(yàn)
下載
第一步:添加 gradlew plugins 倉(cāng)庫(kù)到你項(xiàng)目根目錄的 gradle 文件中。
buildscript {
repositories {
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "gradle.plugin.com.kelin.environment:environment:1.1.2"
}
}
第二步:在module中引入插件。
apply plugin: "com.kelin.environment"
效果圖

使用
在App gradle中添加如下配置。
environment {
release false
initEnvironment "test"
devConfig {
appIcon "@mipmap/ic_android"
appRoundIcon "@mipmap/ic_android"
appName "EnvPlugin"
// versionCode 100
versionName "1.0.0"
applicationId "${packageName}.test"
}
releaseConfig {
appIcon "@mipmap/ic_launcher"
appRoundIcon "@mipmap/ic_launcher"
appName "@string/app_name"
// versionCode 200
versionName "2.0.0"
applicationId packageName
}
releaseEnv {
alias "生產(chǎn)"
variable "API_HOST", "192.168.31.24"
variable "API_PORT", "8443"
variable "WX_APP_ID", "wxc23iadfaioaiuu0a"
variable "WX_APP_SECRET", "ioa9ad9887ad98ay979axxx"
variable "UM_APP_KEY", '7c2ed9f7f1d5ecccc', true
}
devEnv {
alias "開發(fā)"
variable "API_HOST", "192.168.30.11"
variable "API_PORT", "8016"
variable "UM_APP_KEY", '7c2ed9f7f1d5eefff'
}
testEnv {
alias "測(cè)試"
variable "API_HOST", "192.168.36.18"
variable "UM_APP_KEY", '7c2ed9f7f1d5eebbb'
}
demoEnv {
alias "預(yù)發(fā)"
variable "API_HOST", "192.168.36.10"
variable "UM_APP_KEY", '7c2ed9f7f1d5eeaaa'
}
}
android{
//...省略N多行代碼
}
不要看代碼這么多,其實(shí)很簡(jiǎn)單,你甚至可以直接拷貝我這里的代碼進(jìn)行使用。只是要把devConfig和releaseConfig這兩個(gè)Extension中的值替換成你自己的就可以了。
下面主要來講一下這些參數(shù)的含義。
一、release&initEnvironment。
1).release:用來配置當(dāng)前如果要打包的話是打生產(chǎn)包還是要打開發(fā)包,如果要打生產(chǎn)包著要把改值設(shè)置為true(release true),否者需要把值設(shè)置為false(release false)。一旦設(shè)置為true之后無(wú)論你是打debug(調(diào)試)包還是release(簽名)包,initEnvironment的值將會(huì)無(wú)效,而且除了releaseConfig和releaseEnv以外的其他配置包括:devConfig、devEnv、testEnv和demoEnv都會(huì)失效,他們是不會(huì)被打包到.apk文件中的,否則就會(huì)。
2).initEnvironment:用來配置當(dāng)前應(yīng)用安裝到一臺(tái)設(shè)備上之后默認(rèn)是什么環(huán)境,這里有以下這些值可以選擇。
| 參數(shù) | 說明 |
|---|---|
| "release" | 生產(chǎn)環(huán)境。 |
| "dev" | 開發(fā)環(huán)境。 |
| "test" | 測(cè)試環(huán)境。 |
| "demo" | 預(yù)發(fā)布環(huán)境。 |
注意:只是在新安裝到一個(gè)設(shè)備上才會(huì)有效,如果是覆蓋安裝的話則不一定有效,因?yàn)槟闵弦粋€(gè)版本可能做過環(huán)境切換,當(dāng)切換后即使覆蓋安裝也會(huì)繼續(xù)使用之前的環(huán)境,這是為了保證環(huán)境不會(huì)被莫名其妙的切換。還有就是該參數(shù)也可以不配置,如果不配置,這默認(rèn)會(huì)是"release",并且該配置項(xiàng)只有在release為true的時(shí)候才是有意義的,你也沒有必要把release改為true后就刪除該配置,一直留著就行,留著不會(huì)有任何影響。
二、devConfig和releaseConfig這兩個(gè)Extension。
來說下這兩個(gè)Extension中都是有哪些配置。
1).appIcon:devConfig中用來配置開發(fā)時(shí)的應(yīng)用圖標(biāo),releaseConfig用來配置打成生產(chǎn)包后的圖標(biāo)。做要作用是用來在沒有打開應(yīng)用的時(shí)候區(qū)分當(dāng)前是生產(chǎn)包還是開發(fā)包。
2).appRoundIcon:devConfig中用來配置開發(fā)時(shí)Android7.0的圓形應(yīng)用圖標(biāo),releaseConfig用來配置生產(chǎn)包的Android7.0的圓形應(yīng)用圖標(biāo)。做要作用是用來在沒有打開應(yīng)用的時(shí)候區(qū)分當(dāng)前是生產(chǎn)包還是開發(fā)包。(如果不做Android7.0圓形圖標(biāo)適配的話可以不配置該參數(shù))。
3).appName:devConfig中用來配開發(fā)時(shí)的應(yīng)用名稱,releaseConfig用來配置打成生產(chǎn)包后的應(yīng)用名稱。做要作用是用來在沒有打開應(yīng)用的時(shí)候區(qū)分當(dāng)前是生產(chǎn)包還是開發(fā)包。
4).versionCode:devConfig中用來配開發(fā)時(shí)的版本號(hào),releaseConfig用來配置打成生產(chǎn)包后的版本號(hào)。主要作用是用來分離開發(fā)包和生產(chǎn)包的不同版本,因?yàn)樵陂_發(fā)過程中我們可能要出很多個(gè)版本這時(shí)你只需要修改devConfig中的viersionCode的值就可以了。改配置可以省略,如果省略則會(huì)使用默認(rèn)值,默認(rèn)的versionCode編碼規(guī)則(省略versionCode配置時(shí))為yyMMddHH格式(年份后兩位+月份+日期+小時(shí)),例如:2020年5月20號(hào) 5:20分打包的話默認(rèn)versionCode為20052005。如不希望使用這個(gè)規(guī)則,則需要手動(dòng)指定(不省略versionCode的配置)。
5).versionName:devConfig中用來配開發(fā)時(shí)的版本名,releaseConfig用來配置打成生產(chǎn)包后的版本名。做要作用與versionCode的主要作用一致。
6).applicationId:通常情況下你不需要使用該參數(shù)進(jìn)行applicationId的配置,因?yàn)檫@個(gè)就是你的應(yīng)用包名(packageName),而包名不應(yīng)該有多個(gè),如果你有多個(gè)且有分享功能or三方登錄功能的話你就要針對(duì)不同的包名申請(qǐng)不同的key,這是比較麻煩的,但是有的時(shí)候又確實(shí)需要不同的包名進(jìn)行測(cè)試,例如推送Push服務(wù)(例如極光推送),如果你沒有分離包名這有可能造成生產(chǎn)環(huán)境下的用戶都能收到推送消息,如果項(xiàng)目還沒有上線還好,但是如果上線了,這就會(huì)給用戶帶來不必要的困擾。所以,這個(gè)參數(shù)配置根據(jù)你的義務(wù)場(chǎng)景酌情選擇。
devConfig和releaseConfig這兩個(gè)Extension中就只有這么多的配置,But你只在這里配置過了并不會(huì)有什么作用,你需要在正常配置這些參數(shù)的地方使用它。就像下面這樣。
versionCode、versionName以及applicationId的使用
app gradle 中
android {
defaultConfig {
//使用environment中配置的versionCode
versionCode environment.versionCode
//使用environment中配置的versionName
versionName environment.versionName
//使用environment中配置的applicationId
applicationId environment.applicationId
}
}
appIcon、appRoundIcon以及appName的使用
AndroidManifest 清單文件中
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kelin.environmenttoolsdemo">
<application
android:allowBackup="true"
android:name=".App"
android:icon="${APP_ICON}" //使用environment中配置的appIcon
android:label="${APP_NAME}"http://使用environment中配置的appName
android:roundIcon="${APP_ROUND_ICON}"http://使用environment中配置的appRoundIcon
android:theme="@style/AppTheme">
<activity ...>
</application>
</manifest>
注意:通過這種方式配置后,當(dāng)你的release的值為true時(shí)使用的就是releaseConfig中的配置,否則使用的就是devConfig中的配置。還有一點(diǎn)需要注意就是appIcon、 appRoundIcon和 appName這三項(xiàng)配置在devConfig和releaseConfig這兩個(gè)Extension中必須保證對(duì)等出現(xiàn),例如你在devConfig中使用了appIcon這個(gè)配置,那么就必須要在releaseConfig中也配置appIcon否者在manifest清單文件中使用他們時(shí)是會(huì)報(bào)錯(cuò)的。
你也可能會(huì)覺得這么配置比較麻煩,首先聲明我覺得不麻煩,就算是麻煩也就這一次,以后每次出包的時(shí)候省掉了很多修改。其次如果你實(shí)在不需要這樣配,那么devConfig和releaseConfig這兩個(gè)Extension也是可以省略不配的。就像下面這樣:
environment {
release false
initEnvironment "test"
releaseEnv {
alias "生產(chǎn)"
variable "API_HOST", "192.168.31.24"
variable "API_PORT", "8443"
variable "WX_APP_ID", "wxc23iadfaioaiuu0a"
variable "WX_APP_SECRET", "ioa9ad9887ad98ay979axxx"
variable "UM_APP_KEY", '7c2ed9f7f1d5ecccc', true
}
devEnv {
alias "開發(fā)"
variable "API_HOST", "192.168.30.11"
variable "API_PORT", "8016"
variable "UM_APP_KEY", '7c2ed9f7f1d5eefff'
}
testEnv {
alias "測(cè)試"
variable "API_HOST", "192.168.36.18"
variable "UM_APP_KEY", '7c2ed9f7f1d5eebbb'
}
demoEnv {
alias "預(yù)發(fā)"
variable "API_HOST", "192.168.36.10"
variable "UM_APP_KEY", '7c2ed9f7f1d5eeaaa'
}
}
像這樣的話你就通過正常的配置來配置你的項(xiàng)目就可以了。
三、releaseEnv 、devEnv 、testEnv 和demoEnv 。
releaseEnv、devEnv、testEnv和demoEnv中除了releaseEnv以外,其他的都是非必須的,例如你只有開發(fā)環(huán)境和生產(chǎn)環(huán)境,你只需要配置releaseEnv和devEnv這兩個(gè)就行了。因?yàn)?code>releaseEnv是必須要配置的,所以你的releaseEnv中的環(huán)境變量(姑且先這么稱呼吧,環(huán)境變量)必須是最全的,而其他的只需要配置與releaseEnv中不同的環(huán)境變量即可。如果其他的環(huán)境配置中包含了releaseEnv中沒有的環(huán)境變量則會(huì)被舍棄。
這些環(huán)境配置中一共有兩個(gè)方法,他們的使用方式及作用如下:
1).alias:別名,用來配置當(dāng)前環(huán)境變量的可讀性更好的(你自己更加容易讀懂的)名稱,這個(gè)別名的值將出現(xiàn)在EnvConfig.Type枚舉中,可以通過EnvConfig.Type.alias獲取,也可以用作切換環(huán)境時(shí)的展示名??梢允÷?,如果省略則默認(rèn)為他們對(duì)應(yīng)的英文名,例如releaseEnv默認(rèn)的別為為Release。配置方式:
alias "生產(chǎn)" //配置當(dāng)前環(huán)境配置的別名為生產(chǎn)。
2).variable:聲明環(huán)境變量,用于生成一個(gè)環(huán)境變量。它一共有三個(gè)參數(shù):
| 參數(shù)名 | 樣例 | 可省略 | 說明 |
|---|---|---|---|
| name | "API_HOST" | 否 | 變量名稱,將會(huì)生成代碼到Environment類文件中,必須遵循Java的命名規(guī)范,使用時(shí)通過EnvConfig.getEnv().變量名調(diào)用,例如EnvConfig.getEnv().API_HOST
|
| value | "192.168.31.24" | 否 | 變量值,將會(huì)生成代碼到Environment類文件中,使用時(shí)通過EnvConfig.getEnv().變量名調(diào)用,例如EnvConfig.getEnv().API_HOST
|
| placeholder | true | 是 | 是否同時(shí)生成到manifestPlaceholder,如果為true則可以在Manifest清單文件中使用,否則不行。可以省略,如果省略默認(rèn)為false。該參數(shù)在不通的環(huán)境配置中只設(shè)置一次也可以,就像栗子中的UM_APP_KEY,在release中設(shè)置為了true,其他的環(huán)境配置中就可以省略。 |
clean項(xiàng)目。
以上都配置好以后像下面這樣clean一下項(xiàng)目,或者點(diǎn)擊一下gradle右上角的Sync Now。

然后你就能得到下面這兩個(gè)類,EnvConfig 和 Environment。

EnvConfig
release為false時(shí)EnvConfig的代碼如下:
public final class EnvConfig {
public static final boolean IS_RELEASE = Boolean.parseBoolean("false");
public static final Type INIT_ENV = Type.nameOf("test");
private static final Environment RELEASE_ENV = new EnvironmentImpl("192.168.31.24", "8443", "wxc23iadfaioaiuu0a", "ioa9ad9887ad98ay979ad86", "7c2ed9f7f1d5ecccc");
private static final Environment DEV_ENV = new EnvironmentImpl("192.168.30.11", "8016", "wxc23iadfaioaiuu0a", "ioa9ad9887ad98ay979ad86", "7c2ed9f7f1d5eefff");
private static final Environment TEST_ENV = new EnvironmentImpl("192.168.36.18", "8001", "wxc23iadfaioaiuu0a", "ioa9ad9887ad98ay979ad86", "7c2ed9f7f1d5eebbb");
private static final Environment DEMO_ENV = new EnvironmentImpl("192.168.36.10", "8109", "wxc23iadfaioaiuu0a", "ioa9ad9887ad98ay979ad86", "7c2ed9f7f1d5eeaaa");
private static Context context;
private static Type curEnvType;
EnvConfig() {
throw new RuntimeException("EnvConfig can't be constructed");
}
public static void init(Application app) {
context = app.getApplicationContext();
curEnvType = Type.nameOf(PreferenceManager.getDefaultSharedPreferences(context).getString("current_environment_type_string_name", INIT_ENV.name()));
}
public static boolean setEnvType(Type type) {
if (type != curEnvType) {
curEnvType = type;
if (context != null) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString("current_environment_type_string_name", type.name()).apply();
}
return true;
} else {
return false;
}
}
public static Type getEnvType() {
return curEnvType;
}
public static Environment getEnv() {
switch (curEnvType) {
case RELEASE:
return RELEASE_ENV;
case DEV:
return DEV_ENV;
case TEST:
return TEST_ENV;
case DEMO:
return DEMO_ENV;
default:
throw new RuntimeException("the type:" + curEnvType.toString() + " is unkonwn !");
}}
public enum Type {
RELEASE("生產(chǎn)"),
DEV("開發(fā)"),
TEST("測(cè)試"),
DEMO("預(yù)發(fā)");
public final String alias;
Type(String alias) {
this.alias = alias;
}
private static Type nameOf(String typeName) {
if (typeName != null) {
for (Type value : values()) {
if (value.name().toLowerCase().equals(typeName.toLowerCase())) {
return value;
}
}
}
return RELEASE;
}
}
private static final class EnvironmentImpl extends Environment {
EnvironmentImpl(String var0, String var1, String var2, String var3, String var4) {
super(var0, var1, var2, var3, var4);
}
}
}
release為true時(shí)EnvConfig的代碼如下:
public final class EnvConfig {
public static final boolean IS_RELEASE = Boolean.parseBoolean("true");
public static final Type INIT_ENV = Type.nameOf("release");
private static final Environment RELEASE_ENV = new EnvironmentImpl("192.168.31.24", "8443", "wxc23iadfaioaiuu0a", "ioa9ad9887ad98ay979ad86", "7c2ed9f7f1d5ecccc");
private static Type curEnvType = Type.RELEASE;
EnvConfig() {
throw new RuntimeException("EnvConfig can't be constructed");
}
public static void init(Application app) {
}
public static boolean setEnvType(Type type) {
return true;
}
public static Type getEnvType() {
return curEnvType;
}
public static Environment getEnv() {
return RELEASE_ENV;
}
public enum Type {
RELEASE("生產(chǎn)");
public final String alias;
Type(String alias) {
this.alias = alias;
}
private static Type nameOf(String typeName) {
return RELEASE;
}
}
private static final class EnvironmentImpl extends Environment {
EnvironmentImpl(String var0, String var1, String var2, String var3, String var4) {
super(var0, var1, var2, var3, var4);
}
}
}
Environment
release為false時(shí)和為true時(shí),Environment的代碼不變,代碼如下:
public abstract class Environment {
public final String API_HOST;
public final String API_PORT;
public final String WX_APP_ID;
public final String WX_APP_SECRET;
public final String UM_APP_KEY;
protected Environment(String var0, String var1, String var2, String var3, String var4) {
API_HOST = var0;
API_PORT = var1;
WX_APP_ID = var2;
WX_APP_SECRET = var3;
UM_APP_KEY = var4;
}
}
初始化。
在你的Application的onCreate方法中進(jìn)行初始化。其實(shí)初始化只有在release=false時(shí)才有意義,但是你不需要在調(diào)用之前判斷當(dāng)前是不是生產(chǎn)包(盡管你可以通過EnvConfig.IS_RELEASE來判斷gradle中release的值),因?yàn)樵?code>release=true時(shí)EnvConfig中依然保留了init方法,只是變成了空實(shí)現(xiàn)而已,所以你直接調(diào)用即可。
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
EnvConfig.init(this); //初始化環(huán)境配置。
}
}
獲取當(dāng)前環(huán)境。
你可以在任何時(shí)候通過EnvConfig.getEnv()來獲取當(dāng)前的環(huán)境變量,樣例代碼如下:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(EnvConfig.getEnv().API_HOST) //使用當(dāng)前環(huán)境的主機(jī)地址構(gòu)建Retrofit.
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
切換當(dāng)前環(huán)境。
你可以在任何時(shí)候通過EnvConfig.setEnvType(Type type)來切換環(huán)境,這里的參數(shù)是一個(gè)EnvConfig.Type枚舉,雖然這里的參數(shù)是一個(gè)EnvConfig.Type枚舉,但是你絕對(duì)不可以使用EnvConfig.Type.RELEASE以外的其他任何枚舉。因?yàn)橐坏┠銓radle中release的值改為true,除EnvConfig.Type.RELEASE以外的其他枚舉都將不存在。如果你使用了,將會(huì)報(bào)錯(cuò)。甚至EnvConfig.Type.RELEASE都不建議你直接使用。你可以像下面這樣使用:
@SuppressLint("SetTextI18n", "InflateParams")
fun showEnvSwitcherDialog(context: Activity, onSwitch: (envType: EnvConfig.Type) -> Unit) {
val custom = LayoutInflater.from(context).inflate(R.layout.dialog_env_switcher, null)
val dialog = AlertDialog.Builder(context, R.style.CommonWidgetDialog)
.setView(custom)
.setCancelable(true)
.create()
val buttons = listOf<Button>(
custom.findViewById(R.id.dialog_dev),
custom.findViewById(R.id.dialog_test),
custom.findViewById(R.id.dialog_release),
custom.findViewById(R.id.dialog_prepare)
)
EnvConfig.Type.values().forEachIndexed { index, type ->
val button = buttons[index]
button.visibility = View.VISIBLE
button.text = "${type.alias}環(huán)境"
button.setOnClickListener {
ToastUtil.showShortToast("已切換至${type.alias}環(huán)境")
dialog.dismiss()
onSwitch(type)
}
}
dialog.show()
}
然后在調(diào)用的地方像下面這樣寫:
if (!EnvConfig.IS_RELEASE) {
showEnvSwitcherDialog(requireActivity()) {
EnvConfig.setEnvType(it)
//TODO 執(zhí)行切換環(huán)境的邏輯,比如重新創(chuàng)建網(wǎng)絡(luò)請(qǐng)求(如Retrofit、OkHttp、GRpc等)。
}
}
好了先寫到這里,不知不覺也啰嗦了挺多了。如果你覺得我寫的還可以的話,fork
和star或者點(diǎn)一個(gè)??、一個(gè)賞,都是對(duì)我繼續(xù)創(chuàng)作下去的鼓勵(lì)。再次感謝。