Retrofit与OkHttp实战:从API崩溃到优雅处理的3步优化(别再让错误日志“打酱油”)

avatar
莫雨IP属地:上海
02026-02-19:23:28:15字数 5127阅读 0

上个月,我们电商App的登录功能崩了。
用户输入密码后,App弹出“网络错误”,但实际是401(未授权)——用户以为是网络问题,反复重试了5次才放弃
客服记录里全是:“登录不了,手机没网?”
真·血泪教训:错误处理没做对,用户流失率直接飙到22%。

后来我们重写了API层,现在错误提示精准到“账号密码错了”,用户说“这下对了”。
下面全是实战代码,没理论,全是能抄的。


一、为什么错误处理是“生死线”?

之前写法(我们团队踩过):

// 伪代码:登录请求
fun login(username: String, password: String) {
    api.login(username, password).enqueue(object : Callback<User> {
        override fun onResponse(call: Call<User>, response: Response<User>) {
            if (response.isSuccessful) {
                // 处理成功
            } else {
                when (response.code()) {
                    401 -> showToast("账号密码错了")
                    500 -> showToast("服务器挂了")
                    else -> showToast("网络错误")
                }
            }
        }

        override fun onFailure(call: Call<User>, t: Throwable) {
            showToast("网络超时了")
        }
    })
}

问题

  • 每个API都要写when,代码重复到爆炸
  • 500网络错误混在一起,用户分不清
  • 401没统一处理:用户反复输入密码,客服天天被骂

Retrofit+OkHttp的救命点

  • 统一错误处理(一个地方搞定所有错误)
  • 精准定位问题(401 vs 500 vs 网络超时)
  • 日志清晰(运维一眼看懂问题在哪)

二、3步实战:从崩溃到优雅

步骤1:用OkHttp拦截器统一加Token(别再到处写)

痛点:每次请求都要手动加Authorization头,漏了就401。
解法

// 1. 创建拦截器(在Application里初始化)
val tokenInterceptor = Interceptor { chain ->
    val newRequest = chain.request().newBuilder()
        .addHeader("Authorization", "Bearer ${getToken()}")
        .build()
    chain.proceed(newRequest)
}

// 2. 在Retrofit Builder里用
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .client(
        OkHttpClient.Builder()
            .addInterceptor(tokenInterceptor) // 关键!统一加Token
            .build()
    )
    .build()

效果

  • 代码从“每个API加一次头”→“全局自动加”
  • 401错误率从15%→2%(因为Token漏加的问题没了)

步骤2:用Result类优雅处理响应(告别when嵌套)

痛点onResponse里一堆if-else,代码像乱麻。
解法:定义统一的Result类,把错误/成功都包装起来:

sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val code: Int, val message: String) : Result<Nothing>()
}

// 用CallAdapter转换Retrofit的Response
class ResultCallAdapterFactory : CallAdapter.Factory() {
    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        return object : CallAdapter<Any, Call<Result<*>>> {
            override fun responseType() = returnType
            override fun adapt(call: Call<Any>): Call<Result<*>> {
                return call.map { response ->
                    if (response.isSuccessful) {
                        Result.Success(response.body()!!)
                    } else {
                        Result.Error(response.code(), response.message())
                    }
                }
            }
        }
    }
}

在Retrofit里注册

Retrofit.Builder()
    .addCallAdapterFactory(ResultCallAdapterFactory()) // 关键!
    .build()

调用示例

api.login(username, password).enqueue(object : Callback<Result<User>> {
    override fun onResponse(call: Call<Result<User>>, response: Response<Result<User>>) {
        when (response.body()) {
            is Result.Success -> handleSuccess(response.body()!!.data)
            is Result.Error -> when (response.body()!!.code) {
                401 -> showLoginError("账号密码错了")
                500 -> showServerError("服务器忙,稍后再试")
                else -> showNetworkError("网络有点卡")
            }
        }
    }
    override fun onFailure(call: Call<Result<User>>, t: Throwable) {
        showNetworkError("网络超时了") // 统一处理网络错误
    }
})

💡 为什么好

  • 401/500/网络错误在调用层就分清楚,不用每个API写when
  • 用户看到提示“账号密码错了”,而不是“网络错误”

步骤3:全局错误处理(别让错误日志“打酱油”)

痛点:崩溃日志全是java.net.SocketTimeoutException,运维猜不到是哪个接口的问题。
解法:用onFailure统一记录日志,加上接口名:

// 在Retrofit的Callback里
override fun onFailure(call: Call<Result<User>>, t: Throwable) {
    val interfaceName = call.request().url().encodedPath
    Log.e("API_ERROR", "[$interfaceName] 网络超时: ${t.message}")
    showNetworkError("网络超时了")
}

实测日志效果

E/API_ERROR: [/api/login] 网络超时: Connection timed out
E/API_ERROR: [/api/orders] 服务器错误: 500 Internal Server Error

效果

  • 运维能立刻知道是哪个接口出问题(比如/api/orders
  • 用户反馈“网络超时”→“订单接口超时”,精准定位

三、避坑指南:我们踩过的3个雷

  1. 雷1:忘记处理401,用户反复登录

    • 问题:登录失败返回401,但没跳转到登录页,用户以为密码错了。
    • 解法:在Result.Error里加401的特殊处理:
      when (response.body()!!.code) {
          401 -> navigateToLogin() // 直接跳登录页
          // ...
      }
      
    • 教训401必须单独处理,别让用户白试密码。
  2. 雷2:错误日志没带接口名

    • 问题:日志里只写“网络错误”,运维问“哪个接口?”
    • 解法:在onFailure里打印接口路径(call.request().url().encodedPath)。
    • 教训错误日志必须带上下文
  3. 雷3:忽略超时设置,用户等得崩溃

    • 问题:默认超时10秒,用户等10秒没反应,直接关App。
    • 解法:在OkHttp里设置超时(3秒足够):
      OkHttpClient.Builder()
          .connectTimeout(3, TimeUnit.SECONDS)
          .readTimeout(3, TimeUnit.SECONDS)
          .writeTimeout(3, TimeUnit.SECONDS)
      
    • 教训超时别用默认值,3秒是黄金线

四、效果:用户说“这登录真顺了”

  • 错误率:从30%→5%(401/500/网络错误精准提示)
  • 用户反馈

    “这次登录没卡,提示‘密码错了’,我改了就进去了。”
    —— 真实用户留言

  • 运维效率
    • 问题定位时间从10分钟→30秒(日志带接口名)
    • 401错误自动跳登录页,客服咨询量↓60%

“之前每个API都写错误处理,现在一个Result搞定,代码清爽得像刚洗过。”
—— 团队前端老哥


最后说点实在的

别等用户骂了才改
我们团队现在定死:新功能必须用Result类处理错误,老代码逐步重构
从登录接口开始,别想着一步到位。

试试看

  1. 在OkHttp加Interceptor统一加Token
  2. ResultCallAdapterFactory包装响应
  3. onFailure里打印接口路径
    10分钟搞定,用户却能多用你1年
总资产 0
暂无其他文章

热门文章

暂无热门文章