文 | Promise Sun
1. 2022上班第一天,整理一下过去的工作,发现这方面的小知识点,去年忘记记录博客了,于是就有了这篇文章。分享给大家,希望对有需要的朋友有帮助。
2. 项目在开发调试过程中,后台的接口域名一般会分生产环境、测试环境、自定义本地环境等等多个url地址环境,供开发人员使用,而且经常会遇到频繁切换url地址的情况,就需要更改接口域名地址,然后AndroidStudio再重新编译运行App,这样就会非常麻烦!
如果我们可以通过在app中直接切换环境,不需要再重新运行打包,是不是就会方便很多呢?
3. 在开发、打包app上线前,有时会需要手动改动很多配置变量,比较麻烦,也很容易遗漏。
如果可以通过配置文件直接设置好,就会避免很多问题。
1.实现主要功能:App一键切换url环境、一键打包。
app应用内一键切换正式、测试环境,无需重新打包。
包括一键打包,无需手动改动过多配置上线变量。
2.实现方案:
主要通过配置本地文件的方式,将所有涉及的相关变量写在配置文件中,然后通过代码实现相关功能。
3.实现功能项目下载地址:
下载本文Demo请点击此处
首先在app目录下,新建configs文件夹,在configs下新建auto和release两个文件目录。
auto:此文件夹中存放 开发版本使用的配置文件。
release:此文件夹中存放 线上版本使用的配置文件。
(注:这里的文件夹名称是可以自定义的,只要开发的代码中也做相应更改就没问题。)
下图仅供参考:
每个文件代表一种url环境的变量配置,环境可以相互切换。**
设置了5个配置文件(包括config.properties、configDev.properties、configPre.properties、configCustom.properties、configProduct.properties),大家可以根据自己的需要进行设置,若不需要这么多开发环境,可以自行删除或者增加相关配置文件。
(注:以下文件中的属性配置仅供参考,大家也可以根据实际需要进行设置。)
config.properties文件:
#app 运行环境设置
#环境名
name=develop
#项目环境 url,此处只是示例,需要替换成你自己的域名地址
api.base.url=https://download.csdn.net/download/Sun_2134/73795907
#是否为线上
isProduct=false
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true
configDev.properties文件:
# dev环境
#环境名
name=develop
#项目环境 url,此处只是示例,需要替换成你自己的
api.base.url=https://blog.csdn.net/sun_promise/dev/
#是否为线上
isProduct=false
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true
configPre.properties文件:
# pre环境
#环境名
name=pre
#项目环境 url,此处只是示例,需要替换成你自己的
api.base.url=https://blog.csdn.net/sun_promise/pre/
#是否为线上
isProduct=false
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true
configCustom.properties文件:
# 自定义测试环境
#环境名
name=custom
#项目环境 url,此处只是示例,需要替换成你自己的域名地址
api.base.url=http://192.168.xx.xx:11008/
#是否为线上
isProduct=false
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true
configProduct.properties文件:
# 正式环境
#环境名
name=product
#项目环境 url,此处只是示例,需要替换成你自己的
api.base.url=https://blog.csdn.net/sun_promise
#是否为线上
isProduct=true
#是否显示Log日志
isShowLog=true
#是否显示JSON格式
isJSON=true
只有一个文件config.properties,代表线上产品版本,不能切换环境,此文件的配置是给线上真正的用户使用的。
config.properties文件:
# 线上环境
#环境名
name=product
#项目环境 url,此处只是示例,需要替换成你自己的
api.base.url=https://blog.csdn.net/sun_promise
#若以下配置不使用,可以删除不设置
#是否为线上
isProduct=true
#是否显示Log日志
isShowLog=false
#是否显示JSON格式
isJSON=false
apply plugin: 'com.android.application'
android {
compileSdk 31
defaultConfig {
applicationId "com.sun.urlenvconfig"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 多渠道打包,AS3.0之后:原因就是使用了productFlavors分包,
// 解决方法就是在build.gradle中的defaultConfig中添加 一个flavorDimensions "1"就可以了,后面的1一般是跟你的versionCode相同
// defaultConfig.versionCode
flavorDimensions "\"${defaultConfig.versionCode}\""
//记录下利用buildConfigField为项目进行动态配置(对应BuildConfig.class)
// eg: ----- debug:打印日志,在内网测试.----- release:关闭日志,外网,签名等
// 已经通过配置文件设置了,此处可以不设置了
// buildConfigField("boolean", "IS_PRODUCT", "\"${IS_PRODUCT}\"")
// buildConfigField("boolean", "IS_JSON", "true")
}
buildTypes {
debug {
minifyEnabled false
zipAlignEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// signingConfig signingConfigs.release
}
release {
minifyEnabled true
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// signingConfig signingConfigs.release
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
//开发版本使用的配置文件
auto {
assets.srcDirs = ['assets', 'configs/auto']
}
// 线上版本使用的配置文件
product {
assets.srcDirs = ['assets', 'configs/release']
}
}
//多渠道打包
productFlavors {
auto {
//可以设置app不同环境的名字 货主测试版
manifestPlaceholders = [app_name: "环境配置测试版"]
}
// 线上产品版本
product {
manifestPlaceholders = [app_name: "环境配置正式版"]
}
}
// 打包时选择,这些都是可以自己在config.properties文件中自己配置,然后组合打包的。
// autoDebug 指定默认测试环境,有 log,可切换
// autoRelease 指定默认测试环境,无 log,可切换
// productDebug 环境,无 log,不可切换
// productRelease 环境,无 log,不可切换
applicationVariants.all { variant ->
variant.outputs.all { output ->
def buildTypeName = variant.buildType.name
def versionName = defaultConfig.versionName
def versionCode = defaultConfig.versionCode
// 多渠道打包的时候,后台不支持中文
outputFileName = "envconfig-v${versionName}-${versionCode}-${buildTypeName}-${buildTime()}.apk"
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
/*butterknife*/
api 'com.jakewharton:butterknife:10.2.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
//MMKV 组件
implementation 'com.tencent:mmkv-static:1.2.7'
}
//设置默认环境:不写参数或者环境名错误,则默认develop环境
setDefaultEnv()
def setDefaultEnv() {
def envName = envConfig()
def envConfigDir = "${rootDir}/app/configs/auto/"
//def envConfigDir = "${rootDir}/app/configs/release/"
def renameFile = "config.properties"
println("打包接口环境:${envName}")
task configCopy(type: Copy) {
copy {
delete "${envConfigDir}/${renameFile}"
from(envConfigDir)
into(envConfigDir)
include(envName)
rename(envName, renameFile)
}
}
}
//这里可以更改AndroidStudio的默认运行环境: 更改envName这里对应的值即可。
String envConfig() {
def envName = "develop" //默认运行环境设置:pre、custom、product、develop
if (hasProperty("env")) {
envName = getPropmerty("env")
}
println("配置环境为:${envName}")
def envFileName = 'configDev'
if (envName == "develop") {
envFileName = 'configDev'
} else if (envName == "pre") {
envFileName = 'configPre'
} else if (envName == "custom") {
envFileName = 'configCustom'
} else if (envName == "product") {
envFileName = 'configProduct'
}
return envFileName + ".properties"
}
static def buildTime() {
def date = new Date()
def formattedDate = date.format('yyyyMMdd_HHmmss')
return formattedDate
}
更改清单文件的设置android:label="${app_name}"
MyApplication类
/**
* @Author Promise Sun
*/
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//SP框架要在PropertyUtils工具类之前进行初始化,如果你的项目中已有其他的SP工具类,可以直接使用
MMKV.initialize(this);
SpUtil.getInstance();
//Url相关
PropertyUtils.init(this);
//设置打印开关
// LogUtil.setIsLog(PropertyUtils.isShowLog());
}
}
/**
* @Author Promise Sun
*/
public class PropertyUtils {
private static Properties mProps = new Properties();
private static boolean mHasLoadProps = false;
private static final Object mLock = new Object();
private static final String TAG = "PropertyUtils";
public PropertyUtils() { }
/**
* 在AppApplication中初始化
*/
public static void init(Context context) {
if (!mHasLoadProps) {
synchronized (mLock) {
if (!mHasLoadProps) {
try {
//获取环境类型
ConfigManager.EnvironmentType environmentType = ConfigManager.getDefault().getAppEnv();
//Log.e("xyh", "init: " + environmentType.configType + ".properties");
InputStream is = context.getAssets().open(environmentType.configType + ".properties");
mProps.load(is);
mHasLoadProps = true;
Log.e(TAG, "load config.properties successfully!");
} catch (IOException var4) {
Log.e(TAG, "load config.properties error!", var4);
}
}
}
}
}
public static String getApiBaseUrl() {
if (mProps == null) {
throw new IllegalArgumentException("must call #UtilsManager.init(context) in application");
} else {
return mProps.getProperty(PropertyKey.BASE_URL, "");
}
}
public static boolean isProduct() {
return mProps.getProperty(PropertyKey.IS_PRODUCT, "false").equals("true");
}
public static boolean isJSON() {
return mProps.getProperty(PropertyKey.IS_JSON, "false").equals("true");
}
public static boolean isShowLog() {
return mProps.getProperty(PropertyKey.IS_SHOW_LOG, "false").equals("true");
}
public static String getEnvironmentName() {
return mProps.getProperty(PropertyKey.NAME_ENV, "");
}
public static ConfigManager.EnvironmentType environmentMap() {
String envName = getEnvironmentName();
switch (envName) {
case "config":
return ConfigManager.EnvironmentType.DEV;
case "pre":
return ConfigManager.EnvironmentType.PRE;
case "custom":
return ConfigManager.EnvironmentType.CUSTOM;
case "product":
return ConfigManager.EnvironmentType.PRODUCT;
default:
return ConfigManager.EnvironmentType.DEFAULT;
}
}
}
/**
* @Author Promise Sun
* 环境配置管理类
*/
public class ConfigManager {
//当前环境
private EnvironmentType mCurrentEnvType;
private static final String APP_ENV = "appEnv";
private ConfigManager() {
}
public static ConfigManager getDefault() {
return HOLDER.INSTANCE;
}
private static class HOLDER {
static ConfigManager INSTANCE = new ConfigManager();
}
/***
* 保存环境:指在切换环境时调用一次
*/
public void saveAppEnv(EnvironmentType type) {
SpUtil.setString(APP_ENV, type.configType);
}
/***
* 获取环境类型
*/
public EnvironmentType getAppEnv() {
if (mCurrentEnvType == null) {
// Log.e("sun:", "FLAVOR: " + BuildConfig.FLAVOR);
String env;
if (GlobalConstant.AUTO.equals(BuildConfig.FLAVOR)) {
env = SpUtil.getString(APP_ENV, EnvironmentType.DEFAULT.configType);
if (TextUtils.isEmpty(env)) {
env = EnvironmentType.DEFAULT.configType;
}
} else {
env = EnvironmentType.DEFAULT.configType;
}
mCurrentEnvType = EnvironmentType.map(env);
}
return mCurrentEnvType;
}
//环境类型
public enum EnvironmentType {
// 默认环境dev config:环境配置文件名
DEFAULT("config"),
// develop环境
DEV("configDev"),
// 自定义测试环境
CUSTOM("configCustom"),
// 预发布环境
PRE("configPre"),
// 线上环境
PRODUCT("configProduct");
String configType;
EnvironmentType(String configType) {
this.configType = configType;
}
public static EnvironmentType map(String configType) {
if (TextUtils.equals(EnvironmentType.DEV.configType, configType)) {
return EnvironmentType.DEV;
} else if (TextUtils.equals(EnvironmentType.PRE.configType, configType)) {
return EnvironmentType.PRE;
} else if (TextUtils.equals(EnvironmentType.CUSTOM.configType, configType)) {
return EnvironmentType.CUSTOM;
} else if (TextUtils.equals(EnvironmentType.PRODUCT.configType, configType)) {
return EnvironmentType.PRODUCT;
} else {
return EnvironmentType.DEFAULT;
}
}
}
}
@StringDef({PropertyKey.BASE_URL,PropertyKey.IS_PRODUCT
,PropertyKey.IS_SHOW_LOG,PropertyKey.IS_JSON
, PropertyKey.NAME_ENV})
@Retention(RetentionPolicy.SOURCE)
public @interface PropertyKey {
String NAME_ENV = "name";
String BASE_URL = "api.base.url";
String IS_PRODUCT = "isProduct";
String IS_JSON = "isJSON";
String IS_SHOW_LOG = "isShowLog";
}
/**
* 常量池
* @Author Promise Sun
*/
public class GlobalConstant {
public static final String AUTO="auto";
}
(注:SP工具类可以使用你自己项目中已有的,下面这段可以忽略)
public class SpUtil {
private static SpUtil mInstance;
private static MMKV mv;
private SpUtil() {
mv = MMKV.defaultMMKV();
}
/**
* 初始化MMKV,只需要初始化一次,建议在Application中初始化
*/
public static SpUtil getInstance() {
if (mInstance == null) {
synchronized (SpUtil.class) {
if (mInstance == null) {
mInstance = new SpUtil();
}
}
}
return mInstance;
}
/**
* 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法
*
* @param key
* @param object
*/
public static void setFloat(String key, Object object) {
mv.encode(key, (Float) object);
}
public static void setString(String key, Object object) {
mv.encode(key, (String) object);
}
public static void setInt(String key, Object object) {
mv.encode(key, (Integer) object);
}
public static void setDouble(String key, Object object) {
mv.encode(key, (Double) object);
}
public static void setLong(String key, Object object) {
mv.encode(key, (Long) object);
}
public static void setBoolean(String key, Object object) {
mv.encode(key, (Boolean) object);
}
public static void setStringSet(String key, Set sets) {
mv.encode(key, sets);
}
public static void setParcelable(String key, Parcelable obj) {
mv.encode(key, obj);
}
/**
* 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值
*/
public static Integer getInt(String key) {
return mv.decodeInt(key, 0);
}
public static Double getDouble(String key) {
return mv.decodeDouble(key, 0.00);
}
public static Long getLong(String key) {
return mv.decodeLong(key, 0L);
}
public static Boolean getBoolean(String key) {
return mv.decodeBool(key, false);
}
public static Float getFloat(String key) {
return mv.decodeFloat(key, 0F);
}
public static String getString(String key) {
return mv.decodeString(key, "");
}
public static String getString(String key, String defaultValue) {
return mv.decodeString(key, defaultValue);
}
public static Set getStringSet(String key) {
return mv.decodeStringSet(key, Collections.emptySet());
}
public static Parcelable getParcelable(String key) {
return mv.decodeParcelable(key, null);
}
/**
* 移除某个key对
*
* @param key
*/
public static void removeByKey(String key) {
mv.removeValueForKey(key);
}
/**
* 清除所有key
*/
public static void removeAll() {
mv.clearAll();
}
/**
* 是否包含某个key
*/
public static boolean containsKey(String key) {
return mv.containsKey(key);
}
}
(注:功能已实现,只是UI有点丑,大家可以自行开发设置)
/**
* @Author Promise Sun
*/
public class ChangeUrlEnvActivity extends AppCompatActivity {
@BindView(R.id.toolbar_left)
RelativeLayout ShowBack;
@BindView(R.id.tvTitle)
TextView tvTitle;
@BindView(R.id.tv_env)
TextView tv_env;
@BindView(R.id.tv_Env_Show)
TextView tv_Env_Show;
@BindView(R.id.group)
RadioGroup mRadioGroup;
@BindView(R.id.rb_test)
RadioButton rb_test;
@BindView(R.id.et_base_url)
EditText et_base_url;
@BindView(R.id.ll_set_env)
LinearLayout ll_set_env;
@BindView(R.id.btn_ok)
Button btn_ok;
private Unbinder unbinder;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_url_env_change);
unbinder =ButterKnife.bind(this);
initView();
}
@Override
protected void onDestroy() {
super.onDestroy();
unbinder.unbind();
}
@OnClick({R.id.toolbar_left,R.id.btn_ok})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.toolbar_left:
finish();
break;
default:
break;
}
}
protected void initView() {
ShowBack.setVisibility(View.VISIBLE);
tvTitle.setTextColor(getResources().getColor(R.color.black));
tvTitle.setText("URL 环境");
tv_env.setText("当前测试环境:"+ PropertyUtils.getEnvironmentName());
tv_Env_Show.setText( "url :" + PropertyUtils.getApiBaseUrl());
ConfigManager.EnvironmentType environmentType = PropertyUtils.environmentMap();
switch (environmentType) {
case DEV:
mRadioGroup.check(R.id.rb_dev);
break;
case CUSTOM:
mRadioGroup.check(R.id.rb_test);
break;
case PRE:
mRadioGroup.check(R.id.rb_pre);
break;
case PRODUCT:
mRadioGroup.check(R.id.rb_product);
break;
default:
mRadioGroup.check(R.id.rb_dev);
break;
}
//点击切换环境
mRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
switch (checkedId) {
case R.id.rb_dev:
if (ConfigManager.getDefault().getAppEnv() != ConfigManager.EnvironmentType.DEV) {
ConfigManager.getDefault().saveAppEnv(ConfigManager .EnvironmentType.DEV);
}
setRestart();
break;
case R.id.rb_pre:
if (ConfigManager.getDefault().getAppEnv() != ConfigManager.EnvironmentType.PRE) {
ConfigManager.getDefault().saveAppEnv(ConfigManager.EnvironmentType.PRE);
}
setRestart();
break;
case R.id.rb_product:
if (ConfigManager.getDefault().getAppEnv() != ConfigManager.EnvironmentType.PRODUCT) {
ConfigManager.getDefault().saveAppEnv(ConfigManager.EnvironmentType.PRODUCT);
}
setRestart();
break;
case R.id.rb_test:
if (ConfigManager.getDefault().getAppEnv() != ConfigManager.EnvironmentType.CUSTOM) {
ConfigManager.getDefault().saveAppEnv(ConfigManager.EnvironmentType.CUSTOM);
}
setRestart();
break;
}
});
}
private void setRestart() {
Toast.makeText(this, "1s后关闭App,重启生效", Toast.LENGTH_SHORT).show();
//退出app要进行退出登录和去除数据相关
// system.exit(0)、finish、android.os.Process.killProcess(android.os.Process.myPid())区别:
//可以杀死当前应用活动的进程,这一操作将会把所有该进程内的资源(包括线程全部清理掉)。
//当然,由于ActivityManager时刻监听着进程,一旦发现进程被非正常Kill,它将会试图去重启这个进程。这就是为什么,有时候当我们试图这样去结束掉应用时,发现它又自动重新启动的原因。
//1. System.exit(0) 表示是正常退出。
//2. System.exit(1) 表示是非正常退出,通常这种退出方式应该放在catch块中。
//3. Process.killProcess 或 System.exit(0)当前进程确实也被 kill 掉了,但 app 会重新启动。
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Process.killProcess(Process.myPid());
}
}, 1000);
}
}
下载本文Demo请点击此处
版权声明:本文为博主原创文章,转载请点赞此文并注明出处,谢谢!