Skip to content

Instantly share code, notes, and snippets.

@sinadarvi
Created August 15, 2020 18:24
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sinadarvi/581d5f0c7023d8c2e35e10004e85e675 to your computer and use it in GitHub Desktop.
Save sinadarvi/581d5f0c7023d8c2e35e10004e85e675 to your computer and use it in GitHub Desktop.
Call Adapter + Suspend function
data class ErrorResponse(
val status: String,
val code: String,
val message: String
)
import com.darvishi.sina.simplenews.model.Result
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Converter
import java.lang.reflect.Type
/**
* [responseType] Returns the value type that this adapter uses when converting the HTTP response
* body to a Java object
*
* [adapt] Returns an instance of T which delegates to call, here we will use our NetworkResponseCall
* that we just created.
*
* @param <S> Successful body type
* @param <E> Error Body Type
*/
class NetworkResponseAdapter<S : Any, E : Any>(
private val successType: Type,
private val errorBodyConverter: Converter<ResponseBody, E>
) : CallAdapter<S, Call<Result<S, E>>> {
override fun responseType(): Type = successType
override fun adapt(call: Call<S>): Call<Result<S, E>> {
return NetworkResponseCall(call, errorBodyConverter)
}
}
import com.darvishi.sina.simplenews.model.Result
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
class NetworkResponseAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
// suspend functions wrap the response type in `Call`
if (Call::class.java != getRawType(returnType)) {
return null
}
// check first that the return type is `ParameterizedType`
check(returnType is ParameterizedType) {
"return type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>"
}
// get the response type inside the `Call` type
val responseType = getParameterUpperBound(0, returnType)
// if the response type is not ApiResponse then we can't handle this type, so we return null
if (getRawType(responseType) != Result::class.java) {
return null
}
// the response type is ApiResponse and should be parameterized
check(responseType is ParameterizedType) { "Response must be parameterized as NetworkResponse<Foo> or NetworkResponse<out Foo>" }
val successBodyType = getParameterUpperBound(0, responseType)
val errorBodyType = getParameterUpperBound(1, responseType)
val errorBodyConverter =
retrofit.nextResponseBodyConverter<Any>(null, errorBodyType, annotations)
return NetworkResponseAdapter<Any, Any>(successBodyType, errorBodyConverter)
}
}
import com.darvishi.sina.simplenews.model.Result
import okhttp3.Request
import okhttp3.ResponseBody
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Converter
import retrofit2.Response
import java.io.IOException
internal class NetworkResponseCall<S : Any, E : Any>(
private val delegate: Call<S>,
private val errorConverter: Converter<ResponseBody, E>
) : Call<Result<S, E>> {
/**
* What is the [enqueue] method?
*
* Asynchronously send the request and notify callback of its response or if an error occurred
* talking to the server, creating the request, or processing the response.
*/
override fun enqueue(callback: Callback<Result<S, E>>) {
/**
* enqueue takes a callback which has two methods to implement:
*
* **onResponse**: which is invoked for a received HTTP response, this response could be success
* response or failure one. So we have to check here if the response is successful, we return
* the success state of our NetworkResponse sealed class If it’s not a success response, we try
* to parse the error body as the expected error data class we provide as a type, if the parse
* succeeded we return the error as ApiError state, otherwise it’s UnknownError.
*
* **onFailure**: which is invoked when a network exception occurred talking to the server or when
* an unexpected exception occurred creating the request or processing the response. Here we can
* simply check if the exception is IOException then we return the NetworkError state, otherwise
* it should be UnknownError state.
*
*/
return delegate.enqueue(object : Callback<S> {
override fun onResponse(call: Call<S>, response: Response<S>) {
val body = response.body()
val code = response.code()
val error = response.errorBody()
if (response.isSuccessful) {
if (body != null) {
callback.onResponse(
this@NetworkResponseCall,
Response.success(
Result.Success(
body
)
)
)
} else {
// Response is successful but the body is null
callback.onResponse(
this@NetworkResponseCall,
Response.success(Result.UnknownError(null))
)
}
} else {
val errorBody = when {
error == null -> null
error.contentLength() == 0L -> null
else -> try {
errorConverter.convert(error)
} catch (ex: Exception) {
null
}
}
if (errorBody != null) {
callback.onResponse(
this@NetworkResponseCall,
Response.success(Result.ApiError(errorBody, code))
)
} else {
callback.onResponse(
this@NetworkResponseCall,
Response.success(Result.UnknownError(null))
)
}
}
}
override fun onFailure(call: Call<S>, throwable: Throwable) {
val networkResponse = when (throwable) {
is IOException -> Result.NetworkError(throwable)
else -> Result.UnknownError(throwable)
}
callback.onResponse(this@NetworkResponseCall, Response.success(networkResponse))
}
})
}
override fun isExecuted() = delegate.isExecuted
override fun clone() = NetworkResponseCall(delegate.clone(), errorConverter)
override fun isCanceled() = delegate.isCanceled
override fun cancel() = delegate.cancel()
override fun execute(): Response<Result<S, E>> {
//because it will send a request Synchronously
throw UnsupportedOperationException("NetworkResponseCall doesn't support execute")
}
override fun request(): Request = delegate.request()
override fun timeout(): Timeout = delegate.timeout()
}
data class NewsResponse(
val status: String,
val totalResults: Int,
val articles: List<Article>
)
interface NewsService {
@GET("top-headlines")
suspend fun getTopHeadLines(
@Query("country") country: String? = null,
@Query("category") category: String? = null,
@Query("sources") sources: String? = null,
@Query("q") query: String? = null,
@Query("pageSize") pageSize: Int? = null,
@Query("page") page: Int? = null
): Result<NewsResponse, ErrorResponse>
}
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.buffer
import okio.source
import com.darvishi.sina.simplenews.model.Result
import com.google.common.truth.Truth.assertThat
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import com.google.gson.GsonBuilder
import kotlinx.coroutines.runBlocking
import okio.EOFException
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import org.junit.Rule
@ExperimentalCoroutinesApi
@RunWith(JUnit4::class)
class NewsServiceTest {
@get:Rule
val liveDataRule = InstantTaskExecutorRule()
private lateinit var service: NewsService
private lateinit var mockWebServer: MockWebServer
@Before
fun createService() {
val strategy: ExclusionStrategy = object : ExclusionStrategy {
override fun shouldSkipClass(clazz: Class<*>?): Boolean {
return false
}
override fun shouldSkipField(field: FieldAttributes): Boolean {
return field.getAnnotation(Exclude::class.java) != null
}
}
val gson = GsonBuilder()
.addSerializationExclusionStrategy(strategy)
.create()
mockWebServer = MockWebServer()
service = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addCallAdapterFactory(NetworkResponseAdapterFactory())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(NewsService::class.java)
}
@After
fun teardown() {
mockWebServer.shutdown()
}
@ExperimentalCoroutinesApi
@Test
fun `getting top headlines in US as result of Success NewsResponse`() = runBlocking {
enqueueResponse("top_headlines_in_us.json")
val topHeadlinesInUs =
service.getTopHeadLines(country = "us") as Result.Success<NewsResponse>
val request = mockWebServer.takeRequest()
assertThat(request.path)
.isEqualTo("/top-headlines?country=us")
assertThat(topHeadlinesInUs)
.isNotNull()
assertThat(topHeadlinesInUs.body.totalResults)
.isEqualTo(38)
assertThat(topHeadlinesInUs.body.articles[0].author)
.isEqualTo("Erica Werner, Jeff Stein, Seung Min Kim")
}
private fun enqueueResponse(
fileName: String = "",
isError: Boolean = false,
headers: Map<String, String> = emptyMap()
) {
val inputStream = javaClass.classLoader!!
.getResourceAsStream("responses/$fileName")
val source = inputStream.source().buffer()
val mockResponse = MockResponse()
for ((key, value) in headers) {
mockResponse.addHeader(key, value)
}
if (isError)
mockResponse.setResponseCode(400)
else
mockResponse.setResponseCode(200)
mockWebServer.enqueue(
mockResponse.setBody(source.readString(Charsets.UTF_8))
)
}
}
import java.io.IOException
sealed class Result<out T : Any, out U : Any> {
/**
* Success response with body
*/
data class Success<T : Any>(val body: T) : Result<T, Nothing>()
/**
* Loading response with body
*/
data class Loading<T: Any>(val body: T? = null): Result<T, Nothing>()
/**
* Failure response with body
*/
data class ApiError<U : Any>(val body: U, val code: Int) : Result<Nothing, U>()
/**
* Network error
*/
data class NetworkError(val error: IOException) : Result<Nothing, Nothing>()
/**
* For example, json parsing error
*/
data class UnknownError(val error: Throwable?) : Result<Nothing, Nothing>()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment