You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Dagger Hilt中动态更新BASE URL?解决URL为空问题

问题描述

我正在开发一个功能:根据用户选择的国家动态更新BASE URL,在第二个页面使用更新后的URL发起API调用并展示详情。代码分布在Activity、ViewModel和Network Module中,但Network Module中的BASE URL始终为空字符串,导致API调用失败。


获取API的Activity代码

@AndroidEntryPoint
class SchoolActivity: AppCompatActivity() {

    private lateinit var schoolBinding: ActivitySchoolBinding
    private lateinit var networkListener: NetworkListener
    private val schoolViewModel: SchoolViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        schoolBinding = ActivitySchoolBinding.inflate(layoutInflater)
        setContentView(schoolBinding.root)

        schoolViewModel.readBackOnline.observe(this) {
            schoolViewModel.backOnline = it
        }

        lifecycleScope.launch(Dispatchers.IO) {
            schoolViewModel.countryCodeFlow.collect{countryCode ->
                val baseUrl = when (countryCode) {
                    "uk" -> if (BuildConfig.BUILD_TYPE == "debug") SCH_BASE_UK_DEBUG else SCH_BASE_UK_PROD
                    "usa" -> if (BuildConfig.BUILD_TYPE == "debug") SCH_BASE_USA_DEBUG else SCH_BASE_USA_PROD
                    else -> SCH_BASE_UK_DEBUG
                }.toString()
                schoolViewModel.updateBaseURL(baseUrl)

                println("The Base URL ${schoolViewModel.getBaseURL()}")
            }
        }

        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED){
                networkListener = NetworkListener()
                networkListener.checkNetworkAvailability(this@SchoolActivity).collect{ status ->
                    Log.d("Network Listener : ", "Listener --- $status")
                    schoolViewModel.networkStatus = status
                    schoolViewModel.showNetworkStatus()
                    requestSchoolApi()
                }
            }
        }
    }

    private fun requestSchoolApi() {
        lifecycleScope.launch {
            requestApiData()
        }
    }

    private fun requestApiData() {
        Log.d("School Activity ", "Get School API call")
        schoolViewModel.getSchool()
        schoolViewModel.schoolDetailResponse.observe(this){ response ->
            println("Response $response")
        }
    }

}

ViewModel代码(使用Dagger Hilt发起API调用)

package uk.co

import android.app.Application
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import retrofit2.Response
import uk.co.parentapps.connect.data.SchoolRepository
import uk.co.parentapps.connect.data.SchoolStoreRepository
import uk.co.parentapps.connect.model.School
import uk.co.parentapps.connect.util.ApiConfig
import uk.co.parentapps.connect.util.NetworkResult
import javax.inject.Inject

