一、背景:
1. 2022上班第一天,整理一下过去的工作,发现这方面的小知识点,去年忘记记录博客了,于是就有了这篇文章。分享给大家,希望对有需要的朋友有帮助。
2. 项目在开发调试过程中,后台的接口域名一般会分生产环境、测试环境、自定义本地环境等等多个url地址环境,供开发人员使用,而且经常会遇到频繁切换url地址的情况,就需要更改接口域名地址,然后AndroidStudio再重新编译运行App,这样就会非常麻烦!
如果我们可以通过在app中直接切换环境,不需要再重新运行打包,是不是就会方便很多呢?
3. 在开发、打包app上线前,有时会需要手动改动很多配置变量,比较麻烦,也很容易遗漏。
如果可以通过配置文件直接设置好,就会避免很多问题。
二、功能和方案:
1.实现主要功能:App一键切换url环境、一键打包。
app应用内一键切换正式、测试环境,无需重新打包。
包括一键打包,无需手动改动过多配置上线变量。
2.实现方案:
主要通过配置本地文件的方式,将所有涉及的相关变量写在配置文件中,然后通过代码实现相关功能。
3.实现功能项目下载地址:
三、解决方案步骤一:基础配置
1. 新建configs目录
首先在app目录下,新建configs文件夹,在configs下新建auto和release两个文件目录。
auto:此文件夹中存放 开发版本使用的配置文件。
release:此文件夹中存放 线上版本使用的配置文件。
(注:这里的文件夹名称是可以自定义的,只要开发的代码中也做相应更改就没问题。)
下图仅供参考:
2. 配置文件设置
1)auto目录:
每个文件代表一种url环境的变量配置,环境可以相互切换。**
设置了5个配置文件(包括config.properties、configDev.properties、configPre.properties、configCustom.properties、configProduct.properties),大家可以根据自己的需要进行设置,若不需要这么多开发环境,可以自行删除或者增加相关配置文件。
(注:以下文件中的属性配置仅供参考,大家也可以根据实际需要进行设置。)
config.properties文件:
#app 运行环境设置 #环境名 name=develop #项目环境 url,此处只是示例,需要替换成你自己的域名地址 api.base.url=https://www.jianshu.com/u/d346ccc6f7a4 #是否为线上 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
2)release目录:
只有一个文件config.properties,代表线上产品版本,不能切换环境,此文件的配置是给线上真正的用户使用的。
config.properties文件:
# 线上环境 #环境名 name=product #项目环境 url,此处只是示例,需要替换成你自己的 api.base.url=https://blog.csdn.net/sun_promise #若以下配置不使用,可以删除不设置 #是否为线上 isProduct=true #是否显示Log日志 isShowLog=false #是否显示JSON格式 isJSON=false
3. 配置build.gradle(app下的Module)
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 }
4. 配置渠道包的app名
更改清单文件的设置android:label="${app_name}",
<application android:name=".MyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="${app_name}" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.UrlEnvironmentConfig">
四、解决方案步骤二:开发代码完善功能
1. 涉及功能代码目录,仅供参考。
2. 初始化环境配置
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()); } }
3. 加载配置文件工具类 PropertyUtils
/** * @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; } } }
4. 设置环境配置管理类ConfigManager
/** * @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; } } } }
5. PropertyKey :配置文件相关属性设置
@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"; }
6. 常量类GlobalConstant
/** * 常量池 * @Author Promise Sun */ public class GlobalConstant { public static final String AUTO="auto"; }
7. SharedPreferences工具类
(注: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<String> 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<String> getStringSet(String key) { return mv.decodeStringSet(key, Collections.<String>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); } }
五、解决方案步骤三:页面切换环境功能实现
1. 功能实现类ChangeUrlEnvActivity
(注:功能已实现,只是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); } }
2. 布局文件 activity_url_env_change.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <include layout="@layout/include_toolbar" /> <TextView android:id="@+id/tv_env" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:text="当前测试环境:" android:textSize="18sp" android:textColor="#FFFFFF" android:textStyle="bold" /> <TextView android:id="@+id/tv_Env_Show" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="16dp" android:paddingRight="16dp" android:textSize="18sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:text="切换环境,请选择:" android:textSize="18sp" android:textStyle="bold" /> <RadioGroup android:id="@+id/group" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="25dp" android:gravity="center"> <RadioButton android:id="@+id/rb_dev" android:layout_width="170dp" android:layout_height="wrap_content" android:gravity="center_vertical" android:padding="16dp" android:text="dev 环境" android:textStyle="bold"/> <RadioButton android:id="@+id/rb_pre" android:layout_width="170dp" android:layout_height="wrap_content" android:gravity="center_vertical" android:padding="16dp" android:text="pre 环境" android:textStyle="bold"/> <RadioButton android:id="@+id/rb_product" android:layout_width="170dp" android:layout_height="wrap_content" android:gravity="center_vertical" android:padding="16dp" android:text="prod 线上环境" android:textStyle="bold"/> <RadioButton android:id="@+id/rb_test" android:layout_width="170dp" android:layout_height="wrap_content" android:gravity="center_vertical" android:padding="16dp" android:text="自定义测试环境" android:textStyle="bold"/> </RadioGroup> <LinearLayout android:id="@+id/ll_set_env" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" android:visibility="gone" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:text="手动设置测试环境:" android:textSize="18sp" android:textStyle="bold" /> <LinearLayout android:layout_width="match_parent" android:layout_height="60dp" android:background="@color/white" android:orientation="horizontal"> <TextView android:layout_width="100dp" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="15dp" android:layout_marginRight="10dp" android:gravity="center_vertical" android:text="URL :" android:textSize="16sp" /> <EditText android:id="@+id/et_base_url" android:layout_width="match_parent" android:layout_height="55dp" android:layout_gravity="center_vertical" android:layout_marginRight="10dp" android:layout_toLeftOf="@+id/iv_user_del" android:layout_toRightOf="@+id/iv_icon_username" android:background="#ffffffff" android:hint="请输入测试环境url" android:maxLines="2" android:padding="10dp" android:singleLine="true" android:textColor="#0f30b9" android:textColorHint="#999999" android:textSize="16sp" /> </LinearLayout> <Button android:id="@+id/btn_ok" android:layout_width="150dp" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="100dp" android:background="#0f30b9" android:text="o k" android:textColor="@color/white" android:textSize="18sp" /> </LinearLayout> </LinearLayout>