EnvironmentPlugin 一款用來配置可動態切換App環境的Gradle插件

簡介

gradle中配置開發時的所有環境,你只需要很少的代碼就能實現環境動態切換的功能。而在打生產包時你只需要在gradle中修改release的值爲true就能將非生產環境剔除(不會將非生產環境打包到Apk中),從而保證非生產環境不會泄漏。在gradle中配置完成後只需要clean一下就會在BuildConfig的目錄中生成一個EnvConfig的類,你只需要通過EnvConfig.getEnv()方法就能獲取到所有你在gradle中配置的值,而此時你不需要關心自己當前處於哪個環境。切換環境時你只需要調用EnvConfig.setEnv(Type type)方法切換當前環境即可。
#GitHub
如果你想get源碼,請點擊https://github.com/kelinZhou/EnvironmentPlugin

體驗

點擊下載或掃碼下載DemoApk

DemoApk

下載

第一步:添加 gradlew plugins 倉庫到你項目根目錄的 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 "生產"

        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 "開發"

        variable "API_HOST", "192.168.30.11"
        variable "API_PORT", "8016"
        variable "UM_APP_KEY", '7c2ed9f7f1d5eefff'
    }

    testEnv {
        alias "測試"

        variable "API_HOST", "192.168.36.18"
        variable "UM_APP_KEY", '7c2ed9f7f1d5eebbb'
    }

    demoEnv {
        alias "預發"

        variable "API_HOST", "192.168.36.10"
        variable "UM_APP_KEY", '7c2ed9f7f1d5eeaaa'
    }
}

android{
    //...省略N多行代碼
}

不要看代碼這麼多,其實很簡單,你甚至可以直接拷貝我這裏的代碼進行使用。只是要把devConfigreleaseConfig這兩個Extension中的值替換成你自己的就可以了。

#####下面主要來講一下這些參數的含義。
######一、release&initEnvironment。

1).release:用來配置當前如果要打包的話是打生產包還是要打開發包,如果要打生產包着要把改值設置爲true(release true),否者需要把值設置爲false(release false)。一旦設置爲true之後無論你是打debug(調試)包還是release(簽名)包,initEnvironment的值將會無效,而且除了releaseConfig和releaseEnv以外的其他配置包括:devConfig、devEnv、testEnv和demoEnv都會失效,他們是不會被打包到.apk文件中的,否則就會。

2).initEnvironment:用來配置當前應用安裝到一臺設備上之後默認是什麼環境,這裏有以下這些值可以選擇。

參數 說明
“release” 生產環境。
“dev” 開發環境。
“test” 測試環境。
“demo” 預發佈環境。

**注意:**只是在新安裝到一個設備上纔會有效,如果是覆蓋安裝的話則不一定有效,因爲你上一個版本可能做過環境切換,當切換後即使覆蓋安裝也會繼續使用之前的環境,這是爲了保證環境不會被莫名其妙的切換。還有就是改參數也可以不配置,如果不配置,這默認會是"release",並且該配置項只有在releasetrue的時候纔是有意義的,但你也沒有必要把release改爲true後就刪除該配置,一直留着就行,留着不會有任何影響。

######二、devConfigreleaseConfig這兩個Extension。
來說下這兩個Extension中都是有哪些配置。
1).appIcon:devConfig中用來配置開發時的應用圖標,releaseConfig用來配置打成生產包後的圖標。做要作用是用來在沒有打開應用的時候區分當前是生產包還是開發包。

2).appRoundIcon:devConfig中用來配置開發時Android7.0的圓形應用圖標,releaseConfig用來配置生產包的Android7.0的圓形應用圖標。做要作用是用來在沒有打開應用的時候區分當前是生產包還是開發包。(如果不做Android7.0圓形圖標適配的話可以不配置改參數)

3).appName:devConfig中用來配開發時的應用名稱,releaseConfig用來配置打成生產包後的應用名稱。做要作用是用來在沒有打開應用的時候區分當前是生產包還是開發包。

