Handle dynamic response sometimes object / array on same key on android kotlin - android

I have a response from this api, and there is different response on
...
"value": [
{
"#unit": "C",
"#text": "28"
}
]
sometimes
"value":
{
"#unit": "C",
"#text": "28"
}
I have try create this json adapter & model class from the answer
object WeatherResponse {
open class CuacaResponse{
#SerializedName("Success")
val success : Boolean = false
val row : RowBean? = null
}
data class RowBean(
val data : DataBean? = null
)
data class DataBean (
val forecast : ForecastBean? = null
)
data class ForecastBean(
val area : List<Area>? = null
)
data class Area(
#SerializedName("#id")
val id :String?="",
#SerializedName("#description")
val nama :String?="",
val parameter : List<DataMain>?=null
)
data class DataMain(
#SerializedName("#description")
val namaData :String?="",
#SerializedName("#id")
val id :String?="",
#SerializedName("timerange")
val timeRange : List<TimeRangeItem>
)
data class TimeRangeItem(
// sample data : 202107241800 => 2021-07-24-18:00
#SerializedName("#datetime")
val datetime : String,
#JsonAdapter(ValueClassTypeAdapter::class)
val value : ArrayList<ValueData>? = null,
)
data class ValueData(
#SerializedName("#unit")
val unit :String?="",
#SerializedName("#text")
val value :String?="",
)
class ValueClassTypeAdapter :
JsonDeserializer<ArrayList<ValueData?>?> {
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
ctx: JsonDeserializationContext
): ArrayList<ValueData?> {
return getJSONArray(json, ValueData::class.java, ctx)
}
private fun <T> getJSONArray(json: JsonElement, type: Type, ctx:
JsonDeserializationContext): ArrayList<T> {
val list = ArrayList<T>()
if (json.isJsonArray) {
for (e in json.asJsonArray) {
list.add(ctx.deserialize<Any>(e, type) as T)
}
} else if (json.isJsonObject) {
list.add(ctx.deserialize<Any>(json, type) as T)
} else {
throw RuntimeException("Unexpected JSON type: " + json.javaClass)
}
return list
}
}
}
my retrofit service :
interface WeatherService {
#GET("/api/cuaca/DigitalForecast-{province}.xml?format=json")
suspend fun getWeather(
#Path("province") provinceName: String? = "JawaTengah"
) : WeatherResponse.CuacaResponse?
companion object {
private const val URL = "https://cuaca.umkt.ac.id"
fun client(context: Context): WeatherService {
val httpClient = OkHttpClient.Builder()
httpClient.apply {
addNetworkInterceptor(
ChuckerInterceptor(
context = context,
alwaysReadResponseBody = true
)
)
addInterceptor { chain ->
val req = chain.request()
.newBuilder()
.build()
return#addInterceptor chain.proceed(req)
}
cache(null)
}
val gsonConverterFactory = GsonConverterFactory.create()
return Retrofit.Builder()
.baseUrl(URL)
.client(httpClient.build())
.addConverterFactory(gsonConverterFactory)
.build()
.create(WeatherService::class.java)
}
}
}
But the result that i got, from log request :
...
{
"#datetime": "202108070000",
"value": {
"size": 4
}
},
{
"#datetime": "202108070600",
"value": {
"size": 4
}
},
{
"#datetime": "202108071200",
"value": {
"size": 4
}
}
...
the value return size that IDK from where, it should return array of unit & text from the api.
Please anyone help me from this stuck, thanks in advance!

Finaly, i have solved this by change JsonDeserializer to TypeAdapterFactory as mentioned on this answer

Related

Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $ JSON Array Kotlin

