Retrofit & Moshi: Get request with sealed class & generics - Is it possible? - android

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.

Related

How to implement delete without returning unit/void using retrofit?

I've got the root of the problem of this implmenetation:
How to delete with Body using retrofit with usecase?
In brief:
With this usecase method like underneath I return Unit.
My logs didn't show it directly, but in debugger I have seen that the problem is because of
Service methods cannot return void.
for method NotificationApi.deleteToken
if (e.isSameExceptionAs(fromDownstream) || e.isCancellationCause(coroutineContext)) {
How to change this method deleteToken to work with delete and not return void/unit?
How to provide it with usecase properly?
Thanks.
This is my UseCase
class DeleteTokenSecondUseCase #Inject constructor(private val api: NotificationApi) :
BaseApiRequestUseCase<DeleteTokenSecondUseCase.Params, Unit>() {
override fun create(params: Params): Flow<Unit> = flow {
api.deleteToken(NotificationApi.TokenChangedBody(params.token))
}
data class Params(val token: String)
}
this is my delete
#HTTP(method = "DELETE", path = "account/firebase", hasBody = true)
fun deleteToken(#Body body: TokenChangedBody)
data class TokenChangedBody(val token: String)
fun method in vm
fun notifyNotificationTokenChanged(token: String) {
val params = DeleteTokenSecondUseCase.Params(token)
deleteTokenSecondUseCase.buildWithState(params)
.withSuccess { Log.d("build", "WORKS $params") }
.withError { Log.d("build", "NOT WORKS $params") }
.launchIn(viewModelScope)
}
EDIT:
I have implmeneted it with Call event but still it goes onFailure, guys why?
#HTTP(method = "DELETE", path = "account/firebase", hasBody = true)
fun deleteToken(#Body body: TokenChangedBody) : Call<ResponseBody>
fun notifyNotificationTokenChanged(token: String) {
val params = NotificationApi.TokenChangedBody(token)
val deleteRequest: Call<ResponseBody> = notificationApi.deleteToken(params)
deleteRequest.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(call: Call<ResponseBody?>?, response: Response<ResponseBody?>?) {
Log.d("apitoken", "WORKS")
}
override fun onFailure(call: Call<ResponseBody?>?, t: Throwable?) {
Log.d("apitoken", "DOESNT WORK")
}
})
}
EDIT2:
WORKS!
SOLUTION
fun notifyNotificationTokenChanged(token: String) {
val params = NotificationApi.TokenChangedBody(token)
val deleteRequest: Call<Void> = notificationApi.deleteToken(params)
deleteRequest.enqueue(object : Callback<Void?> {
override fun onResponse(call: Call<Void?>?, response: Response<Void?>?) {
}
override fun onFailure(call: Call<Void?>?, t: Throwable?) {
}
})
}
#HTTP(method = "DELETE", path = "account/firebase", hasBody = true)
fun deleteToken(#Body body: TokenChangedBody) : Call<Void>
Someone told in Kotlin use :Completable, but haven't test it.
stackoverflow.com/questions/35429481/retrofit-2-void-return

Android Kotlin Retrofit MVVM return Null

I'm currently making a sample project about diagrams. I'm starting to use MVVM architecture recently, and I got stuck when the response is null. I also checked the Mutable Live Data to make sure that it is calling the API. Here's some of my code and the error-tag:
Model.kt
data class Model(
#SerializedName("FID") val FID: Int,
#SerializedName("region") val region: String,
#SerializedName("positive") val positive: Float
) {
}
ModelWrap.kt
data class ModelWrap(#SerializedName("samplesAPI") val attributes: Model){
}
ApiClient.kt
object ApiClient {
var retrofitService: ApiInterface? = null
const val BASE_URL = "https://sampleapi.../"
fun getApiSample() : ApiInterface {
if (retrofitService == null){
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofitService = retrofit.create(ApiInterface::class.java)
}
return retrofitService!!
}
}
ApiInterface.kt
interface ApiInterface {
#GET("samples")
fun getSampleData(): Call<List<ModelWrap>>
}
MainViewModel.kt
class MainViewModelconstructor(private val repository: ModelRepository) : ViewModel(){
val sampleList= MutableLiveData<List<ModelWrap>>()
val errorMessage = MutableLiveData<String>()
fun getSampleData(pieChart: PieChart){
val response = repository.getSampleData()
response.enqueue(object : Callback<List<ModelWrap>> {
override fun onResponse(
call: Call<List<ModelWrap>>,
response: Response<List<ModelWrap>>
) {
sampleList.postValue(response.body())
}
override fun onFailure(call: Call<List<ModelWrap>>, t: Throwable) {
errorMessage.postValue(t.message)
}
})
}
}
MainViewModelFactory.kt
class MainViewModelFactoryconstructor(private val repository: MainRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MainViewModel::class.java)){
MainViewModel(this.repository) as T
} else {
throw IllegalArgumentException("Sample ViewModel Not Found")
}
}
}
MainRepository.kt
class MainRepository constructor(private val retrofitService: ApiInterface){
fun getSampleData() = retrofitService.getSampleData()
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var pieChart: PieChart
lateinit var sampleViewModel: MainViewModel
private val sampleService = ApiClient.getApiSample()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
pieChart = findViewById(R.id.PieChart)
sampleViewModel= ViewModelProvider(this, MainViewModelFactory(MainRepository(sampleService))).get(MainViewModel::class.java)
getPieChart(pieChart)
}
private fun getPieChart(pieCharts: PieChart) {
mainViewModel.mainList.observe(this, Observer {
Log.d("TAG sample" , "onCreate PieChart: $it")
Log.d("Tag Samples Response" , response.body().toString())
if (it != null) {
val sampleEntries: List<PieEntry> = ArrayList()
for ((attributes) in it!!) {
sampleEntries.toMutableList()
.add(PieEntry(attributes.positive, attributes.region))
//........................................................................
val description = Description()
description.text = "Samples Data"
pieChart.description = description
pieChart.invalidate()
}
}
})
mainViewModel.errorMessage.observe(this, Observer { })
mainViewModel.getSampleData(pieCharts)
}
}
and Lastly, here's some or log message:
V/InputMethodManager: Starting input: tba=android.view.inputmethod.EditorInfo#8b795c0 nm : com.example.diargram ic=null
D/Tag Sample Response: null
D/TAG Sample: onCreate PieChart: null
E/libc: Access denied finding property "ro.serialno"
V/StudioTransport: Agent command stream started.
V/StudioTransport: Transport agent connected to daemon.
I would appreciate it if someone can help me :D, Thank you
Finally, I found a solution for my problem:
I type the wrong endpoint inside the interface class and it should be like this:
interface ApiInterface {
#GET("sample")
fun getSampleData(): Call<List> }
When it comes to assigning the livedata to the view, based on my JSON I should call ArrayList instead of List
List item
Before :
val sampleEntries: List = ArrayList()
After :
val sampleEntries: ArrayList<PieEntry> = ArrayList()

