Kotlin JSON Data not arranging into arraylist properly with Retrofit - android

I'm working on a kotlin android app with Retrofit. I'm making an API call to IEX stock data using this link:
https://api.iextrading.com/1.0/stock/market/batch?symbols=aapl,fb,ge&types=quote
The JSON data doesn't seem to arrange itself into an arraylist naturally. When I plug the data into jsonschema2pojo, it tells me that I should create class names for each of the stocks like this:
public class GE {
#SerializedName("quote")
#Expose
public Quote__ quote;
}
Naturally, I want the stock names to be variable so I can plug any list into there. Is there something wrong with the JSON data, or am I missing a step??
My Methods in case you wanted to see them (They're generic):
private fun getStock(stock: String) {
Timber.d("Start Retrofit Get Stocks")
val service = initiateRetrofit()
val call = service.queryStock("GE")
Timber.d("Url: " + call.request().url())
call.enqueue(object : retrofit2.Callback<StockModel> {
override fun onResponse(call: Call<StockModel>, response: retrofit2.Response<StockModel>) {
Timber.d("Successful Query. Message: " + response.message())
val stocklist : StockModel = response.body()
Timber.d("See what you get in the stock model")
}
override fun onFailure(call: Call<StockModel>, t: Throwable) {
Timber.d("Failed Call: " + t)
}
})
}
private fun initiateRetrofit(): RetrofitService {
val gson = GsonBuilder().setLenient().create()
val retrofit = Retrofit.Builder().baseUrl(RetrofitService.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson)).build()
return retrofit.create(RetrofitService::class.java)
}

There's a really clean and simple way of solving for this problem.
To get at the "quote" JSON object, you'll want to create a custom JSON deserializer. The JsonDeserializer is an interface that you implement from the Gson library.
First, we need our response object to use for deserialization.
// PortfolioResponse.kt
class PortfolioResponse {
var quotes: List<Quote>? = null
}
Next, we'll setup our ApiService class to make a call for a PortfolioResponse object.
// ApiService.kt
interface ApiService {
#GET("stock/market/batch")
abstract fun queryStockList(#Query("symbols") stocks: String, #Query("types") types: String): Call<PortfolioResponse>
}
Then, setup the deserializer. This is where we'll strip the unnecessary JSON object keys, and get the "quote" JSON objects we're looking for.
// PortfolioDeserializer.kt
class PortfolioDeserializer : JsonDeserializer<PortfolioResponse> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): PortfolioResponse {
val portfolioResponse = PortfolioResponse()
json?.let {
val jsonObject = it.asJsonObject
val symbolSet = jsonObject.entrySet()
val quoteElements = ArrayList<JsonObject>()
val quotes = ArrayList<Quote>()
val gson = Gson()
// this will give us a list of JSON elements that look like ""Quote": {}"
symbolSet.mapTo(quoteElements) { it.value.asJsonObject }
// this will take each quote JSON element, and only grab the JSON that resembles a Quote
// object, and add it to our list of Quotes
quoteElements.mapTo(quotes) { gson.fromJson(it.entrySet().first().value, Quote::class.java) }
portfolioResponse.quotes = quotes
}
return portfolioResponse
}
}
Finally, update your existing network call in your Activity, and it's done.
// MainActivity.kt
call.enqueue(object : retrofit2.Callback<PortfolioResponse> {
override fun onResponse(call: Call<PortfolioResponse>, response: retrofit2.Response<PortfolioResponse>) {
Timber.d("Successful Market Batch Query. Response.body=${response.body()}")
}
override fun onFailure(call: Call<PortfolioResponse>, t: Throwable) {
Timber.d("Failed Call: " + t)
}
})

The data is in a Map, not an array. Looks like the automated converter is trying to make it an object with field names. Try making your return value in your retrofit interface Call<Map<String, Quote__>>.
You will need to update the rest of your code to pull the key and values out of the map for processing.

Related

How to Parse Json in Kotlin Using Retrofit?

