第七篇:Ktor 统一响应模型:ApiResponse、业务 code 与 data 解包

前面我们已经把请求链路逐渐搭起来了:

复制代码
ApiService
    ↓
NetworkClient
    ↓
HttpClient
    ↓
Plugin
    ↓
Engine

现在已经可以写:

复制代码
val user: User =
    networkClient.get(
        path = "users/1001",
    )

看起来很干净。

但正式项目里,后端通常不会直接返回:

复制代码
{
  "id": 1001,
  "name": "Tom"
}

更常见的是统一包一层:

复制代码
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 1001,
    "name": "Tom"
  }
}

于是就出现了一个非常重要的问题:

Ktor 收到的是 HTTP Response,而业务真正关心的是 data。这两层到底应该怎么处理?

这就是这一篇要解决的问题。


一、先区分两个 Response

很多人第一次写网络层时,很容易把:

复制代码
HttpResponse

和:

复制代码
ApiResponse<T>

混在一起。

但它们完全不是一个层级。


HttpResponse

这是 HTTP 协议层的响应。

例如服务器返回:

复制代码
HTTP/1.1 200 OK
Content-Type: application/json

以及:

复制代码
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 1001,
    "name": "Tom"
  }
}

Ktor 拿到:

复制代码
val response: HttpResponse =
    client.get("users/1001")

这里:

复制代码
HttpResponse

包含的是:

复制代码
HTTP Status
Header
Body
Content-Type

比如:

复制代码
status = 200
body = JSON

ApiResponse

这是我们自己根据后端协议定义的数据模型。

例如:

复制代码
@Serializable
data class ApiResponse<T>(
    val code: Int,
    val msg: String,
    val data: T,
)

它对应的是 HTTP Body:

复制代码
{
  "code": 0,
  "msg": "success",
  "data": {}
}

所以关系是:

复制代码
HTTP Response
    │
    ├── status
    ├── headers
    └── body
          ↓
     ApiResponse<T>

也就是说:

HttpResponse 是 HTTP 层对象,ApiResponse<T> 是业务协议层对象。


二、一次完整响应其实有两层状态

假设接口:

复制代码
GET /users/1001

服务器返回:

复制代码
HTTP/1.1 200 OK

Body:

复制代码
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 1001,
    "name": "Tom"
  }
}

这里其实存在两个成功条件。

第一层:

复制代码
HTTP 200

表示:

HTTP 请求本身成功完成。

第二层:

复制代码
code = 0

表示:

业务执行成功。

所以:

复制代码
HTTP 成功
≠
业务一定成功

这点非常重要。


三、HTTP 200 也可能是业务失败

例如服务器:

复制代码
HTTP/1.1 200 OK

但是 Body:

复制代码
{
  "code": 10001,
  "msg": "用户不存在",
  "data": null
}

从 HTTP 看:

复制代码
200 OK

请求是成功返回了。

但业务上:

复制代码
code = 10001

明显失败。

所以如果你只判断:

复制代码
response.status == HttpStatusCode.OK

是不够的。

正式网络层通常需要处理:

复制代码
第一层:HTTP 状态
        ↓
第二层:业务 code

四、定义统一 ApiResponse

例如:

复制代码
@Serializable
data class ApiResponse<T>(
    val code: Int,
    val msg: String = "",
    val data: T,
)

如果后端:

复制代码
{
  "code": 0,
  "msg": "",
  "data": {
    "id": 1001,
    "name": "Tom"
  }
}

那么:

复制代码
val response =
    client
        .get("users/1001")
        .body<ApiResponse<User>>()

得到:

复制代码
ApiResponse<User>

其中:

复制代码
code = 0
msg = ""
data = User(...)

五、泛型 T 的价值就在这里

同一个结构:

复制代码
ApiResponse<T>

可以表示很多接口。

用户:

复制代码
ApiResponse<User>

订单:

复制代码
ApiResponse<Order>

列表:

复制代码
ApiResponse<List<User>>