@HiltViewModel
class SchoolViewModel @Inject constructor(
    private val schoolStoreRepository: SchoolStoreRepository,
    private val schoolRepository: SchoolRepository,
    private val apiConfig: ApiConfig,
    application: Application): AndroidViewModel(application) {

    var networkStatus = false
    var backOnline = false

    val readBackOnline = schoolStoreRepository.readBackOnline.asLiveData()

    val countryCodeFlow: Flow<String> = schoolStoreRepository.readCountryCode.map { it as String }

    var schoolDetailResponse: MutableLiveData<NetworkResult<School>?> = MutableLiveData()

    fun updateBaseURL(appUrl: String){
        apiConfig.setAppBaseUrl(appUrl)
    }

    fun getBaseURL(): String {
        return apiConfig.baseUrl
    }

    fun getSchool() = viewModelScope.launch {
        getSafeSchoolCall()
    }

    private suspend fun getSafeSchoolCall() {
        if (backOnline){
            try{
                val schoolResponse = schoolRepository.remoteStore.getSchools()
                schoolDetailResponse.value = handleSchoolResponse(schoolResponse)
            }catch (e: Exception){
                schoolDetailResponse.value = NetworkResult.Error("No School Found")
            }
        }else{
            schoolDetailResponse.value = NetworkResult.Error("No Internet Connection")
        }
    }

    private fun handleSchoolResponse(response: Response<School>): NetworkResult<School>? {
        when{
            response.message().toString().contains("timeout") -> {
                return NetworkResult.Error("Timeout")
            }
            response.code() == 500 -> {
                return NetworkResult.Error("Internal Server Error")
            }
            response.body()!!.schoolData.isEmpty() -> {
                return NetworkResult.Error("School Not Found")
            }
            response.isSuccessful -> {
                val schoolResult = response.body()
                return NetworkResult.Success(schoolResult!!)
            }
            else -> {
                return NetworkResult.Error(response.message())
            }
        }
    }

    private fun saveBackOnline(backOnline: Boolean) = viewModelScope.launch(Dispatchers.IO) {
        schoolStoreRepository.saveBackOnline(backOnline)
    }

    fun showNetworkStatus() {
        if (!networkStatus) {
            Toast.makeText(getApplication(), "No Internet Connection.", Toast.LENGTH_LONG).show()
            saveBackOnline(true)
        } else {
            if (backOnline) {
                Toast.makeText(getApplication(), "We're back online.", Toast.LENGTH_LONG).show()
                saveBackOnline(false)
            }
        }
    }
}

Network Module及BASE URL配置代码

ApiConfig类

class ApiConfig {
    var baseUrl: String = ""

    fun setAppBaseUrl(url: String) {
        baseUrl = url
    }
}

NetworkModule类

package uk.co

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import uk.co.parentapps.connect.data.api.SchoolApi
import uk.co.parentapps.connect.util.ApiConfig
import java.util.concurrent.TimeUnit
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {

    @Singleton
    @Provides
    fun provideHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .readTimeout(30, TimeUnit.SECONDS)
            .connectTimeout(30, TimeUnit.SECONDS)
            .build()
    }

    @Singleton
    @Provides
    fun provideConverterFactory(): GsonConverterFactory {
        return GsonConverterFactory.create()
    }

    @Singleton
    @Provides
    fun provideApiConfig(): ApiConfig {
        return ApiConfig() // Provide the ApiConfig instance
    }

    @Singleton
    @Provides
    fun providesRetrofitInstance(
        okHttpClient: OkHttpClient,
        gsonConverterFactory: GsonConverterFactory,
        apiConfig: ApiConfig
    ): Retrofit {
        return Retrofit.Builder()
            .baseUrl(apiConfig.baseUrl)
            .client(okHttpClient)
            .addConverterFactory(gsonConverterFactory)
            .build()
    }

    @Singleton
    @Provides
    fun provideApiService(retrofit: Retrofit): SchoolApi {
        return retrofit.create(SchoolApi::class.java)
    }
}

问题原因

核心问题是:Retrofit实例是Dagger Hilt提供的单例,App启动时就已初始化完成,此时ApiConfig的baseUrl为空字符串;后续更新ApiConfig的baseUrl不会改变已创建好的Retrofit实例——因为Retrofit.Builder构建出的对象是不可变的,baseUrl一旦设置就无法修改。


解决方案

方案一:动态创建Retrofit实例(推荐)

不再提供单例Retrofit,改为提供工厂类,每次发起API调用时用最新baseUrl创建新的Retrofit实例。

1. 修改NetworkModule

移除单例Retrofit和SchoolApi的提供方法,替换为Retrofit工厂:

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {

    @Singleton
    @Provides
    fun provideHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .readTimeout(30, TimeUnit.SECONDS)
            .connectTimeout(30, TimeUnit.SECONDS)
            .build()
    }

    @Singleton
    @Provides
    fun provideConverterFactory(): GsonConverterFactory {
        return GsonConverterFactory.create()
    }

    @Singleton
    @Provides
    fun provideApiConfig(): ApiConfig {
        return ApiConfig()
    }

    // 提供Retrofit工厂类,用于动态创建实例
    @Singleton
    @Provides
    fun provideRetrofitFactory(
        okHttpClient: OkHttpClient,
        gsonConverterFactory: GsonConverterFactory
    ): RetrofitFactory {
        return RetrofitFactory(okHttpClient, gsonConverterFactory)
    }
}