i am new to kotlin and i am in learning phase. I have followed many links but didn't able to understand completely.
I want Json response to show in my textview.
Problem: 1
I have tried this code but was unable to get data, but i want to get the items inside data object. Quote and author are coming null.
{
"status": 200,
"message": "Success",
"data": {
"Quote": "The pain you feel today will be the strength you feel tomorrow.",
"Author": ""
},
"time": "0.14 s"
}
Problem: 2
I dont know how to parse this response in textview
object ServiceBuilder {
private val client = OkHttpClient.Builder().build()
private val retrofit = Retrofit.Builder()
.baseUrl("https://url.com.pk/") // change this IP for testing by your actual machine IP
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
fun<T> buildService(service: Class<T>): T{
return retrofit.create(service)
}}
RestApi
interface RestApi{
#Headers("Content-Type: application/json")
#POST("api/getquotes")
abstract fun addUser(#Body userData: UserInfo): Call<UserInfo>}
RestAPiService
class RestApiService
{
fun addUser(userData: UserInfo, onResult: (UserInfo?) -> Unit)
{
val retrofit = ServiceBuilder.buildService(RestApi::class.java)
retrofit.addUser(userData).enqueue(
object : Callback<UserInfo>
{
override fun onFailure(call: Call<UserInfo>, t: Throwable)
{
onResult(null)
}
override fun onResponse( call: Call<UserInfo>, response: Response<UserInfo>)
{
val addedUser = response.body()
Log.d("responsee",""+addedUser)
onResult(addedUser)
}
}
)
}
}
UserInfo
data class UserInfo (
#SerializedName("Quote")
val quote : String,
#SerializedName("Author")
val author : String
)
MainActivity
fun getQuotes() {
val apiService = RestApiService()
val userInfo = UserInfo("","")
apiService.addUser(userInfo) {
Log.d("Error registering user","errter")
/*if ( != null)
{
// it = newly added user parsed as response
// it?.id = newly added user ID
} else {
Log.d("Error registering user","errter")
}*/
}
}
Any help would be appreciated :)
Status, message and data are all part of the response so you need to take care of that. For example this
data class AddUserResponse(
val `data`: UserInfo, //like you defined it
val message: String,
val status: Int,
val time: String
)
This means parameter and response are different so the RestApi needs to be changed to this
abstract fun addUser(#Body userData: UserInfo): Call<AddUserResponse>}
This in turn also change the types in the service like
class RestApiService
{
fun addUser(userData: UserInfo, onResult: (UserInfo?) -> Unit)
{
val retrofit = ServiceBuilder.buildService(RestApi::class.java)
retrofit.addUser(userData).enqueue(
object : Callback<AddUserResponse>
{
override fun onFailure(call: Call<AddUserResponse>, t: Throwable)
{
onResult(null)
}
override fun onResponse( call: Call<AddUserResponse>, response: Response<AddUserResponse>)
{
val addedUser = response.body()
Log.d("responsee",""+addedUser)
onResult(addedUser.data)
}
}
)
}
}
now in getQuotes you will have that it is a UserInfo object
apiService.addUser(userInfo) {
val returnedUserInfo = it
}
just follow my steps :
File->settings->Plugins
search for JSON To Kotlin class and install it
again click on File->New->Kotlin Data class from JSON
paste your json code here and click on generate. It will generate POJO classes and you will good to go.
The first thing I noticed, is that the data in your json is:
"Quote": "The pain you feel today will be the strength you feel tomorrow.",
"Author": ""
While your UserInfo defined #SerializedName("message") for Quote.

Proper way to update LiveData from the Model?

The "proper" way to update views with Android seems to be LiveData. But I can't determine the "proper" way to connect that to a model. Most of the documentation I have seen shows connecting to Room which returns a LiveData object. But (assuming I am not using Room), returning a LiveData object (which is "lifecycle aware", so specific to the activity/view framework of Android) in my model seems to me to violate the separation of concerns?
Here is an example with Activity...
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_activity);
val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
val nameText = findViewById<TextView>(R.id.nameTextBox)
viewModel.getName().observe(this, { name ->
nameText.value = name
})
}
}
And ViewModel...
class UserViewModel(): ViewModel() {
private val name: MutableLiveData<String> = MutableLiveData()
fun getName() : LiveData<String> {
return name
}
}
But how do I then connect that to my Model without putting a "lifecycle aware" object that is designed for a specific framework in my model (LiveData)...
class UserModel {
val uid
var name
fun queryUserInfo() {
/* API query here ... */
val request = JSONObjectRequest( ...
{ response ->
if( response.name != this.name ) {
this.name = response.name
/* Trigger LiveData update here somehow??? */
}
}
)
}
}
I am thinking I can maybe put an Observable object in my model and then use that to trigger the update of the LiveData in my ViewModel. But don't find any places where anyone else says that is the "right" way of doing it. Or, can I instantiate the LiveData object in the ViewModel from an Observable object in my model?
Or am I just thinking about this wrong or am I missing something?
This is from official documentation. Check comments in code...
UserModel should remain clean
class UserModel {
private val name: String,
private val lastName: String
}
Create repository to catch data from network
class UserRepository {
private val webservice: Webservice = TODO()
fun getUser(userId: String): LiveData<UserModel > {
val data = MutableLiveData<UserModel>() //Livedata that you observe
//you can get the data from api as you want, but it is important that you
//update the LiveDate that you will observe from the ViewModel
//and the same principle is in the relation ViewModel <=> Fragment
webservice.getUser(userId).enqueue(object : Callback<UserModel > {
override fun onResponse(call: Call<User>, response: Response<UserModel >) {
data.value = response.body()
}
// Error case is left out for brevity.
override fun onFailure(call: Call<UserModel >, t: Throwable) {
TODO()
}
})
return data //you will observe this from ViewModel
}
}
The following picture should explain to you what everything looks like
For more details check this:
https://developer.android.com/jetpack/guide
viewmodels-and-livedata-patterns-antipatterns

