Created
August 15, 2020 18:24
-
-
Save sinadarvi/581d5f0c7023d8c2e35e10004e85e675 to your computer and use it in GitHub Desktop.
Call Adapter + Suspend function
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data class ErrorResponse( | |
val status: String, | |
val code: String, | |
val message: String | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data class NewsResponse( | |
val status: String, | |
val totalResults: Int, | |
val articles: List<Article> | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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