分页:

复制代码
ApiResponse<Page<User>>

所以:

复制代码
ApiResponse<T>

真正解决的是:

后端统一外层结构固定,但 data 类型变化的问题。

结构:

复制代码
ApiResponse
│
├── code
├── msg
└── data
     ↓
     T

六、最直接的写法是什么?

假设现在 UserApi:

复制代码
suspend fun getUser(
    id: Long,
): User {

    val response =
        client
            .get("users/$id")
            .body<ApiResponse<User>>()

    if (response.code != 0) {
        throw RuntimeException(
            response.msg
        )
    }

    return response.data
}

功能上完全没问题。

过程:

复制代码
GET
↓
HttpResponse
↓
ApiResponse<User>
↓
检查 code
↓
返回 data
↓
User

但是很快就会发现问题。


七、如果每个 ApiService 都判断 code,会发生什么?

UserApi:

复制代码
if (response.code != 0) {
    throw ...
}

OrderApi:

复制代码
if (response.code != 0) {
    throw ...
}

RepairApi:

复制代码
if (response.code != 0) {
    throw ...
}

所有接口都开始重复:

复制代码
body<ApiResponse<T>>()
↓
if (code != 0)
↓
throw
↓
return data

这和前面每个接口都写:

复制代码
BaseUrl
Header

其实是同一种问题:

公共逻辑放错层了。

业务 ApiService 不应该反复处理统一响应协议。


八、所以应该放到 NetworkClient

前面我们已经建立:

复制代码
ApiService
↓
NetworkClient
↓
HttpClient

那么统一响应处理正好适合放到:

复制代码
NetworkClient

比如:

复制代码
suspend inline fun <reified T> get(
    path: String,
): T {

    val response =
        client
            .get(path)
            .body<ApiResponse<T>>()

    if (response.code != 0) {
        throw ApiException(
            code = response.code,
            message = response.msg,
        )
    }

    return response.data
}

于是业务:

复制代码
suspend fun getUser(
    id: Long,
): User {

    return networkClient.get(
        path = "users/$id",
    )
}

UserApi 已经完全不需要知道:

复制代码
ApiResponse<User>
code
msg
data

九、这一步叫"data 解包"

后端:

复制代码
{
  "code": 0,
  "msg": "",
  "data": {
    "id": 1001,
    "name": "Tom"
  }
}

NetworkClient:

复制代码
ApiResponse<User>
       ↓
检查 code
       ↓
取 data
       ↓
User

所以业务层:

复制代码
val user: User

拿到的不是:

复制代码
ApiResponse<User>

而是:

复制代码
User

这就是:

统一响应解包。


十、为什么业务层最好不要长期持有 ApiResponse?

假设 Repository:

复制代码
suspend fun getUser(): ApiResponse<User>

然后 ViewModel:

复制代码
val response =
    repository.getUser()

if (response.code == 0) {
    val user = response.data
}

这意味着:

复制代码
ViewModel
↓
知道后端 code 协议

甚至 UI 可能也开始知道:

复制代码
code == 0
code == 10001

这样后端协议就逐渐向上泄漏

更理想的是:

复制代码
后端协议
↓
NetworkClient 消化掉
↓
Repository 只得到 User / AppError

所以:

复制代码
ApiResponse<T>

最好主要存在于:

复制代码
网络基础设施层

而不是扩散到整个 App。


十一、理想结构是什么?

可以理解成:

复制代码
Server
↓
HTTP Response
↓
ApiResponse<T>
↓
NetworkClient
↓
T
↓
Repository
↓
ViewModel

业务层看到:

复制代码
成功
↓
T

失败
↓
AppError

而不是:

复制代码
成功?
↓
自己检查 code

失败?
↓
自己解析 msg

十二、但是 data 一定有值吗?

这里开始进入真实项目问题。

假设:

复制代码
{
  "code": 0,
  "msg": "success",
  "data": null
}

有些接口确实可能:

复制代码
没有 data

例如:

复制代码
删除成功
提交成功
退出登录成功

后端可能只返回:

复制代码
{
  "code": 0,
  "msg": "success",
  "data": null
}

那么:

复制代码
data class ApiResponse<T>(
    val code: Int,
    val msg: String,
    val data: T,
)

就可能不够灵活。


十三、可以设计成 T?

例如:

复制代码
@Serializable
data class ApiResponse<T>(
    val code: Int,
    val msg: String = "",
    val data: T? = null,
)

这样:

复制代码
data
↓
允许 null

但是这又带来一个问题:

复制代码
val data: T?

那么普通成功接口:

复制代码
getUser(): User

NetworkClient 要怎么从:

复制代码
T?

安全变成:

复制代码
T

所以这里要根据后端协议设计。


十四、一种做法:成功必须有 data

如果项目后端规范非常明确:

只要 code == 0 且接口声明返回数据,data 一定存在。

那么可以:

Kotlin 复制代码
val data = response.data
        ?: throw ApiException(
            code = response.code,
            message = "Response data is null",
        )

然后:

复制代码
return data

这就把:

复制代码
后端返回成功但 data 异常为空

也归到网络协议异常里。


十五、无 data 接口可以单独用 Unit

比如删除:

复制代码
suspend fun deleteUser(
    id: Long,
) {
    networkClient.delete<Unit>(
        path = "users/$id",
    )
}

后端:

复制代码
{
  "code": 0,
  "msg": "success",
  "data": null
}

这类接口可以根据项目协议单独处理。

也可以定义:

复制代码
@Serializable
object EmptyData

然后:

复制代码
ApiResponse<EmptyData>

具体采用哪种方式,取决于后端协议。

关键不是死记一个固定答案。

而是:

统一响应模型必须和真实后端协议匹配。


十六、code == 0 为什么应该统一定义?

不要在网络层到处:

复制代码
if (response.code == 0)

可以统一:

复制代码
private const val SUCCESS_CODE = 0

或者:

复制代码
object ApiCode {
    const val SUCCESS = 0
}

然后:

复制代码
if (response.code != ApiCode.SUCCESS) {
    ...
}

这样以后后端如果改:

复制代码
成功码
0 → 200

至少修改点更集中。


十七、业务失败应该直接 RuntimeException 吗?

前面为了演示,我们写:

复制代码
throw RuntimeException(
    response.msg
)

正式项目显然不够。

因为:

复制代码
code != 0

是一个明确的:

复制代码
业务 API 错误

所以可以定义:

复制代码
class ApiException(
    val code: Int,
    override val message: String,
) : Exception(message)

然后:

复制代码
if (response.code != ApiCode.SUCCESS) {

    throw ApiException(
        code = response.code,
        message = response.msg,
    )
}

这样上层至少知道:

复制代码
这是业务协议错误

而不是普通未知异常。

后面异常篇还会进一步把它转换成:

复制代码
AppError

十八、HTTP 失败和业务失败完全不同

例如:

复制代码
HTTP 500

这属于:

复制代码
HTTP Error

而:

复制代码
HTTP 200

Body:

复制代码
{
  "code": 10001,
  "msg": "用户不存在",
  "data": null
}

属于:

复制代码
Business Error

所以完整链路:

复制代码
Request
↓
HTTP
↓
HTTP Status 是否成功?
    ↓
   是
    ↓
解析 ApiResponse<T>
    ↓
code 是否成功?
    ↓
   是
    ↓
返回 data

失败分支:

复制代码
HTTP Status 失败
↓
HTTP Exception

code != 0
↓
ApiException

这就是为什么后面一定要建立完整异常体系。


十九、expectSuccess = true 在这里负责哪一层?

前面我们已经接触过:

复制代码
HttpClient {
    expectSuccess = true
}

它主要处理的是:

复制代码
HTTP Status

例如:

复制代码
401
404
500

也就是:

复制代码
HTTP 层错误

