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

如何在Retrofit2中使用密封类作为返回类型?

正确实现Kotlin密封类+Retrofit2+RxJava的翻译请求处理

看起来你已经搭好了基础的结构,接下来我们把各个环节补全,确保整个请求流程能正确跑起来,同时发挥密封类的类型安全优势:

1. 先搞定Retrofit的密封类序列化

Kotlin密封类的序列化需要特殊处理,默认Gson没法自动识别子类。我们得给Gson加个适配器来区分Success和Error这两个子类,步骤如下:

首先在Gradle里添加Gson扩展依赖(如果还没加):

implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.gson:gson-extras:2.10.1'

然后创建Gson实例时,注册密封类的类型适配器——这里需要后端配合返回一个标识字段(比如type),用来告诉Gson当前返回的是哪个子类:

// 注册密封类的类型适配器,"type"是后端返回的区分字段
val typeAdapter = RuntimeTypeAdapterFactory.of(TranslationResponse::class.java, "type")
    .registerSubtype(Success::class.java, "success")
    .registerSubtype(Error::class.java, "error")

val gson = GsonBuilder()
    .registerTypeAdapterFactory(typeAdapter)
    .create()

举个例子,后端成功时应该返回类似这样的JSON:

{"type":"success", "code":200, "text":["你好"]}

错误时返回:

{"type":"error", "code":401, "message":"API密钥无效"}

如果后端没法加这个type字段,也可以换一种思路:用Retrofit的Response包裹成功数据,手动处理错误,后面会提到这种方案。

2. 完善Retrofit的初始化和接口定义

你的接口定义基本没问题,但注意@POST配合@Query是把参数拼在URL上,如果需要把参数放在请求体里,应该用@Body或者@Field(配合@FormUrlEncoded)。另外,默认的key参数建议填你的实际API密钥,而不是空字符串:

interface TranslationApi {
    @POST("/your-translate-path") // 替换成实际的接口路径
    fun query(
        @Query("text") text: String,
        @Query("lang") lang: String,
        @Query("key") key: String = "your-real-api-key"
    ): Observable<TranslationResponse>
}

然后初始化Retrofit时,要把刚才的Gson和RxJava适配器加上:

val retrofit = Retrofit.Builder()
    .baseUrl("https://your-api-domain.com/") // 替换成实际的API域名
    .addConverterFactory(GsonConverterFactory.create(gson))
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .build()

val translationApi = retrofit.create(TranslationApi::class.java)

3. 正确处理RxJava的请求和错误逻辑

你用onErrorReturn把错误转换成Error实例的思路是对的,但可以优化错误码的获取——Retrofit抛出的错误大多是HttpException,里面包含了HTTP响应的状态码,我们可以针对性处理:

translationApi.query("Hello", "en-zh")
    .onErrorReturn { throwable ->
        // 根据错误类型获取对应的错误码
        val errorCode = when (throwable) {
            is HttpException -> throwable.code() // 拿到HTTP状态码,比如404、500
            else -> 0 // 非HTTP错误,比如解析失败、网络断开
        }
        Error(
            code = errorCode,
            message = throwable.message ?: "发生未知错误"
        )
    }
    .subscribeOn(Schedulers.io()) // 在IO线程执行网络请求
    .observeOn(AndroidSchedulers.mainThread()) // Android环境下切回主线程处理结果
    .subscribe({ response ->
        // 用when处理密封类,编译器会帮我们做穷尽检查,不会遗漏分支
        when (response) {
            is Success -> {
                // 处理成功结果,比如把翻译文本展示到UI上
                println("翻译成功:${response.text.joinToString()}")
            }
            is Error -> {
                // 处理错误,比如弹Toast提示用户
                println("翻译失败:${response.message}(错误码:${response.code})")
            }
        }
    }, {
        // 这里其实不会走到,因为onErrorReturn已经把所有错误转换成了Error实例
        // 但如果有极端情况可以保留这个分支做兜底
        println("意外错误:${it.message}")
    })

备选方案:不用密封类序列化,手动封装结果

如果后端没法返回type字段,或者你不想处理密封类的序列化,可以改用Response包裹成功数据,手动封装成TranslationResponse的子类:

// 修改接口定义,返回Observable<Response<Success>>
interface TranslationApi {
    @POST("/your-translate-path")
    fun query(
        @Query("text") text: String,
        @Query("lang") lang: String,
        @Query("key") key: String = "your-api-key"
    ): Observable<Response<Success>>
}

// 调用时手动处理响应
translationApi.query("Hello", "en-zh")
    .map { response ->
        if (response.isSuccessful) {
            // 成功时取出body,封装成Success实例
            response.body()?.let { Success(it.code, it.text) } ?: Error(-1, "响应内容为空")
        } else {
            // 错误时解析错误信息,封装成Error实例
            val errorMsg = runCatching {
                response.errorBody()?.string()
            }.getOrNull() ?: "请求失败"
            Error(response.code(), errorMsg)
        }
    }
    .onErrorReturn { Error(0, it.message ?: "网络异常") }
    .subscribe(...) // 后续的订阅逻辑和之前一样

这种方式更灵活,不用依赖后端的类型字段,适合格式差异较大的成功/错误响应。

内容的提问来源于stack exchange,提问作者Danil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 10:32:11