I want to retrieve a JSON Array, How can I adjust my codebase to that. I have used the retrofit library to retrieve the data and I used the MVVM architecture. I get the error Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $.
this is my endpoint class:
#GET("v2/venues/search")
fun fetchAllVenues(): Call<List<Venue>>
}
this is my Repository class:
class VenueRepository {
private var apiInterface: VenuesEndpoint? = null
init {
apiInterface = ApiClient.getApiClient().create(VenuesEndpoint::class.java)
}
fun fetchAllVenues(): MutableLiveData<List<Venue>?> {
val data = MutableLiveData<List<Venue>?>()
apiInterface?.fetchAllVenues()?.enqueue(object : Callback<List<Venue>> {
override fun onFailure(call: Call<List<Venue>>, t: Throwable) {
data.value = null
}
override fun onResponse(
call: Call<List<Venue>>,
response: Response<List<Venue>>
) {
val res = response.body()
if (response.code() == 200 && res != null) {
data.value = res
} else {
data.value = null
}
}
})
return data
}
}
and this is my model class:
data class Venue(var id:Int,var name:String)
and this is my viewmodel class:
class VenueViewModel : ViewModel() {
private var venueRepository: VenueRepository? = null
var postModelListLiveData: MutableLiveData<List<Venue>?>? = null
init {
venueRepository = VenueRepository()
postModelListLiveData = MutableLiveData()
}
fun fetchAllVenues() {
postModelListLiveData = venueRepository?.fetchAllVenues()
}
}
this is the JSON I want to retrieve:
"response": { "venues": [ { "id": "4b83cb72f964a520d71031e3" "name": "Stadhuis" "contact": { "phone": "+3114010" "formattedPhone": "+31 14010" "twitter": "rotterdam" } "location": { "address": "Coolsingel 40" "lat": 51.92258962728412 "lng": 4.480227190204032 "labeledLatLngs": [ "0": { "label": "display" "lat": 51.92258962728412 "lng": 4.480227190204032 } ] "postalCode": "3011 AD" "cc": "NL" "city": "Rotterdam" "state": "Zuid-Holland" "country": "Nederland" "formattedAddress": [ "0": "Coolsingel 40" "1": "3011 AD Rotterdam" "2": "Nederland"
The issue is, the response returns you venues and you are expecting a List<Venue>, so what should work for you is, create another data class that is like this:
data class Venues(
val venues: List<Venue>
)
and then inside the GET request return Call<Venues>
Do tell if that fixes it for you :)
UPDATE
Ok, that was a bit lengthy conversation but finally here is your detailed solution, hope this resolves everything for you!
ViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
class ViewModel : ViewModel() {
private val repository = Repository()
fun getData(longLat: String, date: String): LiveData<mainResponse?> {
repository.fetch(longLat, date)
return repository.data
}
}
Repository
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class Repository {
private val _data: MutableLiveData<mainResponse?> = MutableLiveData(null)
val data: LiveData<mainResponse?> get() = _data
fun fetch(longlat: String, date: String) {
val retrofit = Retro()
val api = retrofit.retro.create(api::class.java)
api.get(
longLat = longlat,
date = date
).enqueue(object : Callback<mainResponse>{
override fun onResponse(call: Call<mainResponse>, response: Response<mainResponse>) {
val res = response.body()
if (response.code() == 200 && res != null) {
_data.value = res
} else {
_data.value = null
}
}
override fun onFailure(call: Call<mainResponse>, t: Throwable) {
_data.value = null
}
})
}
}
MainActivity
private val viewModel by viewModels<ViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.getData(
longLat = "40.7,-74", // sample latitude and longitude
date = "20210715" // date format is: YYYYMMDD
).observe(this, Observer {
it?.let { res ->
res.response.venues.forEach { venue ->
val name = venue.name
val location = venue.location
Log.d("name ",name)
Log.d("address ", location.address)
}
}
})
}
}
Api Interface
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface api {
#GET("v2/venues/search")
fun get(
#Query("ll") longLat: String,
#Query("client_id") id: String = Const.clientId,
#Query("client_secret") secret: String = Const.clientSecret,
#Query("v") date: String
): Call<mainResponse>
}
Model Classes
mainResponse
data class mainResponse(
val response: Response
)
Response
data class Response(
val venues: List<Venue>,
val confident: Boolean
)
Location
data class Location(
val address: String,
val crossStreet: String,
val lng: Double,
val lat: Double
)
Venue
data class Venue(
val id: String,
val name: String,
val location: Location
)
Const
object Const {
const val BASE_URL = "https://api.foursquare.com"
const val clientId = "" // add yours
const val clientSecret = "" // add yours
}
Retro
class Retro {
val retro = Retrofit.Builder()
.baseUrl(Const.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
Dependencies: make sure to add activity-ktx for using ViewModel in activity
def coroutines_version = "1.4.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutines_version"
def lifecycle_version = "2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "androidx.activity:activity-ktx:1.2.3"

Retrofit2 & JSON response array with multiple possible values

I haven't written anything here for a long time, but now I really need advice )
I'm using Retrofit2 as api client. Server API has one endpoint, for example /api/stats which receive JSON body request and return JSON response as:
data class StatsResult<T>(
#SerializedName("code") val code: Int,
#SerializedName("message") val msg: String?,
#SerializedName("request_id") val requestId: String?,
#SerializedName("data") val data: T?
)
If some error, data is null.
Otherwise data is an array that can contain different type of data depending on the type of request.
For example:
Request1:
{
"type": "type1",
"params": {
}
}
Response:
{
"code": 0,
"request_id": "...",
"data": [
{
"key1": "value1",
"key2": "value2"
},
{
"key1": "value3",
"key2": "value4"
}
]
}
Request2:
{
"type": "type2",
"params": {
}
}
Response:
{
"code": 0,
"request_id": "...",
"data": [
{
"key3": "value1",
"key4": "value2"
},
{
"key3": "value3",
"key4": "value4"
}
]
}
Here is my implementation in short:
interface StatsApi {
#POST("/api/stats")
suspend fun getStats(#Body request: StatsRequest): ApiResponse<StatsData>
}
sealed class ApiResponse<out T: Any> {
data class Success<T: Any>(val body: T): ApiResponse<T>()
object Unauthorized : ApiResponse<Nothing>()
object Forbidden: ApiResponse<Nothing>()
object NetworkError: ApiResponse<Nothing>()
data class Error(val msg: String? = null): ApiResponse<Nothing>()
data class Exception(val t: Throwable): ApiResponse<Nothing>()
}
typealias StatsData = StatsResult<List<BaseStatsDto>>
open class BaseStatsDto()
class Type1StatsDto: BaseStatsDto() {
#SerializedName("key1") var key1: String? = null
#SerializedName("key2") var key2: String? = null
}
class Type2StatsDto: BaseStatsDto() {
#SerializedName("key3") var key3: String? = null
#SerializedName("key4") var key4: String? = null
}
So I tried to workaround this with open/abstract class BaseStatsDto and than cast it to final class. But this solution didn't work.
For response handling I'm using CallAdapter.Factory() with custom Call<>:
open class ApiResponseCall<T : Any>(
private val delegate: Call<T>
) : Call<ApiResponse<T>> {
override fun enqueue(callback: Callback<ApiResponse<T>>) {
return delegate.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
val rsp = when (t) {
is IOException -> ApiResponse.NetworkError
else -> ApiResponse.Exception(t)
}
callback.onResponse(this#ApiResponseCall, Response.success(rsp))
}
override fun onResponse(call: Call<T>, response: Response<T>) {
val rsp: ApiResponse<T>
rsp = if (response.isSuccessful) {
val body = response.body()
ApiResponse.Success(body as T)
} else {
val code = response.code()
val error = response.errorBody()
when (code) {
401 -> ApiResponse.Unauthorized
403 -> ApiResponse.Forbidden
in 400..499 -> ApiResponse.Error("Client error")
in 500..599 -> ApiResponse.Error("Server error")
else -> ApiResponse.Exception(Exception("Unknown error"))
}
}
callback.onResponse(this#ApiResponseCall, Response.success(rsp))
}
})
}
...
}
I see another solution - to have separate interface functions with separate response types. And it working fine:
#POST("/api/stats")
suspend fun getType1Stats(#Body request: StatsRequest): ApiResponse<StatsResult<List<Type1StatsDto>>>
#POST("/api/stats")
suspend fun getType2Stats(#Body request: StatsRequest): ApiResponse<StatsResult<List<Type2StatsDto>>>
But if statistic data types count increases it will be very uncomfortable to maintain.
I would like to have one statistic api endpoint.
Use http://www.jsonschema2pojo.org/ Set SourceType: JSON and Annonation style: Gson
Data for request 1
class Data1 {
#SerializedName("key1")
#Expose
var key1: String? = null
#SerializedName("key2")
#Expose
var key2: String? = null
}
Response of request1
class Response1 {
#SerializedName("code")
#Expose
var code: Int? = null
#SerializedName("request_id")
#Expose
var requestId: String? = null
#SerializedName("data")
#Expose
var data: List<Data1>? = null
}
Data for request 2
class Data2 {
#SerializedName("key3")
#Expose
var key3: String? = null
#SerializedName("key4")
#Expose
var key4: String? = null
}
Response of request 2
class Response1 {
#SerializedName("code")
#Expose
var code: Int? = null
#SerializedName("request_id")
#Expose
var requestId: String? = null
#SerializedName("data")
#Expose
var data: List<Data2>? = null
}
with
#POST("/api/stats")
suspend fun getStats1(#Body request: StatsRequest1): ApiResponse<Response1>
#POST("/api/stats")
suspend fun getStats2(#Body request: StatsRequest2): ApiResponse<Response2>

Unable to parse JSON using Retrofit in Android

I am successfully able to hit the API and get the json result. I can see the success result in the logs by printing Retrofit response body. and also using Stetho as the network interceptor.
However, I am not able to understand why is the api response still "null" in the onResponse() method in the repository. I believe, I am not passing the correct model maybe for the JSON to be parsed properly ? Can anybody help me to find out what's the issue here?
Following is the json:
{
"photos": {
"page": 1,
"pages": 2864,
"perpage": 100,
"total": "286373",
"photo": [
{
"id": "49570734898",
"owner": "165034061#N07",
"secret": "f3cb2c2590",
"server": "65535",
"farm": 66,
"title": "Hello",
"ispublic": 1,
"isfriend": 0,
"isfamily": 0
}
],
"photo": [
{
"id": "12344",
"owner": "23444#N07",
"secret": "f233edd",
"server": "65535",
"farm": 66,
"title": "Hey",
"ispublic": 1,
"isfriend": 0,
"isfamily": 0
}
]
},
"stat": "ok"
}
My Pojo Class :
data class Photos(
#SerializedName("page")
val page: Int,
#SerializedName("pages")
val pages: Int,
#SerializedName("perpage")
val perpage: Int,
#SerializedName("photo")
val photos: List<Photo>,
#SerializedName("total")
val total: String
)
data class Photo(
#SerializedName("farm")
val farm: Int,
#SerializedName("id")
val id: String,
#SerializedName("isfamily")
val isFamily: Int,
#SerializedName("isfriend")
val isFriend: Int,
#SerializedName("ispublic")
val isPublic: Int,
#SerializedName("owner")
val owner: String,
#SerializedName("secret")
val secret: String,
#SerializedName("server")
val server: String,
#SerializedName("title")
val title: String
)
RetrofitClient:
object ApiClient {
private val API_BASE_URL = "https://api.flickr.com/"
private var servicesApiInterface: ServicesApiInterface? = null
fun build(): ServicesApiInterface? {
val builder: Retrofit.Builder = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
val httpClient: OkHttpClient.Builder = OkHttpClient.Builder()
httpClient.addInterceptor(interceptor()).addNetworkInterceptor(StethoInterceptor())
val retrofit: Retrofit = builder
.client(httpClient.build()).build()
servicesApiInterface = retrofit.create(
ServicesApiInterface::class.java
)
return servicesApiInterface as ServicesApiInterface
}
private fun interceptor(): HttpLoggingInterceptor {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
return httpLoggingInterceptor
}
interface ServicesApiInterface {
#GET("/services/rest/?method=flickr.photos.search")
fun getImageResults(
#Query("api_key") apiKey: String,
#Query("text") text: String,
#Query("format") format: String,
#Query("nojsoncallback") noJsonCallback: Boolean
): Call<PhotoResponse>
}
}
OperationCallback:
interface OperationCallback<T> {
fun onSuccess(data:List<T>?)
fun onError(error:String?)
}
PhotoDataSource:
interface PhotoDataSource {
fun retrievePhotos(callback: OperationCallback<Photo>, searchText: String)
fun cancel()
}
PhotoRepository:
class PhotoRepository : PhotoDataSource {
private var call: Call<PhotoResponse>? = null
private val API_KEY = "eff9XXXXXXXXXXXXX"
val FORMAT = "json"
companion object {
val TAG = PhotoRepository::class.java.simpleName
}
override fun retrievePhotos(callback: OperationCallback<Photo>, searchText: String) {
call = ApiClient.build()
?.getImageResults(
apiKey = API_KEY,
text = searchText,
format = FORMAT,
noJsonCallback = true
)
call?.enqueue(object : Callback<PhotoResponse> {
override fun onFailure(call: Call<PhotoResponse>, t: Throwable) {
callback.onError(t.message)
}
override fun onResponse(
call: Call<PhotoResponse>,
response: Response<PhotoResponse>
) {
response?.body()?.let {
Log.d(TAG, "got api response total pics are :${it.data?.size}")
if (response.isSuccessful && (it.isSuccess())) {
callback.onSuccess(it.data)
} else {
callback.onError(it.msg)
}
}
}
})
}
override fun cancel() {
call?.let {
it.cancel()
}
}
}
PhotoResponse:
data class PhotoResponse(val status: Int?, val msg: String?, val data: List<Photo>?) {
fun isSuccess(): Boolean = (status == 200)
}
Try to change your PhotoResponse to match with your json response.
data class PhotoResponse(
#SerializedName("stat")
val status: String?,
#SerializedName("photos")
val photos: Photos?
) {
fun isSuccess(): Boolean = status.equals("ok", true)
}
And then inside onResponse, You can get List<Photo> like below:
override fun onResponse(
call: Call<PhotoResponse>,
response: Response<PhotoResponse>
) {
response?.body()?.let {
//This should be your list of photos
it.photos.photos
}
}
The issue is with your data class. You need one extra data class here.
So if you look at your JSON response closely, then you will understand whats going wrong.
Your photos data class should not be the first class. Instead it should be inside one more class lets say PhotoApiResponse.
Your first class will contain both photos and stat.
And then rest can be the same.

Serialize sealed class with Moshi

The following will produce an IllegalArgumentException because you "Cannot serialize abstract class"
sealed class Animal {
data class Dog(val isGoodBoy: Boolean) : Animal()
data class Cat(val remainingLives: Int) : Animal()
}
private val moshi = Moshi.Builder()
.build()
#Test
fun test() {
val animal: Animal = Animal.Dog(true)
println(moshi.adapter(Animal::class.java).toJson(animal))
}
I have tried solving this using a custom adapter, but the only solution I could figure out involves explicitly writing all of the property names for each subclass. e.g:
class AnimalAdapter {
#ToJson
fun toJson(jsonWriter: JsonWriter, animal: Animal) {
jsonWriter.beginObject()
jsonWriter.name("type")
when (animal) {
is Animal.Dog -> jsonWriter.value("dog")
is Animal.Cat -> jsonWriter.value("cat")
}
jsonWriter.name("properties").beginObject()
when (animal) {
is Animal.Dog -> jsonWriter.name("isGoodBoy").value(animal.isGoodBoy)
is Animal.Cat -> jsonWriter.name("remainingLives").value(animal.remainingLives)
}
jsonWriter.endObject().endObject()
}
....
}
Ultimately I'm looking to produce JSON that looks like this:
{
"type" : "cat",
"properties" : {
"remainingLives" : 6
}
}
{
"type" : "dog",
"properties" : {
"isGoodBoy" : true
}
}
I'm happy with having to use the custom adapter to write the name of each type, but I need a solution that will automatically serialize the properties for each type rather than having to write them all manually.
This can be done with PolymorphicJsonAdapterFactory and including an extra property in the json to specify the type.
For example:
This JSON
{
"animals": [
{
"type": "dog",
"isGoodBoy": true
},
{
"type": "cat",
"remainingLives": 9
}
]
}
Can be mapped to the following classes
sealed class Animal {
#JsonClass(generateAdapter = true)
data class Dog(val isGoodBoy: Boolean) : Animal()
#JsonClass(generateAdapter = true)
data class Cat(val remainingLives: Int) : Animal()
object Unknown : Animal()
}
With the following Moshi config
Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(Animal::class.java, "type")
.withSubtype(Animal.Dog::class.java, "dog")
.withSubtype(Animal.Cat::class.java, "cat")
.withDefaultValue(Animal.Unknown)
)
I think you need the polymorphic adapter to achieve this which requires the moshi-adapters artifact. This will enable serialization of sealed classes with different properties. More details are in this article here: https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5
I have solved this by creating a Factory, an enclosing class, and an enum that can provide the classes for each item type. However this feels rather clunky and I would love a more straight forward solution.
data class AnimalObject(val type: AnimalType, val properties: Animal)
enum class AnimalType(val derivedClass: Class<out Animal>) {
DOG(Animal.Dog::class.java),
CAT(Animal.Cat::class.java)
}
class AnimalFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<AnimalObject>? {
if (!Types.getRawType(type).isAssignableFrom(AnimalObject::class.java)) {
return null
}
return object : JsonAdapter<AnimalObject>() {
private val animalTypeAdapter = moshi.adapter<AnimalType>(AnimalType::class.java)
override fun fromJson(reader: JsonReader): AnimalObject? {
TODO()
}
override fun toJson(writer: JsonWriter, value: AnimalObject?) {
writer.beginObject()
writer.name("type")
animalTypeAdapter.toJson(writer, value!!.type)
writer.name("properties")
moshi.adapter<Animal>(value.type.derivedClass).toJson(writer, value.properties)
writer.endObject()
}
}
}
}
Answer is taken from: github.com/square/moshi/issues/813
You should be able to create your own JsonAdapter.Factory and provide custom adapter whenever an Animal need to be serialized/deserialized:
sealed class Animal {
#JsonClass(generateAdapter = true)
data class Dog(val isGoodBoy: Boolean) : Animal()
#JsonClass(generateAdapter = true)
data class Cat(val remainingLives: Int) : Animal()
}
object AnimalAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? =
when (type) {
Animal::class.java -> AnimalAdapter(moshi)
else -> null
}
private class AnimalAdapter(moshi: Moshi) : JsonAdapter<Animal>() {
private val mapAdapter: JsonAdapter<MutableMap<String, Any?>> =
moshi.adapter(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java))
private val dogAdapter = moshi.adapter(Animal.Dog::class.java)
private val catAdapter = moshi.adapter(Animal.Cat::class.java)
override fun fromJson(reader: JsonReader): Animal? {
val mapValues = mapAdapter.fromJson(reader)
val type = mapValues?.get("type") ?: throw Util.missingProperty("type", "type", reader)
val properties = mapValues["properties"] ?: throw Util.missingProperty("properties", "properties", reader)
return when (type) {
"dog" -> dogAdapter.fromJsonValue(properties)
"cat" -> catAdapter.fromJsonValue(properties)
else -> null
}
}
override fun toJson(writer: JsonWriter, value: Animal?) {
writer.beginObject()
writer.name("type")
when (value) {
is Animal.Dog -> writer.value("dog")
is Animal.Cat -> writer.value("cat")
}
writer.name("properties")
when (value) {
is Animal.Dog -> dogAdapter.toJson(writer, value)
is Animal.Cat -> catAdapter.toJson(writer, value)
}
writer.endObject()
}
}
}
private val moshi = Moshi.Builder()
.add(AnimalAdapterFactory)
.build()
#Test
fun test() {
val dog: Animal = Animal.Dog(true)
val cat: Animal = Animal.Cat(7)
println(moshi.adapter(Animal::class.java).toJson(dog))
println(moshi.adapter(Animal::class.java).toJson(cat))
val shouldBeDog: Animal? = moshi.adapter(Animal::class.java).fromJson(moshi.adapter(Animal::class.java).toJson(dog))
val shouldBeCat: Animal? = moshi.adapter(Animal::class.java).fromJson(moshi.adapter(Animal::class.java).toJson(cat))
println(shouldBeDog)
println(shouldBeCat)
}

populate spinner with retrofit and moshi

i am beginner in android dev (both java or kotlin). I am trying to populate spinner from json with retrofit and moshi but I have no idea how to populate it into spinner. to be honest I dont know whether the return of Json data is correct or not, since Log.d() return is not detail as dump() laravel or php.
script in activity onCreate (Please read the comment of the script, i put debug result of Log.d() there):
val task = object : AsyncTask<Void, Void, Response<List<ProductTypeResponse>>>() {
override fun doInBackground(vararg params: Void): Response<List<ProductTypeResponse>> {
val typeAPI = RestAPI()
val callResponse = typeAPI.getNews()
val response = callResponse.execute()
return response
}
override fun onPostExecute(response: Response<List<ProductTypeResponse>>) {
if (response.isSuccessful) {
val news = response.body()
Log.d("test:", news!![0].data.toString()) // method 'java.lang.String com.example.mockie.tigaer.api.TypeDataResponse.toString()' on a null object reference
Log.d("test:", news!!.size.toString()) // it says 67 but the data from the url is 63 array of json object
Log.d("test:", news!![0].toString()) // com.example.mockie.tigaer.api.ProductTypeResponse#f17fd5e
}
}
RestApi.kt
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
class RestAPI() {
private val tigaerApi: TigaerApi
init {
val retrofit = Retrofit.Builder()
.baseUrl("http://app.tigaer.id/laravel/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
tigaerApi = retrofit.create(TigaerApi::class.java)
}
fun getNews(): Call<List<ProductTypeResponse>> {
return tigaerApi.getTop()
}
}
ApiModel.kt
package com.example.mockie.tigaer.api
class ProductTypeResponse(val data: TypeDataResponse)
class TypeDataResponse(
val children: List<ProductTypeChildrenResponse>
)
class ProductTypeChildrenResponse(val data: ProductTypeDataResponse)
class ProductTypeDataResponse(
val productType: String,
val readable: String
)
TigaerApi.kt
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface TigaerApi {
#GET("api/type")
fun getTop(): Call<List<ProductTypeResponse>>
}
return Json: https://jsoneditoronline.org/?id=ce90c41b859218e746e41d64eddb4c30
so my questions are :
is there any function to debug object/array as detail as in laravel ?
how to populate my json return data into spinner?
Here is code for same, I modified and integrate in your code only:
"MainActivity.kt" class:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var spinner: Spinner = findViewById(R.id.spinner)
val task = object : AsyncTask<Void, Void, Response<List<ProductTypeDataResponse>>>() {
override fun doInBackground(vararg params: Void): Response<List<ProductTypeDataResponse>> {
val typeAPI = RestAPI()
val callResponse = typeAPI.getNews()
val response = callResponse.execute()
return response
}
override fun onPostExecute(response: Response<List<ProductTypeDataResponse>>) {
if (response.isSuccessful) {
val news: List<ProductTypeDataResponse>? = response.body()
var adapter: SpinnerAdapter = SpinnerAdapter(this#MainActivity, news!!);
spinner.adapter=adapter
}
}
}.execute()
}
}
Now Layout "activity_main":
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.ankitpatidar.checkkotlin.MainActivity">
<Spinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/spinner"></Spinner>
</LinearLayout>
Now Spinner Adapter as "SpinnerAdapter":
class SpinnerAdapter internal constructor(internal var context: Context, internal var list: List<ProductTypeDataResponse>) : BaseAdapter() {
override fun getCount(): Int {
return list.size
}
override fun getItem(i: Int): Any? {
return null
}
override fun getItemId(i: Int): Long {
return 0
}
override fun getView(i: Int, view: View?, viewGroup: ViewGroup): View {
var view = view
if (view == null) {
val inflater = LayoutInflater.from(context)
view = inflater.inflate(R.layout.item, viewGroup, false)
}
val textView = view!!.findViewById<TextView>(R.id.textView)
textView.text = list[i].productType + " " + list[i].readable
return textView
}
}
Spinner item layout as "item":
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/textView"/>
</LinearLayout>
Now some changes in your existing files:
"ApiModel.kt":
class TypeDataResponse(
val children: List<ProductTypeChildrenResponse>
)
class ProductTypeChildrenResponse(val data: ProductTypeDataResponse)
class ProductTypeDataResponse(
val productType: String,
val readable: String
)
"RestAPI.kt"
class RestAPI() {
private val tigaerApi: TigaerApi
init {
val retrofit = Retrofit.Builder()
.baseUrl("http://app.tigaer.id/laravel/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
tigaerApi = retrofit.create(TigaerApi::class.java)
}
fun getNews(): Call<List<ProductTypeDataResponse>> {
return tigaerApi.getTop()
}
}
Hence it will work for you.
is there any function to debug object/array as detail as in laravel ?
Go through this to run your app in debug mode. https://developer.android.com/studio/debug/index.html
You can always use breakpoints to evaluate expressions while the app is running. Instead of logging put a breakpoint at this line.
val news = response.body()
so when you'll receive a response from server, app will stop at this point and you can check what you are getting in response in detail.
how to populate my json return data into spinner?
If you are getting response from server in Json format as shown in provided link, you'll have to parse the data into a list of objects(POJO).
And then you have to forward this data(maybe you'll have to iterate over list to get the required data because you have two fields in each object) into an adapter and set that adapter to your spinner. It is explained very clearly at following link.
https://developer.android.com/guide/topics/ui/controls/spinner.html
I got an idea in this post, maybe my logic can help anyone in here
this is my JSON
{
"success": 1,
"dataset": [
{
"id": "3",
"nama": "Rush"
},
{
"id": "5",
"nama": "Avanza"
},
{
"id": "6",
"nama": "Innova"
},
{
"id": "14",
"nama": "Sienta"
},
{
"id": "15",
"nama": "Alphard"
},
{
"id": "16",
"nama": "Calya"
}
],
"sql_duration": 0.0013179779052734375,
"auth_duration": 1.9073486328125e-6,
"req_duration": 0.004480123519897461,
"debug_duration": []
}
this is my API Service
ApiMain.kt
class ApiMain : Application(){
private var BASE_URL = "your url in here"
private val client = OkHttpClient().newBuilder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
})
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
val services: ApiServices = retrofit.create(ApiServices::class.java)
}
and this is my API service
ApiServices.kt
interface ApiServices {
#GET("yourlink")
fun MerkKendaraan():Call<MerkKendaraan>
}
and this is my model
kendaraan.kt
data class MerkKendaraan(
#SerializedName("success")
#Expose
var success: Int? = null,
#SerializedName("dataset")
#Expose
var dataset: List<MerkMobil>? = null,
#SerializedName("sql_duration")
#Expose
var sql_duration: String? = null,
#SerializedName("auth_duration")
#Expose
var auth_duration: String? = null,
#SerializedName("req_duration")
#Expose
var req_duration: String? = null
)
data class MerkMobil(
#SerializedName("id")
#Expose
var id: String? = null,
#SerializedName("nama")
#Expose
var nama: String? = null
)
and this is my main activity
AddKendaraan
class AddKendaraan : AppCompatActivity() {
private var merk : ArrayList<MerkMobil> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_kendaraan)
configspinnermobil()
}
private fun configspinnermobil() {
val spinner: Spinner = findViewById(R.id.spinnerMerk)
ApiMain().services.MerkKendaraan().enqueue(object :
Callback<MerkKendaraan> {
override fun onResponse(call: Call<MerkKendaraan>, response: Response<MerkKendaraan>) {
//Tulis code jika response sukses
Log.d("data api", "data masuk")
if(response.code() == 200){
merk = response.body()?.dataset as ArrayList<MerkMobil>
var data : MutableList<String> = ArrayList()
merk.forEach {
data.add(0,it.nama.toString())
}
spinner.adapter = ArrayAdapter<String>(this#AddKendaraan, R.layout.support_simple_spinner_dropdown_item, data)
}else{
}
}
override fun onFailure(call: Call<MerkKendaraan>, t: Throwable){
//Tulis code jika response fail
Toast.makeText(applicationContext, t.message, Toast.LENGTH_LONG).show()
Log.d("data api", "data tidak masuk")
}
})
}

Categories

Resources