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.
Related
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.
I am trying to make a simple post request to googles dialogflow in retrofit using kotlin. I am modeling my code off of this site. However, I keep getting 400 errors when trying to make a search so there must be something wrong with my interface creating the message body I believe. I have working python code that does the same functionality as shown here:
url = "https://api.dialogflow.com/v1/query?v=20170712"
headers = {
'Authorization': 'Bearer ' + my_key ,
'Content-Type' : 'application/json'
}
body = {
'lang': 'en',
'query': 'id like to fix my wire c1000 stocks',
'sessionId': 'me'
}
resp = r.post(url,headers=headers,data=json.dumps(body))
I have set this up in android studio as 3 classes:
1) Message.kt
The body of the post request
object Message {
data class MsgBody(val lang: String, val query: String, val sesId: String)
}
2) Model.kt
The response from dialogflow
object Model {
data class Response(val resp: Result)
data class Result(val fulfillment: Fulfillment)
data class Fulfillment(val speech: String)
}
3) DialogFlowService.kt
The interface that has the post request enpoint
interface DialogFlowService {
#Headers(
"Authorization: Bearer {MY API KEY}",
"Content-Type: application/json"
)
#POST("query")
fun getAiMessage(#Body msg: Message.MsgBody,
#Query("v") v: String): Observable<Model1.Response>
companion object {
fun create(): DialogFlowService {
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://api.dialogflow.com/v1/")
.build()
return retrofit.create(DialogFlowService::class.java)
}
}
}
All of this is then used in my main activity as seen below:
class MainActivity : AppCompatActivity() {
private var disposable: Disposable? = null
private val dialogFlowService by lazy {
DialogFlowService.create()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
if (editText.text.toString().isNotEmpty()) {
sendMessage(editText.text.toString())
}
}
}
private fun sendMessage(msg: String){
disposable = dialogFlowService.getAiMessage(Message.MsgBody("en",msg,"me"),"20170712")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result -> textView.text = "DialogFlow says: ${result.resp.fulfillment.speech}" },
{ error -> Toast.makeText(this, error.message, Toast.LENGTH_SHORT).show() }
)
}
override fun onPause() {
super.onPause()
disposable?.dispose()
}
}
I tried to follow the tutorial as close as possible and am very confused as to what I did wrong. Like I said above I think this is related to my DialogFlowService.kt file. Thanks for any help in advance.
Error in post request since Model variable name sesId did not equal the actual key sessionId. As Raghunandan said a logging interceptor is very useful
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
I'm new to Kotlin, Android and OOP in general (Natural-ADABAS background, never did Java, C++, etc) so I'm pretty desperate.
I have an API whose data looks like this, an array of book details:
API data sample
I'm confused about data models. I know it's supposed to look like how the data in the API and return an array but how exactly do I code it in Kotlin? And then how do I parse it? I've read some tutorials but they all differ. Some use an object, and some use a class.
I'm also probably breaking some standard by putting everything in the main activity but I haven't gotten to that part yet.
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
class MainActivity : AppCompatActivity()
{
private val api: RestAPI = RestAPI()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val apiGetBooks = api.getBooksList("token123123123")
val response = apiGetBooks.execute()
if (response.isSuccessful) {
val books = response.body()?.title
println(books)
} else {
println("error on API") // What do I do?
}
}
object Model {
val ResultArray : MutableList<BookProperties>? = null
}
data class BookProperties (val id: Int,val title: String, val coverURI: String, val pageURI: String, val pageCount: Int, val languageId: Int,val description: String, val isFree: Boolean) {
}
private val buriApi: MainActivity.BooksAPI? = null
class RestAPI {
private val buriApi: BooksAPI
init {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.someurl.com")
.addConverterFactory(MoshiConverterFactory.create())
.build()
buriApi = retrofit.create(BooksAPI::class.java)
}
fun getBooksList(token: String): Call<BookProperties>{
return buriApi.getBooks(token)
}
}
fun getBooksList(token: String): Call<MainActivity.BookProperties> {
return buriApi!!.getBooks(token)
}
interface BooksAPI {
#GET("/v1/books")
fun getBooks (#Query("token")token: String) : Call<BookProperties>
}
}
After much googling, I finally solved my problem thanks to How to Quickly Fetch Parse JSON with OkHttp and Gson on YouTube.
fun fetchBooks () {
println("fetching books")
val url = "https://api.someurl.com/v1/books?"
val request = Request.Builder().url(url).build()
println(request)
val client = OkHttpClient()
client.newCall(request).enqueue(object: Callback {
override fun onResponse(call: Call?, response: Response?) {
val body = response?.body()?.string()
println(body)
}
override fun onFailure(call: Call?, e: IOException?) {
println("Failed to execute request")
e?.printStackTrace()
}
})
}
Still need to format the data and figure out how to turn on wifi in my Android emulator but at least I can consume the JSON.
Let's start with a sample and I guess you can map it accordingly to your requirement.
I don't have your JSON as text so I am giving an example of mine.
sample JSON response
{
"status": true,
"message": "User created Successfully.",
"response": {
"user": {
"id": 12,
"email": "testmail#gmailtest.com"
},
"token": "eyJlbWFpbCI6ImVzaGFudHNhaHUxMTBAZ21hc2kyMmwuY29tIiwidXNlcklkIjoxNSwiaWF0IjoxNTIxNTYyNjkxfQ"
}
}
so create a new class and name it something like this
CreateResponse.kt
and just map those objects and arrays from json to data classes and list here.
data class CreateUserResponse(override val status: Boolean? = null,
override val message: String? = null,
val response: Response? = null)
data class Response(val user: User?, val token: String)
data class User(val id: Int, val email: String)
easy right, now with Kotlin you can declare your data classes without creating separate files each time for each object just create one file and declare all of them at once.
I'm attaching some of the resources here which may help you understand the things better.
https://antonioleiva.com/retrofit-android-kotlin/
https://segunfamisa.com/posts/using-retrofit-on-android-with-kotlin
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.