如何在Kotlin依赖注入中使用外部变量作为Retrofit的baseUrl?
解决Hilt中Retrofit动态BaseUrl(从DataStore获取)的问题
核心问题
Hilt的单例依赖在应用启动时就会初始化,但DataStore的服务器IP是UI层异步获取的,直接传参给provideAuthApi行不通——依赖注入时机和IP获取时机不匹配。下面提供两种可行方案:
方案一:使用OkHttp拦截器动态替换BaseUrl
这是最常用的方案,通过拦截器在每个请求发起时替换为DataStore中保存的IP,无需修改原有依赖注入结构。
步骤1:注入DataStore实例
先把你的StoredServerIP类加入Hilt依赖,方便后续在拦截器中调用:
@Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideStoredServerIP(@ApplicationContext app: Application): StoredServerIP { return StoredServerIP(app) } // 其他Provider保持不变... }
步骤2:优化StoredServerIP,加入内存缓存
为避免拦截器每次请求都异步读取DataStore,给StoredServerIP加个内存缓存,提升性能:
class StoredServerIP @Inject constructor(@ApplicationContext private val context: Context) { private val Context.dataStore by preferencesDataStore(name = "server_ip_prefs") private val SERVER_IP_KEY = stringPreferencesKey("server_ip") // 内存缓存,避免重复读取DataStore private var cachedServerIp: String? = null val getServerIP: Flow<String> = context.dataStore.data .map { preferences -> preferences[SERVER_IP_KEY] ?: "" } .onEach { ip -> cachedServerIp = ip // 更新缓存 } suspend fun saveServerIP(ip: String) { context.dataStore.edit { preferences -> preferences[SERVER_IP_KEY] = ip } cachedServerIp = ip // 同步更新缓存 } // 同步获取IP,优先用缓存 fun getServerIpSync(): String { return cachedServerIp ?: runBlocking { getServerIP.first() } } }
步骤3:配置带动态BaseUrl拦截器的OkHttpClient
修改AppModule,提供带拦截器的OkHttpClient,替换请求的BaseUrl:
@Module @InstallIn(SingletonComponent::class) object AppModule { // 提供带拦截器的OkHttpClient @Provides @Singleton fun provideOkHttpClient(storedServerIP: StoredServerIP): OkHttpClient { return OkHttpClient.Builder() .addInterceptor { chain -> val originalRequest = chain.request() // 获取缓存的服务器IP,为空则用默认值 val serverIp = storedServerIP.getServerIpSync() val baseUrl = if (serverIp.isNotEmpty()) "http://$serverIp:8080/" else "http://默认IP:8080/" // 替换请求的BaseUrl val newUrl = originalRequest.url() .newBuilder() .host(serverIp) .port(8080) .build() val newRequest = originalRequest.newBuilder() .url(newUrl) .build() chain.proceed(newRequest) } .build() } // 修改provideAuthApi,依赖OkHttpClient,BaseUrl用占位符即可 @Provides @Singleton fun provideAuthApi(okHttpClient: OkHttpClient): AuthAPI { return Retrofit.Builder() .baseUrl("http://placeholder:8080/") // 占位符,拦截器会替换 .client(okHttpClient) .addConverterFactory(MoshiConverterFactory.create()) .build() .create(AuthAPI::class.java) } // 其他Provider保持不变... }
方案二:延迟初始化Retrofit实例
如果服务器IP仅在启动时设置一次,或者不需要全局单例Api,可以在ViewModel中获取IP后手动初始化Retrofit,绕过Hilt的提前初始化限制。
修改ViewModel
@HiltViewModel class MainViewModel @Inject constructor( private val prefs: SharedPreferences, private val storedServerIP: StoredServerIP, @ApplicationContext private val app: Application ) : ViewModel() { // 用Flow暴露可观察的AuthAPI实例 val authApiFlow: Flow<AuthAPI> = storedServerIP.getServerIP .filter { it.isNotEmpty() } // 过滤空IP .distinctUntilChanged() // 仅IP变化时重新创建 .map { ip -> Retrofit.Builder() .baseUrl("http://$ip:8080/") .addConverterFactory(MoshiConverterFactory.create()) .build() .create(AuthAPI::class.java) } // 或者在初始化时获取IP并创建Api private lateinit var authApi: AuthAPI init { viewModelScope.launch { val serverIp = storedServerIP.getServerIP.first() if (serverIp.isNotEmpty()) { authApi = Retrofit.Builder() .baseUrl("http://$serverIp:8080/") .addConverterFactory(MoshiConverterFactory.create()) .build() .create(AuthAPI::class.java) } } } }
在UI层使用
@Composable @Destination(start = true) fun AuthScreen( navigator: DestinationsNavigator, viewModel: MainViewModel = hiltViewModel() ) { val authApi by viewModel.authApiFlow.collectAsState(initial = null) authApi?.let { api -> // 拿到Api实例后执行网络请求 } // 其他代码... }
方案选择
- 方案一:适合IP可能动态修改、需要全局单例Api的场景,所有请求自动使用最新IP。
- 方案二:适合IP仅初始化一次、不需要全局单例的场景,实现更简单。
内容的提问来源于stack exchange,提问作者Schwoaz1802
相关产品推荐
相关产品推荐

