如何在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
相关产品推荐
相关产品推荐

