App一键切换url环境、一键打包__Android (Java)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
.cn 域名,1个 12个月
简介: App一键切换url环境、一键打包__Android (Java)

一、背景:


1. 2022上班第一天,整理一下过去的工作,发现这方面的小知识点,去年忘记记录博客了,于是就有了这篇文章。分享给大家,希望对有需要的朋友有帮助。

2. 项目在开发调试过程中,后台的接口域名一般会分生产环境、测试环境、自定义本地环境等等多个url地址环境,供开发人员使用,而且经常会遇到频繁切换url地址的情况,就需要更改接口域名地址,然后AndroidStudio再重新编译运行App,这样就会非常麻烦!

如果我们可以通过在app中直接切换环境,不需要再重新运行打包,是不是就会方便很多呢?

3. 在开发、打包app上线前,有时会需要手动改动很多配置变量,比较麻烦,也很容易遗漏。

如果可以通过配置文件直接设置好,就会避免很多问题。


二、功能和方案:


1.实现主要功能:App一键切换url环境、一键打包。


app应用内一键切换正式、测试环境,无需重新打包。

包括一键打包,无需手动改动过多配置上线变量。

2.实现方案:


主要通过配置本地文件的方式,将所有涉及的相关变量写在配置文件中,然后通过代码实现相关功能。

3.实现功能项目下载地址:


下载本文Demo请点击此处


三、解决方案步骤一:基础配置


1. 新建configs目录

首先在app目录下,新建configs文件夹,在configs下新建auto和release两个文件目录。

auto:此文件夹中存放 开发版本使用的配置文件。

release:此文件夹中存放 线上版本使用的配置文件。

注:这里的文件夹名称是可以自定义的,只要开发的代码中也做相应更改就没问题。)

下图仅供参考:


image.png

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. 涉及功能代码目录,仅供参考。


image.png

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>




目录
相关文章
|
15天前
|
存储 Java API
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
23 4
|
18天前
|
Java Maven Android开发
【Azure Developer】VS Code打包Java maven Project 遇见 BUILD FAILURE
Unknown lifecycle phase "lean". You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>
|
1月前
|
分布式计算 大数据 Java
大数据-86 Spark 集群 WordCount 用 Scala & Java 调用Spark 编译并打包上传运行 梦开始的地方
大数据-86 Spark 集群 WordCount 用 Scala & Java 调用Spark 编译并打包上传运行 梦开始的地方
22 1
大数据-86 Spark 集群 WordCount 用 Scala & Java 调用Spark 编译并打包上传运行 梦开始的地方
|
26天前
|
Java Maven 数据安全/隐私保护
如何实现Java打包程序的加密代码混淆,避免被反编译?
【10月更文挑战第15天】如何实现Java打包程序的加密代码混淆,避免被反编译?
41 2
|
1月前
|
Java C++
做了个Java打包工具,可以双击启动了!
本文介绍了作者日常使用Java和Swing进行开发的经验,以及Java程序分发时遇到的问题,如需要JRE环境。文中列举了几种常见的Java程序打包方法,并对比了各自的优缺点,最后作者结合这些方案,利用Winform开发了一款工具,将Java程序打包成二进制可执行文件,简化了分发流程。
做了个Java打包工具,可以双击启动了!
|
1月前
|
Java Shell Maven
Flink-11 Flink Java 3分钟上手 打包Flink 提交任务至服务器执行 JobSubmit Maven打包Ja配置 maven-shade-plugin
Flink-11 Flink Java 3分钟上手 打包Flink 提交任务至服务器执行 JobSubmit Maven打包Ja配置 maven-shade-plugin
95 4
|
2月前
|
消息中间件 分布式计算 Java
Linux环境下 java程序提交spark任务到Yarn报错
Linux环境下 java程序提交spark任务到Yarn报错
41 5
|
2月前
|
Oracle Java 关系型数据库
Linux下JDK环境的配置及 bash: /usr/local/java/bin/java: cannot execute binary file: exec format error问题的解决
如果遇到"exec format error"问题,文章建议先检查Linux操作系统是32位还是64位,并确保安装了与系统匹配的JDK版本。如果系统是64位的,但出现了错误,可能是因为下载了错误的JDK版本。文章提供了一个链接,指向Oracle官网上的JDK 17 Linux版本下载页面,并附有截图说明。
Linux下JDK环境的配置及 bash: /usr/local/java/bin/java: cannot execute binary file: exec format error问题的解决
|
1月前
|
Java 大数据 开发工具
java学习——环境准备(1)
java学习——环境准备(1)
41 0
|
缓存 开发工具 Android开发
Android App性能评测分析-启动时间篇
1、前言 随着项目版本的迭代,App的性能问题会逐渐暴露出来,而好的用户体验与性能表现紧密相关,性能问题从应用的启动优化开始,下面会根据实际app性能测试案例,进行app性能评测之启动时间的分析和总结。
4263 0

热门文章

最新文章