它并不知道你后端 Body 里面:

复制代码
{
  "code": 10001
}

是什么意思。

所以:

复制代码
expectSuccess
↓
处理 HTTP Status


ApiResponse.code
↓
处理业务协议状态

这两个一定不要混。


二十、完整成功链路

例如:

复制代码
HTTP 200

Body:

复制代码
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 1001,
    "name": "Tom"
  }
}

流程:

复制代码
HttpClient
↓
HTTP 200
↓
expectSuccess 通过
↓
ContentNegotiation
↓
ApiResponse<User>
↓
code == 0
↓
data
↓
User

最终:

复制代码
val user: User

二十一、HTTP 失败链路

例如:

复制代码
HTTP 500

流程:

复制代码
HttpClient
↓
500
↓
expectSuccess = true
↓
抛 HTTP 相关异常
↓
异常体系处理

此时甚至可能还没有进入正常:

复制代码
ApiResponse<T> data 解包

流程。


二十二、业务失败链路

例如:

复制代码
HTTP 200

Body:

复制代码
{
  "code": 10001,
  "msg": "用户不存在",
  "data": null
}

流程:

复制代码
HTTP 200
↓
expectSuccess 通过
↓
解析 ApiResponse<User>
↓
code != 0
↓
ApiException

这就是:

复制代码
HTTP 成功
业务失败

非常典型的场景。


二十三、JSON 解析失败又属于第三层

假设:

复制代码
HTTP 200

Body 本来应该:

复制代码
{
  "code": 0,
  "msg": "",
  "data": {}
}

结果后端返回了错误格式:

复制代码
<html>502 Bad Gateway</html>

或者字段类型不匹配:

复制代码
{
  "code": "abc"
}

那么:

复制代码
HTTP
↓
200
↓
但是 JSON 解析失败

这又不是:

复制代码
HTTP Error

也不是:

复制代码
Business Error

而是:

复制代码
Serialization / Parse Error

所以完整网络错误已经逐渐出现:

复制代码
网络连接错误
HTTP 错误
JSON 解析错误
业务 code 错误
未知错误

这就是下一阶段异常体系要处理的内容。


二十四、NetworkClient 可以先写成什么样?

一个基础版本:

Kotlin 复制代码
class NetworkClient(
    private val client: HttpClient,
) {

    suspend inline fun <reified T> get(
        path: String,
        noinline block: HttpRequestBuilder.() -> Unit = {},
    ): T {

        val response =
            client
                .get(path) {
                    block()
                }
                .body<ApiResponse<T>>()

        if (response.code != ApiCode.SUCCESS) {

            throw ApiException(
                code = response.code,
                message = response.msg,
            )
        }

        return response.data
    }
}

现在 get() 已经不再只是:

复制代码
HttpClient.get()
↓
body<T>()

而变成:

复制代码
GET
↓
ApiResponse<T>
↓
检查业务 code
↓
data
↓
T

二十五、POST 也是一样

Kotlin 复制代码
suspend inline fun <
    reified T,
    reified B,
> post(
    path: String,
    body: B,
    noinline block: HttpRequestBuilder.() -> Unit = {},
): T {

    val response =
        client
            .post(path) {

                contentType(
                    ContentType.Application.Json
                )

                setBody(body)

                block()
            }
            .body<ApiResponse<T>>()

    if (response.code != ApiCode.SUCCESS) {

        throw ApiException(
            code = response.code,
            message = response.msg,
        )
    }

    return response.data
}

可以看到:

复制代码
get
post
put
delete

马上又会重复:

复制代码
body<ApiResponse<T>>()
↓
检查 code
↓
return data

这说明还可以继续抽。


二十六、可以抽一个统一解析函数

例如:

Kotlin 复制代码
private fun <T> unwrap(
    response: ApiResponse<T>,
): T {

    if (response.code != ApiCode.SUCCESS) {

        throw ApiException(
            code = response.code,
            message = response.msg,
        )
    }

    return response.data
}