Retrofit CallAdapter for suspending functions which returns for each List<Object> a List<LinkedTreeMap>. How to solve it?

I was inspired by the writing of this adapter to Valery Katkov's answer answer
My Retrofit call adapter is able to transform the JSON of normal objects correctly, but when I expect from a call a List<Object>, Retrofit returns me a List<LinkedTreeMap>. It cannot parse Object within the list
Exception
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.example.networkcalladapter.Post
CallAdapter Factory And CallAdapter
class NetworkCallAdapterFactory : 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)) {
ResponseNetwork::class.java -> {
require(callType is ParameterizedType){ "resource must be paramterized" }
val resultType = getParameterUpperBound(0, callType)
ResponseNetworkAdapter<Any>(getRawType(resultType))
}
else -> null
}
}
else -> null
}
}
class ResponseNetworkAdapter<T: Any>(
private val type: Type
) : CallAdapter<T, Call<ResponseNetwork<T>>> {
override fun responseType() = type
override fun adapt(call: Call<T>): Call<ResponseNetwork<T>> = ResponseNetworkCall(call)
}
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 ResponseNetworkCall<T: Any>(proxy: Call<T>) : CallDelegate<T, ResponseNetwork<T>>(proxy) {
override fun enqueueImpl(callback: Callback<ResponseNetwork<T>>) {
proxy.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
callback.onResponse(this#ResponseNetworkCall, Response.success(ResponseNetwork.create(response)))
}
override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(this#ResponseNetworkCall, Response.success(ResponseNetwork.create(Exception(t))))
}
})
}
override fun cloneImpl() = ResponseNetworkCall(proxy.clone())
}
ResponseNetwork
sealed class ResponseNetwork<T> {
companion object {
fun <T> create(error: Exception): ResponseNetworkError<T> {
return ResponseNetworkError(error)
}
fun <T> create(response: Response<T>): ResponseNetwork<T> {
return if (response.isSuccessful) {
response.body()?.let {
ResponseNetworkSuccess(response.code(), response.headers(), it)
} ?: ResponseNetworkEmpty(
response.code(),
response.errorBody()?.string() ?: "unknown error"
)
} else {
val msg = response.errorBody()?.string()
ResponseNetworkError(Exception(msg))
}
}
}
}
data class ResponseNetworkSuccess<T>(
val code: Int,
val header: Headers,
val body: T
) : ResponseNetwork<T>()
data class ResponseNetworkEmpty<T>(
val code: Int,
val message: String
) : ResponseNetwork<T>()
data class ResponseNetworkError<T>(
val exception: Exception
) : ResponseNetwork<T>()
Remote Api
#GET("posts")
suspend fun getPost(): ResponseNetwork<List<Post>>
Retrofit
Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(NetworkCallAdapterFactory())
.build()
.create(RemoteApi::class.java)
Post Model
data class Post(val userId: Int,
val id: Int,
val title: String,
val body: String)
Someone understands why retrofit always comes back to me List<LinkedTreeMap> whenever I need a list from the network ?
can you replace your remote API with this and check it.
#GET("posts")
suspend fun getPost(): Deferred<Response<ResponseNetwork<List<Post>>>
i fixed my bug in NetworkCallAdapterFactory
ResponseNetworkAdapter<Any>((resultType))

