1. 为什么需要封装网络请求工具链
在Android开发中,网络请求是每个应用必不可少的基础功能。Retrofit+OkHttp+RxJava这套组合拳虽然各自都很强大,但直接裸用会面临几个典型问题:
- 重复代码泛滥:每个接口都要写CallAdapter、Converter、拦截器配置
- 错误处理分散:HTTP状态码、网络异常、业务错误混在一起
- 生命周期隐患:RxJava的订阅可能引发内存泄漏
- 监控困难:缺乏统一的日志、耗时统计机制
我在多个百万级DAU的电商APP中实践发现,良好的封装能让网络层代码量减少40%,同时显著降低Crash率。下面就以一个电商APP的商品详情页为例,展示如何构建健壮的请求工具链。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础组件选型与配置
2.1 三剑客版本匹配
gradle复制// build.gradle
def retrofit_version = "2.9.0"
def okhttp_version = "4.10.0"
def rxjava_version = "3.1.5"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofit_version"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation "io.reactivex.rxjava3:rxjava:$rxjava_version"
注意:Retrofit 2.6.0+开始内置了对RxJava3的支持,避免使用旧版适配器
2.2 OkHttpClient精细化配置
kotlin复制val okHttpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS) // 连接超时
.readTimeout(20, TimeUnit.SECONDS) // 读取超时
.writeTimeout(20, TimeUnit.SECONDS) // 写入超时
.retryOnConnectionFailure(true) // 自动重试
.addInterceptor(HeaderInterceptor()) // 公共请求头
.addInterceptor(LoggingInterceptor()) // 日志打印
.addNetworkInterceptor(MonitorInterceptor()) // 网络监控
.build()
关键点说明:
- 超时时间需要根据业务场景调整(如支付接口需要更长超时)
- 重试机制要配合业务逻辑(非幂等操作不能自动重试)
- 拦截器顺序影响处理流程(addInterceptor vs addNetworkInterceptor)
3. Retrofit服务封装实战
3.1 基础API服务定义
kotlin复制interface ApiService {
@GET("product/detail")
fun getProductDetail(
@Query("productId") id: String,
@Query("region") region: String = "CN"
): Single<BaseResponse<ProductDetail>>
@POST("cart/add")
@FormUrlEncoded
fun addToCart(
@Field("skuId") skuId: String,
@Field("count") count: Int
): Completable
}
3.2 动态URL与多域名处理
kotlin复制@GET
fun searchProducts(
@Url dynamicUrl: String,
@Query("keyword") keyword: String
): Single<BaseResponse<List<Product>>>
// 使用示例
val api = retrofit.create(ApiService::class.java)
api.searchProducts("https://search.api.com/v2/search", "手机")
技巧:通过@Url注解可以实现灰度测试、A/B测试等场景的域名动态切换
4. RxJava的响应式封装
4.1 线程调度统一管理
kotlin复制fun <T> Single<T>.applySchedulers(): Single<T> {
return subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnDispose { Log.d("Network", "Request disposed") }
}
4.2 生命周期绑定
kotlin复制class RxLifecycleHandler(private val lifecycle: Lifecycle) {
private val compositeDisposable = CompositeDisposable()
fun <T> bind(single: Single<T>): Single<T> {
return single.doOnSubscribe { disposable ->
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
compositeDisposable.clear()
}
})
compositeDisposable.add(disposable)
}
}
}
// 在Activity中使用
rxLifecycleHandler.bind(api.getProductDetail("123"))
.applySchedulers()
.subscribe({...}, {...})
5. 异常处理标准化
5.1 统一错误码解析
kotlin复制sealed class NetworkError : Exception() {
data class HttpError(val code: Int, val msg: String) : NetworkError()
data class BusinessError(val errorCode: String) : NetworkError()
object NetworkUnavailable : NetworkError()
object Timeout : NetworkError()
}
fun <T> Single<T>.wrapNetworkError(): Single<T> {
return this.onErrorResumeNext { throwable ->
when (throwable) {
is SocketTimeoutException -> Single.error(NetworkError.Timeout)
is UnknownHostException -> Single.error(NetworkError.NetworkUnavailable)
is HttpException -> {
val errorBody = throwable.response()?.errorBody()?.string()
Single.error(parseHttpError(errorBody))
}
else -> Single.error(throwable)
}
}
}
5.2 全局错误拦截器
kotlin复制class ErrorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (!response.isSuccessful) {
when (response.code) {
401 -> throw UnauthorizedException()
403 -> throw ForbiddenException()
500 -> throw ServerErrorException()
}
}
return response
}
}
6. 高级功能实现
6.1 请求重试策略
kotlin复制fun <T> Single<T>.retryWithDelay(
maxRetries: Int = 3,
delayMillis: Long = 1000
): Single<T> {
return retryWhen { errors ->
errors.zipWith(Observable.range(1, maxRetries)) { _, i ->
if (i < maxRetries) Observable.timer(delayMillis * i, TimeUnit.MILLISECONDS)
else Observable.error(it)
}.flatMap { it }
}
}
6.2 请求缓存控制
kotlin复制val cache = Cache(File(context.cacheDir, "http_cache"), 10 * 1024 * 1024)
val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.addInterceptor { chain ->
val request = chain.request()
val networkAvailable = isNetworkAvailable(context)
val newRequest = request.newBuilder()
.cacheControl(
if (networkAvailable) CacheControl.FORCE_NETWORK
else CacheControl.FORCE_CACHE
)
.build()
chain.proceed(newRequest)
}
.build()
7. 性能监控与优化
7.1 网络请求监控
kotlin复制class MonitorInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val startNs = System.nanoTime()
val response = try {
chain.proceed(request)
} catch (e: Exception) {
logFailedRequest(request, e)
throw e
}
val tookMs = (System.nanoTime() - startNs) / 1_000_000
logRequestMetrics(request, response, tookMs)
return response
}
private fun logRequestMetrics(
request: Request,
response: Response,
tookMs: Long
) {
val metrics = mapOf(
"url" to request.url.toString(),
"method" to request.method,
"status" to response.code,
"size" to response.body?.contentLength() ?: 0,
"time" to tookMs
)
FirebaseAnalytics.logEvent("network_request", metrics)
}
}
7.2 请求合并优化
对于商品详情页的多个并行请求:
kotlin复制fun getProductPageData(productId: String): Single<ProductPageData> {
return Single.zip(
api.getProductDetail(productId),
api.getProductComments(productId),
api.getRecommendations(productId),
Function3 { detail, comments, recommends ->
ProductPageData(detail, comments, recommends)
}
)
}
8. 测试策略
8.1 MockServer测试
kotlin复制@RunWith(AndroidJUnit4::class)
class ApiServiceTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var apiService: ApiService
@Before
fun setup() {
mockWebServer = MockWebServer()
mockWebServer.start()
val retrofit = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
apiService = retrofit.create(ApiService::class.java)
}
@Test
fun testProductDetail() {
mockWebServer.enqueue(
MockResponse()
.setBody(MockResponseFileReader("product_detail.json").content)
.setResponseCode(200)
)
apiService.getProductDetail("123")
.test()
.assertValue { it.code == 200 }
.assertValue { it.data != null }
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
}
8.2 流量统计测试
使用OkHttp的EventListener监控实际网络表现:
kotlin复制val client = OkHttpClient.Builder()
.eventListener(object : EventListener() {
override fun callStart(call: Call) {
super.callStart(call)
TrafficStats.startTracking(call.request().url.toString())
}
override fun callEnd(call: Call) {
super.callEnd(call)
TrafficStats.endTracking(call.request().url.toString())
}
})
.build()
9. 封装后的使用示例
9.1 标准请求流程
kotlin复制class ProductRepository(
private val api: ApiService,
private val lifecycleHandler: RxLifecycleHandler
) {
fun loadProductDetail(productId: String): Single<ProductDetail> {
return api.getProductDetail(productId)
.wrapNetworkError()
.map { it.data ?: throw IllegalStateException("Empty data") }
.retryWithDelay()
.applySchedulers()
.let { lifecycleHandler.bind(it) }
}
}
// ViewModel中使用
productRepository.loadProductDetail("123")
.subscribe(
{ detail -> updateUI(detail) },
{ error -> handleError(error) }
)
9.2 组合请求示例
kotlin复制fun loadFullProductData(productId: String): Single<FullProductData> {
return Single.zip(
loadProductDetail(productId),
loadProductComments(productId),
loadProductInventory(productId),
::FullProductData
).compose(applyCommonOperators())
}
private fun <T> applyCommonOperators(): SingleTransformer<T, T> {
return SingleTransformer { upstream ->
upstream.wrapNetworkError()
.retryWithDelay()
.applySchedulers()
.let { lifecycleHandler.bind(it) }
}
}
10. 封装中的常见陷阱
-
拦截器顺序错误:
- 日志拦截器应该最后添加
- 认证拦截器需要放在最前面
-
RxJava内存泄漏:
- 忘记绑定生命周期
- 在Fragment中使用时没有考虑视图状态
-
缓存策略冲突:
- 服务端Cache-Control与客户端配置冲突
- 动态接口误用缓存
-
线程切换混乱:
- 在IO线程操作UI
- 在主线程执行网络请求
-
错误处理遗漏:
- 未处理JSON解析异常
- 忽略HTTP 3xx重定向
在电商APP的实践中,我们发现商品搜索接口的缓存配置不当曾导致日均300万次无效请求。通过以下改进方案节省了28%的带宽成本:
kotlin复制@Headers("Cache-Control: public, max-age=60")
@GET("search")
fun searchProducts(@Query("q") query: String): Single<SearchResult>
