EitherNet
EitherNet 是一个多平台、可插拔且密封的 API 结果类型,用于建模网络 API 响应。目前,它仅在 JVM 上为 Retrofit 实现,但核心 API 定义在公共代码中,可以为其他平台实现。
以下 README 内容主要关注 Retrofit 实现。
使用方法
默认情况下,Retrofit 使用异常来传播错误。本库利用 Kotlin 密封类型来更好地模型化这些响应,提供类型安全的单一返回点,无需异常处理!
核心类型是 ApiResult<out T, out E>
,其中 T
是成功类型,E
是可能的错误类型。
ApiResult
有两个密封子类型:Success
和 Failure
。Success
类型为 T
,没有错误类型;Failure
类型为 E
,没有成功类型。Failure
又有四个密封子类型:Failure.NetworkFailure
、Failure.ApiFailure
、Failure.HttpFailure
和 Failure.UnknownFailure
。这允许通过一致的、非异常流程使用密封 when
分支简单处理结果。
when (val result = myApi.someEndpoint()) {
is Success -> doSomethingWith(result.response)
is Failure -> when (result) {
is NetworkFailure -> showError(result.error)
is HttpFailure -> showError(result.code)
is ApiFailure -> showError(result.error)
is UnknownFailure -> showError(result.error)
}
}
通常,用户代码只需为 Failure
情况显示一个通用错误消息,但密封子类型也允许更具体的错误消息或错误类型的可插拔性。
只需将端点返回类型更改为类型化的 ApiResult
,并包含我们的调用适配器和委托转换器工厂。
interface TestApi {
@GET("/")
suspend fun getData(): ApiResult<SuccessResponse, ErrorResponse>
}
val api = Retrofit.Builder()
.addConverterFactory(ApiResultConverterFactory)
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.build()
.create<TestApi>()
如果没有自定义错误返回类型,只需将错误类型设为 Unit
。
解码错误主体
如果要解码 HttpFailure
中的错误类型,请使用 @DecodeErrorBody
注解标记端点:
interface TestApi {
@DecodeErrorBody
@GET("/")
suspend fun getData(): ApiResult<SuccessResponse, ErrorResponse>
}
现在,4xx 或 5xx 响应将尝试将其错误主体(如果有)解码为 ErrorResponse
。如果要根据状态码上下文解码错误主体,可以在自定义 Retrofit Converter
中从注解中检索 @StatusCode
注解。
// 在您自己的转换器工厂中
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
val (statusCode, nextAnnotations) = annotations.statusCode()
?: return null
val errorType = when (statusCode.value) {
401 -> Unauthorized::class.java
404 -> NotFound::class.java
// ...
}
val errorDelegate = retrofit.nextResponseBodyConverter<Any>(this, errorType.toType(), nextAnnotations)
return MyCustomBodyConverter(errorDelegate)
}
注意,内容长度为 0 的错误主体将被跳过。
可插拔性
某些 API 的常见模式是返回多态的 200
响应,其中数据需要动态解析。考虑以下示例:
{
"ok": true,
"data": {
...
}
}
同一 API 在错误事件中可能返回这种结构:
{
"ok": false,
"error_message": "请重试。"
}
这很难用单一具体类型建模,但使用 ApiResult
很容易处理。只需在自定义 Retrofit Converter
中抛出带有解码错误类型的 ApiException
,它将自动作为带有该错误实例的 Failure.ApiFailure
类型呈现。
@GET("/")
suspend fun getData(): ApiResult<SuccessResponse, ErrorResponse>
// 在您自己的转换器工厂中
class ErrorConverterFactory : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
// 这返回一个 `@ResultType` 实例,可用于通过 toType() 获取错误类型
val (errorType, nextAnnotations) = annotations.errorType() ?: return null
return ResponseBodyConverter(errorType.toType())
}
class ResponseBodyConverter(
private val errorType: Type
) : Converter<ResponseBody, *> {
override fun convert(value: ResponseBody): String {
if (value.isErrorType()) {
val errorResponse = ...
throw ApiException(errorResponse)
} else {
return SuccessResponse(...)
}
}
}
}
重试
网络请求的一个常见模式是使用指数退避进行重试。EitherNet 提供了一个高度可配置的 retryWithExponentialBackoff()
函数用于这种情况。
// 默认值供参考
val result = retryWithExponentialBackoff(
maxAttempts = 3,
initialDelay = 500.milliseconds,
delayFactor = 2.0,
maxDelay = 10.seconds,
jitterFactor = 0.25,
onFailure = null, // 可选的失败回调,用于日志记录
) {
api.getData()
}
测试
EitherNet 提供了一个 测试夹具 工件,其中包含 EitherNetController
API,允许轻松测试 EitherNet API。这类似于 OkHttp 的 MockWebServer
,可以为特定端点排队结果。
只需在测试中使用 newEitherNetController()
函数之一创建一个新的控制器实例。
val controller = newEitherNetController<PandaApi>() // 具体化类型
然后,您可以从中访问底层模拟的 api
属性,并将其传递给被测试的对象。
// 从控制器获取 api 实例并传递给被测试的对象
val provider = PandaDataProvider(controller.api)
最后,根据需要为端点排队结果。
// 在测试中,您可以为特定端点排队结果
controller.enqueue(PandaApi::getPandas, ApiResult.success("Po"))
您还可以选择传入完整的挂起函数,如果需要动态行为:
controller.enqueue(PandaApi::getPandas) {
// 这是一个挂起函数!
delay(1000)
ApiResult.success("Po")
}
在使用依赖注入的集成测试中,您可以在测试模块中提供控制器及其底层 API,并替换标准模块。这与 Anvil 配合得特别好。
@ContributesTo(
scope = UserScope::class,
replaces = [PandaApiModule::class] // 替换标准模块
)
@Module
object TestPandaApiModule {
@Provides
fun providePandaApiController(): EitherNetController<PandaApi> = newEitherNetController()
@Provides
fun providePandaApi(
controller: EitherNetController<PandaApi>
): PandaApi = controller.api
}
然后,您可以在测试中注入控制器,而 PandaApi
的用户将获得您的测试实例。
Java 互操作性
对于 Java 互操作性,JavaEitherNetControllers.enqueueFromJava
提供了有限的 API。
验证
EitherNetController
会在后台对 API 端点进行一些小型验证。如果您想在此基础上添加自己的验证,可以通过 ServiceLoader
提供 ApiValidator
的实现。有关更多信息,请参阅 ApiValidator
的文档。
安装
dependencies {
implementation("com.slack.eithernet:eithernet:<版本>")
implementation("com.slack.eithernet:eithernet-integration-retrofit:<版本>")
// 测试夹具
testImplementation(testFixtures("com.slack.eithernet:eithernet:<版本>"))
}
开发版本的快照可在 Sonatype 的 snapshots
仓库中获得。
许可证
Copyright 2020 Slack Technologies, LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.