4).versionCode:devConfig中用來配開發時的版本號,releaseConfig用來配置打成生產包後的版本號。做要作用是用來分離開發包和生產包的不同版本,因爲在開發過程中我們可能要出很多個版本這時你只需要修改devConfig中的viersionCode的值就可以了。改配置可以省略,如果省略者會用versionName的值來代替,例如versionName="1.0.1"那麼versionCode的值就爲101,這也就意味着如果你沒有配置versionCode那麼你一定要配置versionName。你可以在clean過後通過BuildConfig類文件查看到這兩個值。

5).versionName:devConfig中用來配開發時的版本名,releaseConfig用來配置打成生產包後的版本名。做要作用與versionCode的主要作用一致。同樣versionName的配置也是可以省略的,如果省略着會使用versionCode轉字符串的方法爲versionName賦值,例如versionCode=121那麼versionName的值就爲1.2.1,如果versionName的值不足3位數,那麼將會在前面補0,例如versionCode=2那麼versionName的值就爲0.0.2。如果你的versionCode與versionName的規則與我的不一致那麼你就需要兩個都進行配置,例如:

versionCode 201908
versionName "1.4.05"

6).applicationId:通常情況下你不需要使用該參數進行applicationId的配置,因爲這個就是你的應用包名(packageName),而包名不應該有多個,如果你有多個且有分享功能or三方登錄功能的話你就要針對不同的包名申請不同的key,這是比較麻煩的,但是有的時候又確實需要不同的包名進行測試,例如推送Push服務(例如極光推送),如果你沒有分離包名這有可能造成生產環境下的用戶都能收到推送消息,如果項目還沒有上線還好,但是如果上線了,這就會給用戶帶來不必要的困擾。所以,這個參數配置根據你的義務場景酌情選擇。

devConfigreleaseConfig這兩個Extension中就只有這麼多的配置,But你只在這裏配置過了並不會有什麼作用,你需要在正常配置這些參數的地方使用它。就像下面這樣。
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}"//使用environment中配置的appName
            android:roundIcon="${APP_ROUND_ICON}"//使用environment中配置的appRoundIcon
            android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

**注意:**通過這種方式配置後,當你的release的值爲true時使用的就是releaseConfig中的配置,否則使用的就是devConfig中的配置。還有一點需要注意就是appIconappRoundIconappName這三項配置在devConfigreleaseConfig這兩個Extension中必須保證對等出現,例如你在devConfig中使用了appIcon這個配置,那麼就必須要在releaseConfig中也配置appIcon否者在manifest清單文件中使用他們時是會報錯的。

你也可能會覺得這麼配置比較麻煩,首先聲明我覺得不麻煩,就算是麻煩也就這一次,以後每次出包的時候省掉了很多修改。其次如果你實在不需要這樣配,那麼devConfigreleaseConfig這兩個Extension也是可以省略不配的。就像下面這樣:

environment {
    release false

    initEnvironment "test"

    releaseEnv {
        alias "生產"

        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 "開發"

        variable "API_HOST", "192.168.30.11"
        variable "API_PORT", "8016"
        variable "UM_APP_KEY", '7c2ed9f7f1d5eefff'
    }

    testEnv {
        alias "測試"

        variable "API_HOST", "192.168.36.18"
        variable "UM_APP_KEY", '7c2ed9f7f1d5eebbb'
    }

    demoEnv {
        alias "預發"

        variable "API_HOST", "192.168.36.10"
        variable "UM_APP_KEY", '7c2ed9f7f1d5eeaaa'
    }
}

像這樣的話你就通過正常的配置來配置你的項目就可以了。

######三、releaseEnv 、devEnv 、testEnv 和demoEnv 。
releaseEnvdevEnvtestEnvdemoEnv中除了releaseEnv以外,其他的都是非必須的,例如你只有開發環境和生產環境,你只需要配置releaseEnvdevEnv這兩個就行了。因爲releaseEnv是必須要配置的,所以你的releaseEnv中的環境變量(姑且先這麼稱呼吧,環境變量)必須是最全的,而其他的只需要配置與releaseEnv中不同的環境變量即可。如果其他的環境配置中包含了releaseEnv中沒有的環境變量則會被捨棄。

