如何在Retrofit2中使用密封类作为返回类型?
看起来你已经搭好了基础的结构,接下来我们把各个环节补全,确保整个请求流程能正确跑起来,同时发挥密封类的类型安全优势:
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