How to call REST API with Retrofit #Query

I can't get data base on idCafe.
This is the url from webservice :
https://admin-services-dot-annular-bucksaw-167705.appspot.com/_ah/api/meja?idCafe=123445
Api Service Interface :
#GET ("meja?idCafe")
fun getTable(#Query("idCafe") idCafe: String): Call<List<Table>>
I call api with this function :
private fun getTableList(idCafe:String){
val apiService : Service = Client.getClient()!!.create(Service::class.java)
apiService.getTable(idCafe).enqueue(object : Callback<List<Table>>{
override fun onResponse(call: Call<List<Table>>?, response: Response<List<Table>>?) {
Log.i("IdMeja", "id : " + response?.body())
if (response != null && response.isSuccessful) {
val listTable = response.body()
if (listTable == null || listTable.isEmpty()) {
Toast.makeText(this#MainActivity, "Tidak ada meja", Toast.LENGTH_SHORT).show()
}
else{
tableList = ArrayList(listTable)
// update list table
dataAdapter.updateData(tableList)
}
}
else{
Log.i("idCafe", " $idCafe")
Toast.makeText(this#MainActivity, "Gagal dapat meja", Toast.LENGTH_SHORT).show()
}
}
override fun onFailure(call: Call<List<Table>>?, t: Throwable?) {
Log.i("fail",t.toString() )
Toast.makeText(this#MainActivity, "Gagal", Toast.LENGTH_SHORT).show()
}
})
}
But the response unsuccessful. I can get idCafe, but can't get data of Table.
Please help me to fix this.
use like this
#GET ("meja")
fun getTable(#Query("idCafe") idCafe: Int): Call<List<Table>>
You dont have to include ? and query variable inside your URL.
When you use #Query Retrofit will build the query parameter for you. No need to specify it in the URL.
This means that you should just declare the interface like:
#GET ("meja")
fun getTable(#Query("idCafe") idCafe: String): Call<List<Table>>
Retrofit will then use the name inside the annotation - idCafe - and build the parameter with it.
You can keep using string, since retrofit can easily deal with it. However, if idCafe is better modeled as an integer in your application, then I'd suggest you use Int instead:
#GET ("meja")
fun getTable(#Query("idCafe") idCafe: Int): Call<List<Table>>
Note that this is only possible because in the URL idCafe is an integer.
Call your api like this
#GET ("meja")
fun getTable(#Query("idCafe") idCafe: String): Call<TableResponse>
And the table response call will be as follows -
class TableResponse {
#SerializedName("daftarMeja")
List<Table> list;
public List<Table> getList() {
return list;
}
public void setList(List<Table> list) {
this.list = list;
}
}
You can not use Call<List<Table>> as an result in the api call as error says "BEGIN_ARRAY but was BEGIN_OBJECT"

parsed gson returns null in kotlin

I'm new to programming,
i'm trying to get sunrise/sunset time out of yahoo weather api and toast it on Ui
(i'm using gson and anko library )
and this is my mainactivity code :
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fetchJson()
}
fun fetchJson(){
val url = "https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22nome%2C%20ak%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys"
val request = Request.Builder().url(url).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call?, e: IOException?) {
toast("Failed to execute request")
}
override fun onResponse(call: Call?, response: Response?) {
val body = response?.body()?.string()
println(body)
val gson = GsonBuilder().create()
val Info = gson.fromJson(body, astronomy::class.java)
runOnUiThread {
// info.sunrise is returning null ???????
toast("this is running from UiThread ${Info.sunrise}")
}
}
})
}
}
class astronomy(val sunrise: String, val sunset: String)
where should i fix?
Thanks
The response you get back from that Yahoo! API is much larger than just the astronomy section. You've got two options (one real option and one temporary one to check things):
Create a number of models to parse the entire stack (meaning you'd have a Query class with properties like count, created, lang, and results). This would be the better approach since you'll be dealing with real classes each step of the way.
data class Query(val count: Int?, val created: String?, val lang: String?, val results: Results?)
data class Results(val channel: Channel?)
//Channel should include more fields for the rest of the data
data class Channel(val astronomy: Astronomy?)
data class Astronomy(val sunrise: String?, val sunset: String?)
Throw the entire string into a generic JsonObject (which is GSON's provided class) and traverse through that object (query -> results -> channel -> astronomy -> sunrise and sunset). This isn't the proper approach but can work to make sure your data is coming in correctly:
val jsonObj: JsonObject = JsonParser().parse(body).asJsonObject
val astronomy = jsonObj
.getAsJsonObject("query")
.getAsJsonObject("results")
.getAsJsonObject("channel")
.getAsJsonObject("astronomy")
runOnUiThread {
toast("this is running from UiThread ${astronomy.get("sunrise").asString}")
}
Hey ebrahim khoshnood!
Welcome to StackOverflow. The problem seems to be, that you haven't created POJOs (classes) for the parent objects of astronomy. If you would like to parse everything only with Gson, you will have to create objects for "query", "results", "channel" and then inside of the channel you can have the astronomy object.
So for example you could have something like this.
class Query(val results: List<Channel>?)
class Channel(val astronomy: astronomy?) // astronomy? is the class you have posted.
and then you could parse everything like this
val query = gson.fromJson(body, astronomy::class.java)
val astronomy = query.results?.astronomy

Moshi's Custom Adapter with RxAndroid & Retrofit & Kotlin

After configuring Kotlin for Android project, I wrote a simple MainActivity.kt. It called Retrofit to get a JSON file which contained the following data:
{
"message": "success",
"user": {
"username": "Eric"
}
}
Now I want to use Moshi to convert the JSON data to Kotlin's class, so here are the two classes to reflect the above JSON structure:
class User(var username: String)
class UserJson(var message: String, var user: User)
And a custom type adapter for Moshi:
class UserAdapter {
#FromJson fun fromJson(userJson: UserJson) : User {
Log.d("MyLog", "message = ${userJson.message}") // = success
Log.d("MyLog", "user = ${userJson.user}") // = null
return userJson.user
}
}
When it goes into the function fromJson(), userJson.message = "success" as expected. But the strange thing is that userJson.user is null, which should be User(username="Eric").
I am new to Moshi and Kotlin, and I have already stuck with this problem for about 10 hours. Please help me out. Thanks for any help.
========================================
The following is the entire code of MainActivity.kt (50 lines only):
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Custom Type Adapters for Moshi
val userMoshi = Moshi.Builder().add(UserAdapter()).build()
val retrofit = Retrofit.Builder()
.baseUrl("https://dl.dropboxusercontent.com/")
.addConverterFactory(MoshiConverterFactory.create(userMoshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
val accountService = retrofit.create(AccountService::class.java)
accountService.signUpAnonymously()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { user ->
Log.d("MyLog", user.toString())
}
}
}
// ========== For Retrofit ==========
interface AccountService {
#GET("u/17350105/test.json")
fun signUpAnonymously() : Observable<User>
}
// ========== For Moshi ==========
class User(var username: String)
class UserJson(var message: String, var user: User)
class UserAdapter {
#FromJson fun fromJson(userJson: UserJson) : User {
Log.d("MyLog", "message = ${userJson.message}") // = success
Log.d("MyLog", "user = ${userJson.user}") // = null
return userJson.user
}
}
The build.gradle is:
compile "io.reactivex.rxjava2:rxjava:2.0.0"
compile "io.reactivex.rxjava2:rxandroid:2.0.0"
compile "com.android.support:appcompat-v7:25.0.0"
compile "com.squareup.retrofit2:retrofit:2.1.0"
compile "com.squareup.retrofit2:converter-moshi:2.1.0"
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
Thank you again.
You can solve the problem by changing your code to do something like below.
Basically in your case when the UserAdapter is registered, it tells moshi that it can create a User only from UserJson object. Hence Moshi does not recognize the JSON object with keyword user.
By adding an indirection in form of User1 (please pardon the naming convention), the UserJson is created properly with User1 from JSON.
class User(var username: String)
class User1(var username: String) // I introduced this class
class UserJson(var message: String, var user: User1) // changed User to User1
class UserAdapter {
#FromJson fun fromJson(userJson: UserJson): User {
println("message = ${userJson.message}")
println("user = ${userJson.user}")
return User(userJson.user.username)
}
}
If you just need the User object. There is a library called Moshi-Lazy-Adapters that provides a #Wrapped annotation, that allows specifying the path to the desired object. All you have to do is add the respective adapter to your Moshi instance and change the service code to:
interface AccountService {
#GET("u/17350105/test.json")
#Wrapped("user")
fun signUpAnonymously() : Observable<User>
}
No need for any other custom adapter.

Categories

Resources