然后 GET:

Kotlin 复制代码
suspend inline fun <reified T> get(
    path: String,
): T {

    val response =
        client
            .get(path)
            .body<ApiResponse<T>>()

    return unwrap(response)
}

POST:

Kotlin 复制代码
suspend inline fun <
    reified T,
    reified B,
> post(
    path: String,
    body: B,
): T {

    val response =
        client
            .post(path) {
                setBody(body)
            }
            .body<ApiResponse<T>>()

    return unwrap(response)
}

于是:

复制代码
HTTP Method
↓
负责发送

unwrap()
↓
负责业务响应解包

职责更清楚。


二十七、还可以进一步抽成 request()

后面你甚至会发现:

复制代码
GET
POST
PUT
DELETE

区别主要在:

复制代码
怎么构建 HttpRequest

而后面的:

复制代码
ApiResponse<T>
↓
code
↓
data

完全一样。

所以可以进一步做成:

Kotlin 复制代码
suspend inline fun <reified T> request(
    crossinline request: suspend () -> HttpResponse,
): T {

    val response =
        request()
            .body<ApiResponse<T>>()

    if (response.code != ApiCode.SUCCESS) {
        throw ApiException(
            code = response.code,
            message = response.msg,
        )
    }

    return response.data
}

GET:

Kotlin 复制代码
suspend inline fun <reified T> get(
    path: String,
): T {

    return request {
        client.get(path)
    }
}

POST:

Kotlin 复制代码
suspend inline fun <
    reified T,
    reified B,
> post(
    path: String,
    body: B,
): T {

    return request {
        client.post(path) {
            setBody(body)
        }
    }
}

这样统一处理链路更明显:

复制代码
GET / POST / PUT / DELETE
         ↓
      request<T>()
         ↓
   ApiResponse<T>
         ↓
       code
         ↓
       data

二十八、但不要为了抽象而抽象

如果项目很简单:

复制代码
get()
post()
put()
delete()

各自只有十几行。

完全没必要一开始就做:

复制代码
request()
requestInternal()
execute()
unwrap()
map()
parse()

层层套娃。

所以还是前面的原则:

先出现重复,再抽公共逻辑。

不要为了"看起来像框架"而提前设计复杂层次。


二十九、ApiService 现在会变成什么样?

例如:

Kotlin 复制代码
class UserApi(
    private val client: NetworkClient,
) {

    suspend fun getUser(
        id: Long,
    ): User {

        return client.get(
            path = "users/$id",
        )
    }
}

业务层完全不知道:

复制代码
HTTP 200
ApiResponse<User>
code == 0
data

它只知道:

复制代码
调用成功
↓
User

失败:

复制代码
抛出统一错误

这就是我们想要的边界。


三十、Repository 会更加干净

例如:

Kotlin 复制代码
class UserRepository(
    private val userApi: UserApi,
) {

    suspend fun getUser(
        id: Long,
    ): User {

        return userApi.getUser(id)
    }
}

Repository 也不需要:

复制代码
if (response.code == 0)

因为:

复制代码
业务协议处理

已经被网络层消化掉。


三十一、业务 code 应该全部在 NetworkClient 处理吗?

这里要区分:

复制代码
通用业务协议错误

和:

复制代码
具体业务分支

例如:

复制代码
code = 10001
用户未登录

可以统一映射:

复制代码
Unauthorized

但是:

复制代码
code = 23001
优惠券已被领取

这种可能是订单业务自己要做特殊 UI。

所以网络层适合负责:

复制代码
把 code 转成结构化错误

而不是:

复制代码
决定所有业务 UI 怎么处理

比如:

复制代码
ApiException(
    code = 23001,
    message = "优惠券已领取",
)

然后更上层根据业务决定:

复制代码
Toast
Dialog
跳转
刷新页面

网络层只负责提供正确的信息。


三十二、不要让 NetworkClient 直接弹 Toast

这是很重要的边界。