// 新增Retrofit工厂类
class RetrofitFactory(
    private val okHttpClient: OkHttpClient,
    private val gsonConverterFactory: GsonConverterFactory
) {
    fun createRetrofit(baseUrl: String): Retrofit {
        require(baseUrl.isNotEmpty()) { "Base URL cannot be empty" }
        return Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(okHttpClient)
            .addConverterFactory(gsonConverterFactory)
            .build()
    }
}

2. 修改Repository获取SchoolApi的方式

每次调用API时,用最新baseUrl创建Retrofit和SchoolApi:

class SchoolRepository @Inject constructor(
    private val retrofitFactory: RetrofitFactory,
    private val apiConfig: ApiConfig
) {
    // 每次调用时获取最新的SchoolApi
    val remoteStore: SchoolApi
        get() {
            return retrofitFactory.createRetrofit(apiConfig.baseUrl).create(SchoolApi::class.java)
        }
}

方案二:使用OkHttp拦截器动态替换Base URL

通过拦截器在请求发出时替换URL,无需重新创建Retrofit实例。

1. 修改ApiConfig

统一URL格式,避免拼接错误:

class ApiConfig {
    var baseUrl: String = ""
        private set

    fun setAppBaseUrl(url: String) {
        baseUrl = url.trimEnd('/') // 移除末尾斜杠,统一格式
    }
}

2. 添加BaseUrlInterceptor

class BaseUrlInterceptor @Inject constructor(
    private val apiConfig: ApiConfig
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val currentBaseUrl = apiConfig.baseUrl

        if (currentBaseUrl.isEmpty()) {
            return chain.proceed(originalRequest)
        }

        // 解析并替换原请求的URL
        val originalHttpUrl = originalRequest.url
        val newHttpUrl = originalHttpUrl.newBuilder()
            .scheme(currentBaseUrl.substringBefore("://"))
            .host(currentBaseUrl.substringAfter("://").substringBefore('/'))
            .encodedPath(currentBaseUrl.substringAfter('/') + originalHttpUrl.encodedPath())
            .build()

        val newRequest = originalRequest.newBuilder()
            .url(newHttpUrl)
            .build()

        return chain.proceed(newRequest)
    }
}

3. 修改NetworkModule添加拦截器

@Singleton
@Provides
fun provideHttpClient(baseUrlInterceptor: BaseUrlInterceptor): OkHttpClient {
    return OkHttpClient.Builder()
        .addInterceptor(baseUrlInterceptor) // 添加动态替换URL的拦截器
        .readTimeout(30, TimeUnit.SECONDS)
        .connectTimeout(30, TimeUnit.SECONDS)
        .build()
}

// 提供拦截器实例
@Singleton
@Provides
fun provideBaseUrlInterceptor(apiConfig: ApiConfig): BaseUrlInterceptor {
    return BaseUrlInterceptor(apiConfig)
}

// Retrofit使用默认占位URL,拦截器会替换为实际URL
@Singleton
@Provides
fun providesRetrofitInstance(
    okHttpClient: OkHttpClient,
    gsonConverterFactory: GsonConverterFactory
): Retrofit {
    return Retrofit.Builder()
        .baseUrl("https://default.example.com/") // 合法占位URL
        .client(okHttpClient)
        .addConverterFactory(gsonConverterFactory)
        .build()
}

额外注意事项

  • 确保发起API调用前,baseUrl已完成更新(比如在requestSchoolApi()前,确认countryCodeFlow已收集到值并执行了updateBaseURL)。
  • 在updateBaseURL中添加URL格式校验,确保包含http/https协议头。

内容的提问来源于stack exchange,提问作者Ankit Tale

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 21:20:54