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>




目录
相关文章
|
2月前
|
Java
Java开发实现图片URL地址检验,如何编码?
【10月更文挑战第14天】Java开发实现图片URL地址检验,如何编码?
92 4
|
1月前
|
Java 程序员
JAVA程序员的进阶之路:掌握URL与URLConnection,轻松玩转网络资源!
在Java编程中,网络资源的获取与处理至关重要。本文介绍了如何使用URL与URLConnection高效、准确地获取网络资源。首先,通过`java.net.URL`类定位网络资源;其次,利用`URLConnection`类实现资源的读取与写入。文章还提供了最佳实践,包括异常处理、连接池、超时设置和请求头与响应头的合理配置,帮助Java程序员提升技能,应对复杂网络编程场景。
63 9
|
1月前
|
人工智能 Java 物联网
JAVA网络编程的未来:URL与URLConnection的无限可能,你准备好了吗?
随着技术的发展和互联网的普及,JAVA网络编程迎来新的机遇。本文通过案例分析,探讨URL与URLConnection在智能API调用和实时数据流处理中的关键作用,展望其未来趋势和潜力。
46 7
|
2月前
|
XML Java 数据库
安卓项目:app注册/登录界面设计
本文介绍了如何设计一个Android应用的注册/登录界面,包括布局文件的创建、登录和注册逻辑的实现,以及运行效果的展示。
195 0
安卓项目:app注册/登录界面设计
|
1月前
|
Java 开发者
JAVA高手必备:URL与URLConnection,解锁网络资源的终极秘籍!
在Java网络编程中,URL和URLConnection是两大关键技术,能够帮助开发者轻松处理网络资源。本文通过两个案例,深入解析了如何使用URL和URLConnection从网站抓取数据和发送POST请求上传数据,助力你成为真正的JAVA高手。
64 11
|
1月前
|
JSON 安全 算法
JAVA网络编程中的URL与URLConnection:那些你不知道的秘密!
在Java网络编程中,URL与URLConnection是连接网络资源的两大基石。本文通过问题解答形式,揭示了它们的深层秘密,包括特殊字符处理、请求头设置、响应体读取、支持的HTTP方法及性能优化技巧,帮助你掌握高效、安全的网络编程技能。
66 9
|
1月前
|
JSON Java API
JAVA网络编程新纪元:URL与URLConnection的神级运用,你真的会了吗?
本文深入探讨了Java网络编程中URL和URLConnection的高级应用,通过示例代码展示了如何解析URL、发送GET请求并读取响应内容。文章挑战了传统认知,帮助读者更好地理解和运用这两个基础组件,提升网络编程能力。
55 5
|
3月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
150 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
2月前
|
存储 网络协议 前端开发
在 Java 中如何完全验证 URL
在 Java 中如何完全验证 URL
94 8
|
1月前
|
Java Spring
JAVA获取重定向地址URL的两种方法
【10月更文挑战第17天】本文介绍了两种在Java中获取HTTP响应头中的Location字段的方法:一种是使用HttpURLConnection,另一种是使用Spring的RestTemplate。通过设置连接超时和禁用自动重定向,确保请求按预期执行。此外,还提供了一个自定义的`NoRedirectSimpleClientHttpRequestFactory`类,用于禁用RestTemplate的自动重定向功能。
下一篇
DataWorks