错误做法:

Kotlin 复制代码
if (response.code != 0) {

    Toast.makeText(
        context,
        response.msg,
        Toast.LENGTH_SHORT,
    ).show()
}

网络层不应该知道:

复制代码
Android Context
Toast
UI

尤其 KMP 中:

复制代码
commonMain

更不能这么做。

正确思路:

复制代码
NetworkClient
↓
ApiException / AppError

ViewModel
↓
决定 UI 行为

这也是为什么错误模型需要结构化。


三十三、统一响应也要考虑 List / Page

例如:

复制代码
{
  "code": 0,
  "msg": "",
  "data": [
    {
      "id": 1
    },
    {
      "id": 2
    }
  ]
}

直接:

复制代码
ApiResponse<List<User>>

分页:

复制代码
{
  "code": 0,
  "msg": "",
  "data": {
    "page": 1,
    "pageSize": 20,
    "total": 100,
    "items": []
  }
}

可以:

复制代码
@Serializable
data class Page<T>(
    val page: Int,
    val pageSize: Int,
    val total: Long,
    val items: List<T>,
)

然后:

复制代码
ApiResponse<Page<User>>

所以泛型可以继续嵌套:

复制代码
ApiResponse
    ↓
Page
    ↓
List
    ↓
User

三十四、这也是 kotlinx.serialization 泛型能力的实际价值

前面单独讲 kotlinx.serialization 时,泛型看起来比较抽象。

现在就真正落地了。

例如:

复制代码
body<ApiResponse<Page<User>>>()

整个类型关系:

复制代码
ApiResponse<T>
        ↓
T = Page<User>
        ↓
Page<T>
        ↓
T = User

最终:

复制代码
JSON
↓
ApiResponse<Page<User>>

这就是统一网络模型能够成立的基础。


三十五、如果不同接口返回结构不统一怎么办?

真实项目可能存在:

复制代码
大部分接口
↓
{ code, msg, data }

第三方接口
↓
直接返回对象

下载接口
↓
ByteReadChannel

某个旧接口
↓
另一套 Response 格式

这时候不要强行所有 Client 都:

复制代码
body<ApiResponse<T>>()

因为前面已经讲过:

不同网络职责可以使用不同 NetworkClient。

例如:

复制代码
apiClient
↓
统一 ApiResponse<T>

weatherClient
↓
第三方 WeatherResponse

downloadClient
↓
流式 Response

所以统一响应也是有作用域的。


三十六、这再次说明多 Client 的价值

假设:

复制代码
apiClient

所有接口:

复制代码
{
  "code": 0,
  "msg": "",
  "data": {}
}

那么:

复制代码
apiClient
↓
统一 unwrap

完全合理。

但是:

复制代码
thirdPartyClient

可能:

复制代码
{
  "status": "ok",
  "result": {}
}

就不应该硬套:

复制代码
ApiResponse<T>

所以:

复制代码
统一

仍然应该理解成:

在正确的网络协议作用域内统一。


三十七、HttpResponse 和 ApiResponse 再对比一次

这个地方非常值得最后再强化。

复制代码
HttpResponse
↓
Ktor / HTTP 层

包含:
status
headers
body

而:

复制代码
ApiResponse<T>
↓
项目 / 后端协议层

包含:
code
msg
data

关系:

复制代码
HttpResponse
   │
   ├── status = 200
   ├── headers
   └── body
        ↓
   ApiResponse<T>

所以千万别把:

复制代码
HTTP status

和:

复制代码
业务 code

当成同一个东西。


三十八、可以把一次请求理解成四层结果

正式项目中,一次请求至少可能经过:

复制代码
第一层
网络连接
↓
有没有断网 / Timeout


第二层
HTTP
↓
401 / 404 / 500


第三层
数据格式
↓
JSON 能不能解析


第四层
业务协议
↓
code == 0 ?

只有都通过:

复制代码
Network OK
↓
HTTP OK
↓
Parse OK
↓
Business OK