How to test api requests and fill classes with fake data?

I can not find a solution to this problem on the Internet. Or is my code so bad?
Interface
interface GetWeatherService {
#GET("/data/2.5/forecast?")
fun getAllWeather(#Query("q")cityName: String, #Query("APPID")app_id: String, #Query("units")units: String="imperial"): Call<ListWeather>
#GET("/data/2.5/weather?")
fun getCurrentWeather(#Query("q")cityName: String, #Query("APPID")app_id: String, #Query("units")units: String="imperial"): Call<MainWeather>
#GET("/data/2.5/weather?")
fun getWeatherDataFromLocation(#Query("lat")lat: String, #Query("lon")lon: String, #Query("APPID") app_id: String): Call<DataFromLocation>
}
Client
object RetrofitClientInstance {
private var retrofit: Retrofit? = null
private var BASE_URL = "https://api.openweathermap.org/"
val retrofitInstance: Retrofit?
get(){
if(retrofit == null){
retrofit = retrofit2.Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return retrofit
}
}
MainActivity()
class MainActivity: AppCompatActivity {
fun getDataFromApi(cityName: String) {
val service = RetrofitClientInstance.retrofitInstance!!.create(GetWeatherService::class.java)
// Service 1
service.getCurrentWeather(cityName, APP_ID).enqueue(object: Callback<MainWeather>{
override fun onFailure(call: Call<MainWeather>, t: Throwable) {
}
override fun onResponse(call: Call<MainWeather>, response: Response<MainWeather>) {
val weather = response.body()
val currentWeather = CurrentWeather(
weather!!.main.temp,
weather.weather[0].description,
weather.name,
weather.weather[0].main
)
updateCurrentWeatherUI(currentWeather)
}
})
service.getAllWeather(cityName, APP_ID).enqueue(object: Callback<ListWeather>{
override fun onFailure(call: Call<ListWeather>, t: Throwable) {
}
override fun onResponse(call: Call<ListWeather>, response: Response<ListWeather>) {
val weather = response.body()!!.list
for(item in weather){
val weatherList = NextFiveDayWeather(
item.dt,
item.main.temp,
item.weather[0].description
)
weatherArray.add(weatherList)
updateUI(weatherArray)
}
}
})
}
}
Data class
data class MainWeather (
var dt: Long,
var main: MainDTO,
var weather: List<WeatherDTO>,
var dt_txt: String,
var name: String
)
data class WeatherDTO (var main: String, var description: String, var icon: String)
data class ListWeather (#SerializedName("list") var list: List<MainWeather>)
I can’t test the request to api and I can’t fill the classes with fake data?
Tell me please, what should I do?
What should I study? And if you are not difficult can give advice for the future?
You can download the software Postman to test api endpoints: https://www.getpostman.com/

Send a parameter to Kotlin retrofit call

I am a beginner in Kotlin. I need to send a variable parameter from my Activity to a Retrofit call.
This is my call in on Create of Detail Activity
override fun onCreate(savedInstanceState: Bundle?) {
//...
val id = intent.getStringExtra("id")
// Get the ViewMode
val mModel = ViewModelProviders.of(this).get(myObjectViewModel::class.java)
//Create the observer which updates the UI.
val myObjectByIdObserver = Observer<MyObject> { myObject->
//...
}
//Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
mModel.getObjectById.observe(this, myObjectByIdObserver)
}
Here I insert value hardcode, I need the parameter received from the previous Activity.
class MyObjectViewModel : ViewModel() {
//this is the data that we will fetch asynchronously
var myObject: MutableLiveData<MyObject>? = null
val getMyObjectById: LiveData<MyObject>
get() {
if (myObject == null) {
myObject = MutableLiveData()
loadMyObjectById()
}
return myObject as MutableLiveData<MyObject>
}
private fun loadMyObjectById() {
val retrofit = Retrofit.Builder()
.baseUrl(Api.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(Api::class.java)
val call = api.myObjectById(100)
call.enqueue(object : Callback<MyObject> {
override fun onResponse(call: Call<MyObject>, response: Response<MyObject>) {
myObject!!.value = response.body()
}
override fun onFailure(call: Call<MyObject>, t: Throwable) {
var tt = t
}
})
}
My API:
interface Api {
companion object {
const val BASE_URL = "https://.../"
}
#GET("myObjects/{id}")
fun myObjectById(#Path("id") id: Int?): Call<MyObject>
}
You can do this by ``#Query``` annotation.
interface Api {
companion object {
const val BASE_URL = "https://.../"
}
#GET("myObjects/{id}")
fun myObjectById(#Path("id") id: Int?, #Query("a_param") aParam: String?): Call<MyObject>
}
Edited. I completely misread your intension.
What you need seems to be ViewModelProvider.NewInstanceFactory like
class MyObjectViewModel(val id: Int): ViewModel() {
class Factory(val id: Int) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MyObjectViewModel(id) as T
}
}
}
then
val myViewModel = ViewModelProviders
.of(this, MyObjectViewModel.Factory(id))
.get(MyObjectViewModel::class.java)

Categories

Resources