這些環境配置中一共有兩個方法,他們的使用方式及作用如下:

1).alias:別名,用來配置當前環境變量的可讀性更好的(你自己更加容易讀懂的)名稱,這個別名的值將出現在EnvConfig.Type枚舉中,可以通過EnvConfig.Type.alias獲取,也可以用作切換環境時的展示名。可以省略,如果省略則默認爲他們對應的英文名,例如releaseEnv默認的別爲爲Release。配置方式:

alias "生產" //配置當前環境配置的別名爲生產。

2).variable:聲明環境變量,用於生成一個環境變量。它一共有三個參數:

參數名 樣例 可省略 說明
name “API_HOST” 變量名稱,將會生成代碼到Environment類文件中,必須遵循Java的命名規範,使用時通過EnvConfig.getEnv().變量名調用,例如EnvConfig.getEnv().API_HOST
value “192.168.31.24” 變量值,將會生成代碼到Environment類文件中,使用時通過EnvConfig.getEnv().變量名調用,例如EnvConfig.getEnv().API_HOST
placeholder true 是否同時生成到manifestPlaceholder,如果爲true則可以在Manifest清單文件中使用,否則不行。可以省略,如果省略默認爲false。該參數在不通的環境配置中只設置一次也可以,就像栗子中的UM_APP_KEY,在release中設置爲了true,其他的環境配置中就可以省略。

clean項目。

以上都配置好以後像下面這樣clean一下項目,或者點擊一下gradle右上角的Sync Now。
Clean項目

然後你就能得到下面這兩個類,EnvConfig 和 Environment。

生成的環境配合的類的位置

EnvConfig

release爲false時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("生產"),

    DEV("開發"),

    TEST("測試"),

    DEMO("預發");

    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時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("生產");

    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時和爲true時,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方法中進行初始化。其實初始化只有在release=false時纔有意義,但是你不需要在調用之前判斷當前是不是生產包(儘管你可以通過EnvConfig.IS_RELEASE來判斷gradle中release的值),因爲在release=true時EnvConfig中依然保留了init方法,只是變成了空實現而已,所以你直接調用即可。

public class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        EnvConfig.init(this);  //初始化環境配置。
    }
}

獲取當前環境。

你可以在任何時候通過EnvConfig.getEnv()來獲取當前的環境變量,樣例代碼如下:

 Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(EnvConfig.getEnv().API_HOST) //使用當前環境的主機地址構建Retrofit.
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();

####切換當前環境。
你可以在任何時候通過EnvConfig.setEnvType(Type type)來切換環境,這裏的參數是一個EnvConfig.Type枚舉,**雖然這裏的參數是一個EnvConfig.Type枚舉,但是你絕對不可以使用EnvConfig.Type.RELEASE以外的其他任何枚舉。**因爲一旦你將gradle中release的值改爲true,除EnvConfig.Type.RELEASE以外的其他枚舉都將不存在。如果你使用了,將會報錯。甚至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}環境"
        button.setOnClickListener {
            ToastUtil.showShortToast("已切換至${type.alias}環境")
            dialog.dismiss()
            onSwitch(type)
        }
    }
    dialog.show()
}

然後在調用的地方像下面這樣寫:

if (!EnvConfig.IS_RELEASE) {
    showEnvSwitcherDialog(requireActivity()) {
        EnvConfig.setEnvType(it)
        //TODO 執行切換環境的邏輯,比如重新創建網絡請求(如Retrofit、OkHttp、GRpc等)。
    }
}

好了先寫到這裏,不知不覺也囉嗦了挺多了。如果你覺得我寫的還可以的話,fork
star或者點一個👍、一個賞,都是對我繼續創作下去的鼓勵。再次感謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章