最后才是:

复制代码
data = T

这张图其实就是后面异常体系的基础。


三十九、现在 NetworkClient 的职责更完整了

上一篇:

复制代码
NetworkClient
↓
统一 get/post/put/delete

这一篇以后:

复制代码
NetworkClient
    │
    ├── 发送 HTTP 请求
    ├── 解析 ApiResponse<T>
    ├── 检查业务 code
    └── 解包 data

业务得到:

复制代码
T

所以它开始真正成为:

项目级网络请求边界。


四十、本篇总结

后端统一响应:

复制代码
{
  "code": 0,
  "msg": "",
  "data": T
}

客户端可以定义:

复制代码
@Serializable
data class ApiResponse<T>(
    val code: Int,
    val msg: String,
    val data: T,
)

但一定要区分:

复制代码
HttpResponse
↓
HTTP 层


ApiResponse<T>
↓
业务协议层

请求成功需要至少经历:

复制代码
HTTP 成功
↓
JSON 解析成功
↓
code 成功
↓
data

所以:

复制代码
HTTP 200
≠
业务一定成功

NetworkClient 可以统一:

复制代码
HttpResponse
↓
ApiResponse<T>
↓
检查 code
↓
解包 data
↓
T

于是 ApiService:

复制代码
suspend fun getUser(
    id: Long,
): User {

    return networkClient.get(
        path = "users/$id",
    )
}

不再知道:

复制代码
code
msg
data

这些后端协议细节。

最终可以记住一句话:

HttpResponse 负责表达 HTTP 层结果,ApiResponse 负责表达后端业务协议;NetworkClient 的职责之一,就是把后端统一响应解析、校验并解包,让业务层最终只拿到真正需要的 T。

但到这里还有一个非常大的问题没有解决:

复制代码
断网怎么办?
Timeout 怎么办?
401 / 404 / 500 怎么办?
JSON 解析失败怎么办?
code != 0 怎么统一?
未知异常怎么办?

所以接下来必须进入整个 Ktor 网络层最重要的一篇之一。


下一篇

《Ktor 异常体系:断网、Timeout、HTTP、JSON 与业务错误如何统一成 AppError》

下一篇会把一次网络请求完整拆成:

复制代码
Network Error
↓
断网 / DNS / Socket

Timeout
↓
Request / Connect / Socket

HTTP Error
↓
401 / 403 / 404 / 500

Serialization Error
↓
JSON 解析失败

Business Error
↓
code != 0

Unknown Error
↓
兜底

最终统一成:

复制代码
Throwable
↓
ExceptionMapper
↓
AppError

让:

复制代码
NetworkClient
↓
不再把各种底层异常直接暴露给 Repository / ViewModel

这一步完成以后,整个 Ktor 网络层才真正开始具备正式项目的完整形态。

相关推荐
消失的旧时光-19439 天前
第一篇:Ktor Client 到底是什么?从 Retrofit 迁移理解 Ktor 网络请求架构
网络·架构·retrofit·ktor·dsl
小书房14 天前
KMP跨平台之数据库
数据库·kmp
消失的旧时光-19431 个月前
KMP Web 开发实战(二):js、wasmJs、browser、executable 到底是什么意思?
开发语言·前端·javascript·跨平台·kmp
小书房3 个月前
移动开发跨平台方案之RN/Flutter/KMP/CMP
flutter·react native·react·跨平台·rn·kmp·cmp
楼兰公子3 个月前
网络子系统学习与开发教程
rk3588·net
KillerNoBlood3 个月前
2026移动端跨平台开发面经总结
android·算法·flutter·ios·移动开发·鸿蒙·kmp
hnjzsyjyj4 个月前
东方博宜OJ 2362:前缀和后缀 ← KMP算法
kmp
WhiteElm4 个月前
KMP算法究竟解决什么问题?核心是什么?
kmp
jyyyx的算法博客5 个月前
KMP 算法
c++·kmp