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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 21:25:01