Currently, I use retrofit2 to call restful apis and get response. Because the response body can be multiple types, I wrote the code following.
//Interface
#FormUrlEncoded
#POST("payments/events/{id}")
fun postPayment(#Path("id") id: String): Call<Any>
//Api Manager
fun postPayment(id: String): Observable<Any> {
return Observable.create {
subscriber ->
val callResponse = api.postPayment(id)
val response = callResponse.execute()
if (response.isSuccessful) {
if (response.body() is MyClass1) {
// never success...
} else if (response.body() is MyClass2) {
// never success...
}
subscriber.onNext(response.body())
subscriber.onCompleted()
} else {
subscriber.onError(Throwable(response.message()))
}
}
}
So I'm not able to cast response.body() to MyClass1 or MyClass2.
response.body() as MyClass1 occurs error too.
MyClass1 and MyClass2 are normal template classes.
class MyClass1( val id: String, val data: String)
Is there any smart way to cast response body to my custom classes?
Small update for MyClass2
class MyClass2( val token: String, val url: String, val quantity: Int)
As mentioned by #Miha_x64, Retrofit doesn't know about your classes (MyClass1 and MyClass2) because your Call uses the Any type. Therefore, Retrofit is not creating an instance of MyClass1 or MyClass2, instead it is just creating an instance of the Any class.
The simplest solution would just be to combine the two classes:
data class MyClass(
val id: String?,
val data: String?,
val token: String?,
val url: String?,
val quantity: Int
)
Then you can specify the response type in your interface:
#FormUrlEncoded
#POST("payments/events/{id}")
fun postPayment(#Path("id") id: String): Call<MyClass>
In the case your response does not have an id or data element, they will just be null. Then you can check which type of response was received simply by checking which values are null:
if (response.body().id != null) {
// Handle type 1 response...
} else if (response.body().token != null) {
// Handle type 2 response...
}
A slightly more complex solution would be to write a wrapper for your two classes, and a type adapter to populate the wrapper. This would avoid the nullability of each of the fields, as well as keep your data structure separated.
This would differ based on the ConverterFactory you are using but if, for example, you are using Gson, it would look something like this:
data class ApiResponse(
val val1: MyClass1? = null,
val val2: MyClass2? = null
)
class ApiResponseAdapter : TypeAdapter<ApiResponse> {
#Throws(IOException::class)
override fun write(out: JsonWriter, value: ApiResponse?) {
if (value != null) {
out.beginObject()
value.val1?.id? let { out.name("id").value(it) }
value.val1?.data? let { out.name("data").value(it) }
value.val2?.token? let { out.name("token").value(it) }
value.val2?.url? let { out.name("url").value(it) }
value.val2?.quantity? let { out.name("quantity").value(it) }
out.endObject()
} else {
out.nullValue()
}
}
#Throws(IOException::class)
override fun read(in: JsonReader): ApiResponse {
reader.beginObject()
var id: String? = null
var data: String? = null
var token: String? = null
var url: String? = null
var quantity: Int = 0
while(in.hasNext()) {
val name = in.nextName()
if (name.equals("id", true)) {
id = in.nextString()
} else if (name.equals("data", true)) {
data = in.nextString()
} else if (name.equals("token", true)) {
token = in.nextString()
} else if (name.equals("url", true)) {
url = in.nextString()
} else if (name.equals("quantity", true)) {
quantity = in.nextInt()
}
}
reader.endObject()
if (id != null && data != null) {
return ApiResponse(MyClass1(id, data), null)
} else if (token != null && url != null) {
return ApiResponse(null, MyClass2(token, url, quantity))
} else {
return ApiResponse()
}
}
}
Then you can add this type adapter to your Gson instance:
val gson = GsonBuilder().registerTypeAdapter(ApiResponse::class.java, ApiResponseAdapter()).create()
Then replace the Call<Any> type with Call<ApiRepsone> and you can then check which response was received by checking which value is null:
if (response.body().val1 != null) {
// Handle MyClass1 response...
} else if (response.body().val2 != null) {
// Handle MyClass2 response...
}
First of all, thanks #Bryan for answer. Your answer was perfect but finally I did something tricky way.
...
if (response.isSuccessful) {
val jsonObject = JSONObject(response.body() as Map<*, *>)
val jsonString = jsonObject.toString()
if (jsonObject.has("id")) {
val myclass1Object = Gson().fromJson(jsonString, MyClass1::class.java)
...
} else {
val myclass2Object = Gson().fromJson(jsonString, MyClass2::class.java)
...
}
}
...
Related
I want to fetch data from the server using Retrofit but it shows me HTTP 500 server error I know it is due to a null value in parameters but I don't where is the null value comes from. I try my best to find the null value but can't find it. If any other reason then please tell me.
Here is my Fragment Code
#RequiresApi(Build.VERSION_CODES.M)
override fun inOnCreateView(mRootView: ViewGroup, savedInstanceState: Bundle?) {
val homeActivity = activity as HomeNavHostActivity
homeActivity.toolbar_id?.visibility = View.VISIBLE
homeActivity.toolbar_search_icon_id.visibility = View.VISIBLE
homeActivity.toolbar_add_icon_id.visibility = View.GONE
homeActivity.home_view_layout?.visibility = View.VISIBLE
homeActivity.bottom_layout?.visibility = View.VISIBLE
homeActivity.toolbar_title_tv.text = "Home"
homeActivity.toolbar_search_icon_id.setOnClickListener() {
showSearchDialog(mRootView)
}
homeActivity.cancel_text.setOnClickListener() {
homeActivity.search_layout.visibility = View.GONE
homeActivity.toolbar_title_tv.visibility = View.VISIBLE
homeActivity.search_view?.setQuery("", false)
homeActivity.search_view?.clearFocus()
}
val dialogHelper by inject<MaterialDialogHelper>()
setupProgressDialog(viewModel.showHideProgressDialog, dialogHelper)
if (isNetworkAvailable(requireContext())) {
var area:String = "20"
var zipcode:String = "WC2N5DU"
viewModel.getSkipFilterList(zipcode, area)
} else {
showAlertDialog(getString(R.string.no_internet))
}
attachViewModel()
}
Here is my ViewModel Code
var filterSkipList: MutableLiveData<SkipListResponse> = MutableLiveData()
fun getSkipFilterList(zipcode: String, area: String) {
viewModelScope.launch {
_showHideProgressDialog.value = true.wrapWithEvent()
sharedWebServices.getFilterSkip(zipcode, area).run {
onSuccess {
_showHideProgressDialog.value = false.wrapWithEvent()
if (it.code == VALID_STATUS_CODE) {
filterSkipList.value = it
}else {
showSnackbarMessage(it.message)
}
}
onFailure {
_showHideProgressDialog.value = false.wrapWithEvent()
it.message?.let { it1 -> showSnackbarMessage(it1) }
}
}
}
}
Here is my data class
#Serializable
data class SkipFilterList(
val zipcode:String,
val area:String
)
Here is my Post
#POST("search-skip")
suspend fun skipListing(
#Header("Authorization") token: String?,
#Body body: SkipFilterList): SkipListResponse
Here is My Repostry
suspend fun getFilterSkip(
zipcode: String,
area: String
) = withContext(dispatcher) {
val token = SharePrefrenceHelper.getInstance(app).getToken()
val body = SkipFilterList(zipcode, area)
safeApiCall {
Result.success(apiServices.skipListing("Bearer" + token, body))
}
}
By passing json object in the body this was solved.
val jsonObject = JsonObject()
jsonObject.addProperty("zipcode", zipcode)
jsonObject.addProperty("radius", area)
i am working on an online shopping application using retrofit, coroutine, livedata, mvvm,...
i want to show progress bar before fetching data from server for afew seconds
if i have one api request i can show that but in this app i have multiple request
what should i do in this situation how i should show progress bar??
Api Service
#GET("homeslider.php")
suspend fun getSliderImages(): Response<List<Model.Slider>>
#GET("amazingoffer.php")
suspend fun getAmazingProduct(): Response<List<Model.AmazingProduct>>
#GET("handsImages.php")
suspend fun getHandsFreeData(
#Query(
"handsfree_id"
) handsfree_id: Int
): Response<List<Model.HandsFreeImages>>
#GET("handsfreemoreinfo.php")
suspend fun gethandsfreemoreinfo(): Response<List<Model.HandsFreeMore>>
#GET("wristmetadata.php")
suspend fun getWristWatchMetaData(
#Query(
"wrist_id"
) wrist_id: Int
): Response<List<Model.WristWatch>>
repository
fun getSliderImages(): LiveData<List<Model.Slider>> {
val data = MutableLiveData<List<Model.Slider>>()
val job = Job()
applicationScope.launch(IO + job) {
val response = api.getSliderImages()
withContext(Main + SupervisorJob(job)) {
data.value = response.body()
}
job.complete()
job.cancel()
}
return data
}
fun getAmazingOffer(): LiveData<List<Model.AmazingProduct>> {
val data = MutableLiveData<List<Model.AmazingProduct>>()
val job = Job()
applicationScope.launch(IO + job) {
val response = api.getAmazingProduct()
withContext(Main + SupervisorJob(job)) {
data.value = response.body()
}
job.complete()
job.cancel()
}
return data
}
fun getHandsFreeData(handsree_id: Int): LiveData<List<Model.HandsFreeImages>> {
val dfData = MutableLiveData<List<Model.HandsFreeImages>>()
val job = Job()
applicationScope.launch(IO + job) {
val response = api.getHandsFreeData(handsree_id)
withContext(Main + SupervisorJob(job)) {
dfData.value = response.body()
}
job.complete()
job.cancel()
}
return dfData
}
fun getHandsFreeMore(): LiveData<List<Model.HandsFreeMore>> {
val data = MutableLiveData<List<Model.HandsFreeMore>>()
val job = Job()
applicationScope.launch(IO + job) {
val response = api.gethandsfreemoreinfo()
withContext(Main + SupervisorJob(job)) {
data.value = response.body()
}
job.complete()
job.cancel()
}
return data
}
VIEWMODEL
fun getSliderImages() = repository.getSliderImages()
fun getAmazingOffer() = repository.getAmazingOffer()
fun recieveAdvertise() = repository.recieveAdvertise()
fun dailyShoes(context: Context) = repository.getDailyShoes(context)
i will appreciate your help
I couldn't help but notice that your repository contains lots of repetitive code. first point to learn here is that all that logic in Repository, it usually goes in the ViewModel. second thing is that you are using applicationScope to launch your coroutines, which usually is done using viewModelScope(takes care of cancellation) object which is available in every viewModel.
So first we have to take care of that repetitive code and move it to ViewModel. So your viewModel would now look like
class YourViewModel: ViewModel() {
// Your other init code, repo creation etc
// Live data objects for progressBar and error, we will observe these in Fragment/Activity
val showProgress: MutableLiveData<Boolean> = MutableLiveData()
val errorMessage: MutableLiveData<String> = MutableLiveData()
/**
* A Generic api caller, which updates the given live data object with the api result
* and internally takes care of progress bar visibility. */
private fun <T> callApiAndPost(liveData: MutableLiveData<T>,
apiCall: () -> Response<T> ) = viewModelScope.launch {
try{
showProgress.postValue(true) // Show prgress bar when api call is active
if(result.code() == 200) { liveData.postValue(result.body()) }
else{ errorMessage.postValue("Network call failed, try again") }
showProgress.postValue(false)
}
catch (e: Exception){
errorMessage.postValue("Network call failed, try again")
showProgress.postValue(false)
}
}
/******** Now all your API call methods should be called as *************/
// First declare the live data object which will contain the api result
val sliderData: MutableLiveData<List<Model.Slider>> = MutableLiveData()
// Now call the API as
fun getSliderImages() = callApiAndPost(sliderData) {
repository.getSliderImages()
}
}
After that remove all the logic from Repository and make it simply call the network methods as
suspend fun getSliderImages() = api.getSliderImages() // simply delegate to network layer
And finally to display the progress bar, simply observe the showProgress LiveData object in your Activity/Fragment as
viewModel.showProgress.observer(this, Observer{
progressBar.visibility = if(it) View.VISIBLE else View.GONE
}
First create a enum class status:
enum class Status {
SUCCESS,
ERROR,
LOADING
}
Then create resource class like this:
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
Now add your request to a list of response:
var list = java.util.ArrayList<Response<*>>()
suspend fun getApis() = list.addAll(
listOf(
api.advertise(),
api.getAmazingProduct(),
api.dailyShoes(),
api.getSliderImages(),
.
.
.
)
)
In your viewmodel class:
private val _apis = MutableLiveData<Resource<*>>()
val apis: LiveData<Resource<*>>
get() = _apis
init {
getAllApi()
}
fun getAllApi() {
val job = Job()
viewModelScope.launch(IO + job) {
_apis.postValue(
Resource.loading(null)
)
delay(2000)
repository.getApis().let {
withContext(Main + SupervisorJob(job)) {
it.let {
if (it) {
_apis.postValue(Resource.success(it))
} else {
_apis.postValue(Resource.error("Unknown error eccured", null))
}
}
}
}
job.complete()
job.cancel()
}
}
Now you can use status to show progress like this . use this part in your target fragment:
private fun setProgress() {
viewModel.apis.observe(viewLifecycleOwner) {
when (it.status) {
Status.SUCCESS -> {
binding.apply {
progress.visibility = View.INVISIBLE
line1.visibility = View.VISIBLE
parentscroll.visibility = View.VISIBLE
}
}
Status.ERROR -> {
binding.apply {
progress.visibility = View.INVISIBLE
line1.visibility = View.INVISIBLE
parentscroll.visibility = View.INVISIBLE
}
}
Status.LOADING -> {
binding.apply {
progress.visibility = View.VISIBLE
line1.visibility = View.INVISIBLE
parentscroll.visibility = View.INVISIBLE
}
}
}
}
}
I hope you find it useful.
I have successfully created few routes overriding configure() method of RouteBuilder. A for loop is used to generate routes on application startup, for eg:
Route1: from("direct:Route1").to("netty-http:http://localhost:8080/route1)
Route2: from("direct:Route2").to("netty-http:http://localhost:8081/route2)
Route3: from("direct:Route3").to("netty-http:http://localhost:8082/route3)
Route4: from("direct:Route4").to("netty-http:http://localhost:8083/route4)
Route5: from("direct:Route5").to("netty-http:http://localhost:8084/route5)
for (endpoint in endpoints.iterator()) {
from("direct:" + endpoint.getEndpointRouteName())
.process(getProcessor(endpoint.getEndpointProcessor(), endpoint.getEndpointRouteName(), objectMapper))
.setHeader(Exchange.HTTP_METHOD, simple(endpoint.getEndpointRequestMethod()))
.setHeader(Exchange.CONTENT_TYPE, constant(endpoint.getEndpointContentType()))
.to("netty-http:" + endpoint.getEndpointUrl())
}
private fun getProcessor(processorClassName: String, name: String, objectMapper: ObjectMapper): Processor {
var processorClass = Class.forName("com.demo.camelpoc.processors.$name.$processorClassName")
return processorClass.getDeclaredConstructor(ObjectMapper::class.java).newInstance(objectMapper) as Processor
}
And there is a source endpoint which starts the workflow. For example the default workflow generated in runtime:
// Workflow
from("netty-http:$sourceUrl").process {
it.setProperty("Workflow", workflowMap)
}
.dynamicRouter(bean(DynamicRouteBean::class.java, "route(*, *, ${startEndpoint})"))
where workflowMap (Used in DynamicRouteBean) is a map of endpoint strings like Key: "direct:Route1 " Value : "direct:Route2", Key: "direct:Route2 " Value : "direct:Route3"... etc
Requirement: Retry sending to the same endpoint in the workflow when exception is thrown in that particular route
For eg:
Lets say, an exception occurs at direct:Route2, I want to retry sending to direct:Route2.
Here is my DynamicRouteBean class.
class DynamicRouteBean {
fun route(
#Header(Exchange.SLIP_ENDPOINT) previousRoute: String?,
exchange: Exchange,
startEndpoint: String
): String? {
if(checkException(exchange)) {
return exchange.getProperty(Exchange.SLIP_ENDPOINT) as String
}
if (exchange.getProperty(Properties.STOP_ROUTE) != null && exchange.getProperty(Properties.STOP_ROUTE) == true) {
return null
}
val workflow: MutableMap<String, Pair<String, String?>> =
exchange.getProperty("Workflow") as MutableMap<String, Pair<String, String?>>
return when (previousRoute) {
null ->
startEndpoint
else -> {
val message = exchange.getIn(NettyHttpMessage::class.java)
// Signifies last endpoint and thus means end of the route
if (!workflow.containsKey(previousRoute)) {
return null
}
if (message?.getHeader(previousRoute.substring(9)) != null) {
val isSuccess = message.getHeader(previousRoute.substring(9)) == true
if (isSuccess) {
"${workflow[previousRoute]?.first}"
} else if (workflow[previousRoute]?.second != null) {
"${workflow[previousRoute]?.second}"
} else {
null
}
} else {
null
}
}
}
}
When I return current Exchange.SLIP_ENDPOINT property as String on exception, it doesn't call that endpoint again but exception message is returned back to the consumer.
Whereas it works in normal cases when there is no exception.
Suggestions would be very helpful on handling this scenario.
For example, server response#1 has name Object:
{
"id":"vfa45f42",
"name": {
"firstName":"UserFirstName",
"lastName":"UserLastName"
}
}
But sometimes server response#2 has name String for other objects of user(that's because server has MongoDB, and at v1 it was String, but at v2 it is Object):
{
"id":"abfaf453",
"name":"OneSentenceUserName"
}
So, if I make with response#2 this:
val type = object : TypeToken<User>() {}.type
gson.fromJson(responseString, type)
where
data class User(val id:String, val name: Name)
data class Name(val firstName: String, val lastName: String)
Error is:
java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line ...
I expected to make name = null if it's String at json
Keeping your actual classes User and Name, you can create a custom TypeAdapter and create a full Name out of the single name string or the complete JSON object by reading it yourself.
class NameAdapter : TypeAdapter<Name>() {
#Throws(IOException::class)
override fun read(reader: JsonReader): Name? {
return when(reader.peek()) {
// if { "name": null }
JsonToken.NULL -> {
reader.nextNull()
null
}
// if { "name": "SomeString" }
JsonToken.STRING -> {
Name(reader.nextString(), "")
}
//if { "name": { "firstName": "some", "lastName": "thing" }
JsonToken.BEGIN_OBJECT -> {
var firstName = ""
var lastName = ""
reader.beginObject()
while (reader.hasNext()) {
val peek = reader.peek()
if(peek == JsonToken.END_OBJECT) break
else if(peek == JsonToken.NAME) {
when(reader.nextName()) {
// it will produce an exception if it isn't a string
"firstName" -> firstName = reader.nextString()
"lastName" -> lastName = reader.nextString()
}
}
}
reader.endObject()
Name(firstName, lastName)
}
else -> throw IOException("Unable to parse a name")
}
}
#Throws(IOException::class)
override fun write(writer: JsonWriter, value: Name?) {
if(value == null) {
writer.nullValue()
return
}
writer.beginObject()
writer.name("firstName")
writer.value(value.firstName)
writer.name("lastName")
writer.value(value.lastName)
writer.endObject()
}
}
Then you can add this type adapter to your gson builder.
val gson = GsonBuilder().registerTypeAdapter(Name::class.java, NameAdapter()).build()
It will deserialize correctly the name in each case and produce a full Name class.
Don't know if it will help you directly, but for the same case I did the following with Kotlinx.Serializartion library:
Make name a JsonElement type
val name: JsonElement
Then create custom deserialization function like:
fun getName(): Name? =
if (name is JsonObject) {
// deserialize Name normally
} else {
// create Name by hand, only with 'name' property...
}
Hope this helps.
Found solution, just need to use one of these:
1.
First solution:
data class User(val id:String, #JsonAdapter(FailSafeNameTypeAdapter::class) val name: Name)
where
class FailSafeNameTypeAdapter internal constructor(private val delegate: TypeAdapter<Name>) : TypeAdapter<Name>() {
#Throws(IOException::class)
override fun write(writer: JsonWriter, value: Name?) {
if (value == null) {
writer.nullValue()
return
}
// This catches non-proxied collections AND initialized proxied collections
delegate.write(writer, value)
}
#Throws(IOException::class)
override fun read(reader: JsonReader): Name? {
val peek = reader.peek()
if (peek == STRING) {
reader.skipValue()
return null
}
return delegate.read(reader) as Name
}
}
Second solution
data class User(val id:String, #JsonAdapter(FailSafePriceTypeAdapterFactory::class) val name: Name)
where
class FailSafePriceTypeAdapterFactory : TypeAdapterFactory {
override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (Price::class.java.isAssignableFrom(type.rawType)) {
val delegate = gson.getDelegateAdapter<T>(this, type) as TypeAdapter<Price>
return FailSafePriceTypeAdapter(delegate) as TypeAdapter<T>
}
return null
}
companion object {
val FACTORY: TypeAdapterFactory = FailSafePriceTypeAdapterFactory()
}
}
3.
Third solution
val gson = GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.apply {
registerTypeAdapterFactory(FailSafePriceTypeAdapterFactory.FACTORY)
}
.create()
I am requesting some data from Facebook over JAVA in android and sending it to the server:
Address[addressLines=[0:"Königspl., 86150 Augsburg, Germany"],feature=Königsplatz,admin=Bayern,sub-admin=Schwaben,locality=Augsburg,thoroughfare=Königsplatz,postalCode=86150,countryCode=DE,countryName=Germany,hasLatitude=true,latitude=48.366384499999995,hasLongitude=true,longitude=10.8943626,phone=null,url=null,extras=null]
I don't know what exactly this is, JAVA Object or I don't know..
I already tried: $array = json_decode($data, true); and it returns NULL
What is it and how do I convert it to PHP Array?
EDIT:
This is the JAVA (actually kotlin) code I use to generate the data:
val geocoder = Geocoder(this, Locale.ENGLISH)
try {
val addresses = geocoder.getFromLocation(48.366512, 10.894446, 1)
if (addresses != null)
{
val returnedAddress = addresses[0]
val strReturnedAddress = StringBuilder("Address:\n")
for (i in 0 until returnedAddress.maxAddressLineIndex) {
strReturnedAddress.append(returnedAddress.getAddressLine(i)).append("\n")
}
geocoderStuff = returnedAddress.toString()
} else
{
// NO ADDRESS
}
} catch (e: IOException) {
e.printStackTrace()
}
And this how I send it:
val params = RequestParams()
params.put("geocoder", geocoderStuff)
letsDoSomeNetworking(params)
private fun letsDoSomeNetworking(params: RequestParams) {
// AsyncHttpClient belongs to the loopj dependency.
val client = AsyncHttpClient()
client.get("http://www.bla.com/android/fb_access.php", params, object : JsonHttpResponseHandler()
{
override fun onSuccess(statusCode: Int, headers: Array<Header>?, response: JSONObject?)
{
// success
}
override fun onFailure(statusCode: Int, headers: Array<Header>?, e: Throwable, response: JSONObject?)
{
// error
}
})
}
The solution is to use gson library on JAVA/Kotlin side after getting the data:
val returnedAddress = addresses[0]
val strReturnedAddress = StringBuilder("Address:\n")
for (i in 0 until returnedAddress.maxAddressLineIndex) {
strReturnedAddress.append(returnedAddress.getAddressLine(i)).append("\n")
}
val gson = Gson() // HERE
val json = gson.toJson(returnedAddress) // HERE
geocoderStuff = json