I need to do custom error handling in my api and I wanted to use coroutines with the new version of Retrofit. Since we don't have to use Deferred any longer, our own Jake Wharton wrote this on reddit a month ago
https://github.com/square/retrofit/blob/master/samples/src/main/java/com/example/retrofit/RxJavaObserveOnMainThread.java
But I'm having problems creating the CallAdapterFactory properly.
To be specific, I don't understand: "Delegates to built-in factory and then wraps the value in sealed class"
Is there anyone already using this setup that can help?
Here's the current code
sealed class Results<out T: Any> {
class Success<out T: Any>(val response: T): Results<T>()
class Failure(val message: String, val serverError: ServerError?): Results<Nothing>()
object NetworkError: Results<Nothing>()
}
class ResultsCallAdapterFactory private constructor() : CallAdapter.Factory() {
companion object {
#JvmStatic
fun create() = ResultsCallAdapterFactory()
}
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
return try {
val enclosedType = returnType as ParameterizedType
val responseType = getParameterUpperBound(0, enclosedType)
val rawResultType = getRawType(responseType)
val delegate: CallAdapter<Any,Any> = retrofit.nextCallAdapter(this,returnType,annotations) as CallAdapter<Any,Any>
if(rawResultType != Results::class.java)
null
else {
object: CallAdapter<Any,Any>{
override fun adapt(call: Call<Any>): Any {
val response = delegate.adapt(call)
//What should happen here?
return response
}
override fun responseType(): Type {
return delegate.responseType()
}
}
}
} catch (e: ClassCastException) {
null
}
}
}
I've created an example of such a factory, you can find it here on GitHub. Also take a look at a similar question: How to create a call adapter for suspending functions in Retrofit?.
Related
The result of API call in my Android app can be a JSON with configuration which is mapped to SupportConfigurationJson class, or just pure null. When I get a JSON, the app works properly, but when I get null, I get this exception:
kotlinx.serialization.json.internal.JsonDecodingException: Expected start of the object '{', but had 'EOF' instead
JSON input: null
I should avoid using GSON in this project. I also found a solution, where API interface will return Response<JSONObject>, and after that my repository should check if this JSONObject is null and map it to SupportConfigurationJson if not. But in the project we always used responses with custom classes so I wonder, is there any other solution to get response with null or custom data class?
GettSupportConfiguration usecase class:
class GetSupportConfiguration #Inject constructor(
private val supportConfigurationRepository: SupportConfigurationRepository
) {
suspend operator fun invoke(): Result<SupportConfiguration?> {
return try {
success(supportConfigurationRepository.getSupportConfiguration())
} catch (e: Exception) {
/*
THIS SOLUTION WORKED, BUT I DON'T THINK IT IS THE BEST WAY TO SOLVE THE PROBLEM
if (e.message?.contains("JSON input: null") == true) {
success(null)
} else {
failure(e)
}
*/
//I WAS USING THROW HERE TO SEE WHY THE APP ISN'T WORKING PROPERLY
//throw(e)
failure(e)
}
}
}
SupportConfigurationJson class:
#Serializable
data class SupportConfigurationJson(
#SerialName("image_url")
val imageUrl: String,
#SerialName("description")
val description: String,
#SerialName("phone_number")
val phoneNumber: String?,
#SerialName("email")
val email: String?
)
SupportConfigurationRepository class:
#Singleton
class SupportConfigurationRepository #Inject constructor(
private val api: SupportConfigurationApi,
private val jsonMapper: SupportConfigurationJsonMapper
) {
suspend fun getSupportConfiguration(): SupportConfiguration? =
mapJsonToSupportConfiguration(api.getSupportConfiguration().extractOrThrow())
private suspend fun mapJsonToSupportConfiguration(
supportConfiguration: SupportConfigurationJson?
) = withContext(Dispatchers.Default) {
jsonMapper.mapToSupportSettings(supportConfiguration)
}
}
fun <T> Response<T?>.extractOrThrow(): T? {
val body = body()
return if (isSuccessful) body else throw error()
}
fun <T> Response<T>.error(): Throwable {
val statusCode = HttpStatusCode.from(code())
val errorBody = errorBody()?.string()
val cause = RuntimeException(errorBody ?: "Unknown error.")
return when {
statusCode.isClientError -> ClientError(statusCode, errorBody, cause)
statusCode.isServerError -> ServerError(statusCode, errorBody, cause)
else -> ResponseError(statusCode, errorBody, cause)
}
}
SupportConfigurationApi class:
interface SupportConfigurationApi {
#GET("/mobile_api/v1/support/configuration")
suspend fun getSupportConfiguration(): Response<SupportConfigurationJson?>
}
SupportConfigurationJsonMapper class:
class SupportConfigurationJsonMapper #Inject constructor() {
fun mapToSupportSettings(json: SupportConfigurationJson?): SupportConfiguration? {
return if (json != null) {
SupportConfiguration(
email = json.email,
phoneNumber = json.phoneNumber,
description = json.description,
imageUrl = Uri.parse(json.imageUrl)
)
} else null
}
}
I create Retrofit like this:
#Provides
#AuthorizedRetrofit
fun provideAuthorizedRetrofit(
#AuthorizedClient client: OkHttpClient,
#BaseUrl baseUrl: String,
converterFactory: Converter.Factory
): Retrofit {
return Retrofit.Builder()
.client(client)
.baseUrl(baseUrl)
.addConverterFactory(converterFactory)
.build()
}
#Provides
#ExperimentalSerializationApi
fun provideConverterFactory(json: Json): Converter.Factory {
val mediaType = "application/json".toMediaType()
return json.asConverterFactory(mediaType)
}
Everything is explained here (1min read)
Api is supposed to return "{}" for null, If you can't change API add this converter to Retrofit
You are interacting with your repository directly, i will suggest to use
usecases
to interact with data layer.
Because you are not catching this exception over here, your app is crashing
suspend fun getSupportConfiguration(): SupportConfiguration? =
mapJsonToSupportConfiguration(api.getSupportConfiguration().extractOrThrow())
Usecase usually catch these errors and show useful error msg at the ui.
I have a favorite way of doing network request on Android (using Retrofit). It looks like this:
// NetworkApi.kt
interface NetworkApi {
#GET("users")
suspend fun getUsers(): List<User>
}
And in my ViewModel:
// MyViewModel.kt
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
val usersLiveData = flow {
emit(networkApi.getUsers())
}.asLiveData()
}
Finally, in my Activity/Fragment:
//MyActivity.kt
class MyActivity: AppCompatActivity() {
private viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.usersLiveData.observe(this) {
// Update the UI here
}
}
}
The reason I like this way is because it natively works with Kotlin flow, which is very easy to use, and has a lot of useful operations (flatMap, etc).
However, I am not sure how to elegantly handle network errors using this method. One approach that I can think of is to use Response<T> as the return type of the network API, like this:
// NetworkApi.kt
interface NetworkApi {
#GET("users")
suspend fun getUsers(): Response<List<User>>
}
Then in my view model, I can have an if-else to check the isSuccessful of the response, and get the real result using the .body() API if it is successful. But it will be problematic when I do some transformation in my view model. E.g.
// MyViewModel.kt
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
val usersLiveData = flow {
val response = networkApi.getUsers()
if (response.isSuccessful) {
emit(response.body()) // response.body() will be List<User>
} else {
// What should I do here?
}
}.map { // it: List<User>
// transform Users to some other class
it?.map { oneUser -> OtherClass(oneUser.userName) }
}.asLiveData()
Note the comment "What should I do here?". I don't know what to do in that case. I could wrap the responseBody (in this case, a list of Users) with some "status" (or simply just pass through the response itself). But that means that I pretty much have to use an if-else to check the status at every step through the flow transformation chain, all the way up to the UI. If the chain is really long (e.g. I have 10 map or flatMapConcat on the chain), it is really annoying to do it in every step.
What is the best way to handle network errors in this case, please?
You should have a sealed class to handle for different type of event. For example, Success, Error or Loading. Here is some of the example that fits your usecases.
enum class ApiStatus{
SUCCESS,
ERROR,
LOADING
} // for your case might be simplify to use only sealed class
sealed class ApiResult <out T> (val status: ApiStatus, val data: T?, val message:String?) {
data class Success<out R>(val _data: R?): ApiResult<R>(
status = ApiStatus.SUCCESS,
data = _data,
message = null
)
data class Error(val exception: String): ApiResult<Nothing>(
status = ApiStatus.ERROR,
data = null,
message = exception
)
data class Loading<out R>(val _data: R?, val isLoading: Boolean): ApiResult<R>(
status = ApiStatus.LOADING,
data = _data,
message = null
)
}
Then, in your ViewModel,
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
// this should be returned as a function, not a variable
val usersLiveData = flow {
emit(ApiResult.Loading(true)) // 1. Loading State
val response = networkApi.getUsers()
if (response.isSuccessful) {
emit(ApiResult.Success(response.body())) // 2. Success State
} else {
val errorMsg = response.errorBody()?.string()
response.errorBody()?.close() // remember to close it after getting the stream of error body
emit(ApiResult.Error(errorMsg)) // 3. Error State
}
}.map { // it: List<User>
// transform Users to some other class
it?.map { oneUser -> OtherClass(oneUser.userName) }
}.asLiveData()
In your view (Activity/Fragment), observe these state.
viewModel.usersLiveData.observe(this) { result ->
// Update the UI here
when(result.status) {
ApiResult.Success -> {
val data = result.data <-- return List<User>
}
ApiResult.Error -> {
val errorMsg = result.message <-- return errorBody().string()
}
ApiResult.Loading -> {
// here will actually set the state as Loading
// you may put your loading indicator here.
}
}
}
//this class represent load statement management operation
/*
What is a sealed class
A sealed class is an abstract class with a restricted class hierarchy.
Classes that inherit from it have to be in the same file as the sealed class.
This provides more control over the inheritance. They are restricted but also allow freedom in state representation.
Sealed classes can nest data classes, classes, objects, and also other sealed classes.
The autocomplete feature shines when dealing with other sealed classes.
This is because the IDE can detect the branches within these classes.
*/
ٍٍٍٍٍ
sealed class APIResponse<out T>{
class Success<T>(response: Response<T>): APIResponse<T>() {
val data = response.body()
}
class Failure<T>(response: Response<T>): APIResponse<T>() {
val message:String = response.errorBody().toString()
}
class Exception<T>(throwable: Throwable): APIResponse<T>() {
val message:String? = throwable.localizedMessage
}
}
create extention file called APIResponsrEX.kt
and create extextion method
fun <T> APIResponse<T>.onSuccess(onResult :APIResponse.Success<T>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Success) onResult(this)
return this
}
fun <T> APIResponse<T>.onFailure(onResult: APIResponse.Failure<*>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Failure<*>)
onResult(this)
return this
}
fun <T> APIResponse<T>.onException(onResult: APIResponse.Exception<*>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Exception<*>) onResult(this)
return this
}
merge it with Retrofit
inline fun <T> Call<T>.request(crossinline onResult: (response: APIResponse<T>) -> Unit) {
enqueue(object : retrofit2.Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
// success
onResult(APIResponse.Success(response))
} else {
//failure
onResult(APIResponse.Failure(response))
}
}
override fun onFailure(call: Call<T>, throwable: Throwable) {
onResult(APIResponse.Exception(throwable))
}
})
}
I have a sealed class for state handling of my Retrofit responses. It's members take a generic type. I would like to get Retrofit to be able to return the proper object, but I am stuck at this error: Unable to create converter for com.my.app.DataResult<?> - Cannot serialize abstract class com.my.app.DataResult
This is my DataResult class:
sealed class DataResult<out T> {
data class Success<out T>(val data: T?) : DataResult<T>()
data class Error<out T>(val code: Int? = null, val error: Exception? = null) : DataResult<T>()
object NetworkError : DataResult<Nothing>()
fun isSuccess() = this is Success<*>
fun isError() = this is Error<*>
fun data() = if (isSuccess()) (this as Success<T>).data else null
}
fun successResult() = DataResult.Success(null)
fun <T> successResult(data: T?) = DataResult.Success(data)
fun errorResult() = DataResult.Error<Nothing>(null)
This is the rest of my current implementation:
class NetworkClient(private val httpClient: HttpClient) {
private val baseUrl: String = "some url"
private val retrofit = Retrofit.Builder()
.baseUrl(mockend)
.addCallAdapterFactory(MyCallAdapterFactory())
.addConverterFactory(MoshiConverterFactory.create())
.client(httpClient.get())
.build()
private val apiService: ApiService = retrofit.create(StaApiService::class.java)
suspend fun <T> sendGet(endPoint: EndPoint, input: String): DataResult<T> {
val result = apiService.sendGetRequest<T>(endPoint.stringValue, queryMapOf(Pair("query", input)))
when (result) {
// do stuff here?
}
return result
}
}
interface ApiService {
#GET
suspend fun <T> sendGetRequest(
#Url url: String,
#QueryMap parameters: Map<String, String>): DataResult<T>
#GET
suspend fun <T> sendGetListRequest(
#Url url: String,
#QueryMap parameters: Map<String, String>): DataResult<List<T>>
}
abstract class CallDelegate<TIn, TOut>(
protected val proxy: Call<TIn>
) : Call<TOut> {
override fun execute(): Response<TOut> = throw NotImplementedError()
final override fun enqueue(callback: Callback<TOut>) = enqueueImpl(callback)
final override fun clone(): Call<TOut> = cloneImpl()
override fun cancel() = proxy.cancel()
override fun request(): Request = proxy.request()
override fun isExecuted() = proxy.isExecuted
override fun isCanceled() = proxy.isCanceled
abstract fun enqueueImpl(callback: Callback<TOut>)
abstract fun cloneImpl(): Call<TOut>
}
class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, DataResult<T>>(proxy) {
override fun enqueueImpl(callback: Callback<DataResult<T>>) = proxy.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val code = response.code()
val result: DataResult<T> = if (code in 200 until 300) {
val body = response.body()
DataResult.Success(body)
} else {
DataResult.Error(code)
}
callback.onResponse(this#ResultCall, Response.success(result))
}
override fun onFailure(call: Call<T>, t: Throwable) {
val result: DataResult<Nothing> = if (t is IOException) {
DataResult.NetworkError
} else {
DataResult.Error(null)
}
callback.onResponse(this#ResultCall, Response.success(result))
}
})
override fun cloneImpl() = ResultCall(proxy.clone())
}
class ResultAdapter(
private val type: Type
) : CallAdapter<Type, Call<DataResult<Type>>> {
override fun responseType() = type
override fun adapt(call: Call<Type>): Call<DataResult<Type>> = ResultCall(call)
}
class MyCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
) = when (getRawType(returnType)) {
Call::class.java -> {
val callType = getParameterUpperBound(0, returnType as ParameterizedType)
when (getRawType(callType)) {
Result::class.java -> {
val resultType = getParameterUpperBound(0, callType as ParameterizedType)
ResultAdapter(resultType)
}
else -> null
}
}
else -> null
}
}
The above code is largely inspired by this answer to another question,
but I'm trying to add Generics to the mix, so I don't have to put every request into the interface by hand. Is it possible or not? I have tried for hours, also tried to build an adapter for the sealed class but failed. Has someone a good resource how this can be done?
As you can also see in the code I'd like to also be able to receive lists. Any tips here are much appreciated too.
I have trouble to parse some inner part of JSON (with Moshi) that can vary from a lot and is highly unstructured. Overall it looks like:
response: {
items: [{
type: "typeA",
data: {
"1563050214700-001": {
foo: 123 ....
}
}
}, {
type: "typeB",
data: {
"1563050214700-002": {[
// differs a lot from previous one
{bar: 123 .... }
]}
}
}]
}
And data class structure looks like:
data class Response(
val items: Map<String,List<Item?>>?
) {
data class Item(
val type: String?,
val data: Map<String,List<DataItem?>>?
) {
data class DataItem(
// members highly unstructured
)
}
}
Schema of "DataItem" varies a lot. Looks like Moshi codegen supports adapters that can potentially allow manual parsing of these inner data classes but I'm not able to find the right tutorial or example. Ideally, I want entire Response parsed just as if it were a well-defined JSON.
Here is how I use retrofit/moshi
#Provides
#Singleton
#MyApp
fun provideMyAppRetrofit(context: Context, #MyApp okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(context.getString(R.string.APP_BASE_URL))
.build()
}
#Provides
#Singleton
fun provideMyAppApiService(#MyApp retrofit: Retrofit): MyAppApiService {
return retrofit.create(MyAppApiService::class.java)
}
How do I achieve this? Any sample or reference implementation will be helpful.
welcome to the polymorphic JSON parsing problems word!
We were writing own JSON adapters that are looking like:
internal class CardJsonAdapter(
moshi: Moshi
) : JsonAdapter<Card>() {
private val cardTypeAdapter = moshi.adapter(Card.Type::class.java)
private val amountsWithActionAdapter = moshi.adapter(AmountsWithActionCard::class.java)
private val backgroundImageCardAdapter = moshi.adapter(BackgroundImageCard::class.java)
#Suppress("TooGenericExceptionCaught")
override fun fromJson(reader: JsonReader): Card = try {
#Suppress("UNCHECKED_CAST")
val jsonMap = reader.readJsonValue() as Map<String, Any?>
val type = cardTypeAdapter.fromJsonValue(jsonMap["type"])
createCardWithType(type, jsonMap)
} catch (error: Exception) {
if (BuildConfig.DEBUG) {
Log.w("CardJsonAdapter", "Failed to parse card", error)
}
// Try not to break the app if we get unexpected data: ignore errors and return a placeholder card instead.
UnknownCard
}
override fun toJson(writer: JsonWriter, value: Card?) {
throw NotImplementedError("This adapter cannot write cards to JSON")
}
private fun createCardWithType(type: Type?, jsonMap: Map<String, Any?>) = when (type) {
null -> UnknownCard
Type.AMOUNTS_WITH_ACTION -> amountsWithActionAdapter.fromJsonValue(jsonMap)!!
Type.BACKGROUND_IMAGE_WITH_TITLE_AND_MESSAGE -> backgroundImageCardAdapter.fromJsonValue(jsonMap)!!
}
}
However, it is not anymore required. Moshi supports polymorphic JSON parsing now - https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5
Make a custom adapter
class YourAdapter {
#FromJson
fun fromJson(reader: JsonReader, itemsAdapter: JsonAdapter<ItemsResponse>): List<ItemsResponse>? {
val list = ArrayList<ItemsResponse>()
if (reader.hasNext()) {
val token = reader.peek()
if (token == JsonReader.Token.BEGIN_ARRAY) {
reader.beginArray()
while (reader.hasNext()) {
val itemResponse = itemsAdapter.fromJsonValue(reader.readJsonValue())
itemsResponse?.let {
list.add(itemResponse)
}
}
reader.endArray()
}
}
return list
}
}
I am developing an application which doing network request using retrofit2 and rxjava2. I am doing it using MVVM approach which is in my RestInterface the result of the request returned in Flowable and in my repository I convert the Flowable into livedata so I can make the activity observe it in my viewmodel. But by doing this I got confuse on how to handle if there is no network where I ussually handle this in the rxJava side but since it's in the repository I can't do much thing about it.
Here is the code for the rest :
#GET(NEWS_ARTICLE)
fun getArticlesFromSources(#Query("domains") source: String,
#Query("apiKey") apiKey: String = BuildConfig.NEWS_API_KEY):
Flowable<NewsResponse>
The code for repository
fun getArticleFromSources(source: String) : LiveData<NewsResponse>{
return LiveDataReactiveStreams.fromPublisher(newsRest.getArticlesFromSources(source)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io()))
}
and in my viewmodel :
private var mTriggerFetchData = MutableLiveData<String>()
private val article: LiveData<NewsResponse> = Transformations.switchMap(mTriggerFetchData){
newsRepository.getArticleFromSources(it)
}
fun getArticle() = article
fun loadArticle(source: String?){
mTriggerFetchData.value = source
}
and I observe it on my Activity :
getViewModel().getArticle().observe(this, Observer {newsResponse ->
Log.v("test", newsResponse?.articles?.size.toString())
articleList?.clear()
newsResponse?.articles?.let { articleList?.addAll(it) }
articleAdapter.notifyDataSetChanged()
})
getViewModel().loadArticle(sourceUrl)
As you can see, I was thinking to handle it in the activity but I still got confused about it. any help would be much appreciated. thanks!
You can try add onErrorReturn() to the chain
fun getArticleFromSources(source: String) : LiveData<NewsResponse>{
return LiveDataReactiveStreams.fromPublisher(newsRest.getArticlesFromSources(source)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.onErrorReturn((e: Throwable) -> {
return e
})
}
Or, rather than exposing just the NewsResponse through your LiveData object you can wrap the object and error into a wrapper class that can hold the error.
You can do something like this:
LiveDataReactiveStreams.fromPublisher(newsRest.getArticlesFromSources(source)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io()
.map(Result::success)
.onErrorReturn(Result::error))
Where Result class that holds either the error or result is something like this
class Result<T>(val data: T?, val error: Throwable?) {
companion object {
fun <T> success(data: T): Result<T> {
return Result(data, null)
}
fun <T> error(error: Throwable): Result<T> {
return Result(null, error)
}
}
}
You can then check if there's an error from the network