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
Related
The API I am using responds with a JSON object nested inside a list, like so:
[
{
"name": "Seattle",
"lat": 47.6038321,
"lon": -122.3300624,
"country": "US",
"state": "Washington"
}
]
I'd like to parse the JSON with Moshi into the following class:
package com.example.weatherapp.entity
// the main JSON response
data class LocationWeather(val name: String, val lat: Float, val lon: Float)
My API service file is as follows.
package com.example.weatherapp.network
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.GET
import com.example.weatherapp.entity.LocationWeather
import retrofit2.http.Query
private const val BASE_URL = "http://api.openweathermap.org/geo/1.0/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface WeatherAPIService {
#GET("direct?")
fun getWeatherFromAPI(#Query("q") loc: String,
#Query("limit") lim: Int,
#Query("appid") key: String): Call<LocationWeather>
}
object WeatherApi {
val retrofitService : WeatherAPIService by lazy {
retrofit.create(WeatherAPIService::class.java)
}
}
My ViewModel, which actually connects to the API, is as follows:
package com.example.weatherapp.overview
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.weatherapp.entity.LocationWeather
import com.example.weatherapp.network.WeatherApi
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class OverviewViewModel : ViewModel() {
// the mutable backing property
private var _response = MutableLiveData<String>()
// the immutable exposed property
val response: LiveData<String>
get() = _response
fun getWeather(location: String) {
WeatherApi.retrofitService.getWeatherFromAPI(location, 1, "API_KEY_REDACTED").enqueue(
object: Callback<LocationWeather> {
override fun onResponse(call: Call<LocationWeather>, response: Response<LocationWeather>) {
_response.value = response.body()?.name
}
override fun onFailure(call: Call<LocationWeather>, t: Throwable) {
_response.value = "Failure: " + t.message
}
}
)
}
}
The fragment associated with this ViewModel just observes the response LiveData and renders the value to a TextView. The value rendered is "Failure: Expected BEGIN_OBJECT but was BEGIN_ARRAY at path $". I believe the problem is that, since the JSON is nested inside a list, the values are not being stored properly in the LocationWeather data class. The code otherwise compiles and the emulator runs. Attempting to use this same code with a different API that is NOT nested in a list works exactly as intended (provided I update the parameter names in LocationWeather). When forced to parse the JSON above, however, I am at a loss. I read the moshi docs and several others posts but I am not entirely certain how to implement their solutions using adapters or the Types.newParameterizedType() method.
How can I parse this JSON with moshi and retrofit2?
Your API is returning a list of LocationWeather objects, but your code is trying to fetch a single LocationWeather object. So, it is throwing the mentioned exception.
Update your code to fix the issue:
fun getWeather(location: String) {
WeatherApi.retrofitService.getWeatherFromAPI(location, 1, "API_KEY_REDACTED").enqueue(
object: Callback<List<LocationWeather>> {
override fun onResponse(call: Call<List<LocationWeather>>, response: Response<List<LocationWeather>>) {
// here I'm trying to access the first element of the list using 0th index.
_response.value = response.body()[0]?.name
}
override fun onFailure(call: Call<LocationWeather>, t: Throwable) {
_response.value = "Failure: " + t.message
}
}
)
}
You have to also update the return type of the method in Interface:
interface WeatherAPIService {
#GET("direct?")
fun getWeatherFromAPI(#Query("q") loc: String,
#Query("limit") lim: Int,
#Query("appid") key: String): Call<List<LocationWeather>>
}
The sample response data is array.
[
{
"name": "Seattle",
"lat": 47.6038321,
"lon": -122.3300624,
"country": "US",
"state": "Washington"
}
]
[] <- This is array.
Change the response type.
This is example.
interface WeatherAPIService {
#GET("direct?")
fun getWeatherFromAPI(#Query("q") loc: String,
#Query("limit") lim: Int,
#Query("appid") key: String): Call<List<LocationWeather>>
}
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 fetch JSON from localhost but unable to do so.
I can access the file by typing the URL (http://10.0.2.2/evapo/json_get_data.php) in browser of virtual device but somehow am not able to access it from within code.
Main Activity Class
class MainActivity : AppCompatActivity()
{
private val CROP_BASE_URL="http://10.0.2.2/"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
makeAPIRequest()
}
fun makeAPIRequest()
{
val api:APIRequest =Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(APIRequest::class.java)
GlobalScope.launch {
try {
Log.d("TEST", "makeAPIRequest: HERE")
val response:ServerResponse=api.getCropData()
//control never reach here
Log.d("TEST", "makeAPIRequest:"+response)
}catch (e:Exception)
{
e.printStackTrace()
}
}
}
}
API Request Interface
interface APIRequest {
#GET("evapo/json_get_data.php")
suspend fun getCropData():ServerResponse
}
Crop Response Class
data class CropResponse(
#SerializedName("server_response")
val serverResponse: List<ServerResponse>
)
Server Response Class
data class ServerResponse(
#SerializedName("cropName")
val cropName: String,
#SerializedName("eigth_month")
val eigthMonth: String,
#SerializedName("eleventh_month")
val eleventhMonth: String,
#SerializedName("fifth_month")
val fifthMonth: String,
#SerializedName("first_month")
val firstMonth: String,
#SerializedName("fourth_month")
val fourthMonth: String,
#SerializedName("nineth_month")
val ninethMonth: String,
#SerializedName("second_month")
val secondMonth: String,
#SerializedName("seventh_month")
val seventhMonth: String,
#SerializedName("sixth_month")
val sixthMonth: String,
#SerializedName("sowingDate")
val sowingDate: String,
#SerializedName("tenth_month")
val tenthMonth: String,
#SerializedName("third_month")
val thirdMonth: String,
#SerializedName("twelveth_month")
val twelvethMonth: String
)
json_get_data.php returns
{
"server_response": [
{
"cropName": "Cotton",
"sowingDate": "03-03-2020",
"first_month": "85.59",
"second_month": "185.134",
"third_month": "261.88",
"fourth_month": "388.608",
"fifth_month": "312.979",
"sixth_month": "219.848",
"seventh_month": "193",
"eigth_month": "0",
"nineth_month": "0",
"tenth_month": "0",
"eleventh_month": "0",
"twelveth_month": "0"
}
]
}
Log Cat
2020-09-01 13:10:06.608 10803-10828/dummy.dummy D/TEST: makeAPIRequest: HERE
Linking Stack trace from log cat, because character limit was reached
Stack trace
Solution
Changed the return type as specified in accepted answer.
Was passing a wrong URL instead of "http://10.0.2.2/" to baseURL()
Since your API returns a list you need to return
suspend fun getCropData(): CropResponse
in API Request Interface
Since your api is returning a list, you need to return CropResponse wrapped in the Resonse object from Retrofit in the getCropData() function as follows.
interface APIRequest {
#GET("evapo/json_get_data.php")
suspend fun getCropData() : Response<CropResponse>
}
Then, inside the coroutine you get the data as follows:
GlobalScope.launch {
try {
Log.d("TEST", "makeAPIRequest: HERE")
val response: CropResponse = api.getCropData().body()
val serverResponse: List<ServerResponse> = response.serverResponse
Log.d("TEST", "makeAPIRequest:"+response)
}catch (e:Exception)
{
e.printStackTrace()
}
}
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
Working on a small android app to help me learn about JSON queries. The little test app works until I try to drill a little deeper in to my test JSON data.
I'll post a link to the JSON data I'm working with to save space here in the question. It is example code pulled from weatherwunderground's API and hosted on myjson.com.
JSON: https://api.myjson.com/bins/19uurt
MAIN ACTIVITY CODE
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val retriever = CycloneRetriever()
val callback = object : Callback<Cyclones> {
override fun onResponse(call: Call<Cyclones>?, response: Response<Cyclones>?) {
println("Got a response!")
println(response?.body()?.currenthurricane?.get(1)?.stormInfo?.get(0)?.stormName).toString()
}
override fun onFailure(call: Call<Cyclones>?, t: Throwable?) {
println("The thing, it failed!")
}
}
retriever.getCyclones(callback)
}
}
This is my class that helps build/gather the JSON data
CLASS
interface WeatherWunderGroundAPI {
#GET("bins/19uurt")
weatherwunderground.com API
fun getCyclones() : Call<Cyclones>
}
class Cyclones(val currenthurricane: List<CurrentHurricane>)
class CurrentHurricane(val stormInfo: List<StormInfo>)
class StormInfo(val stormName: String)
class CycloneRetriever {
val apiRetriever: WeatherWunderGroundAPI
init {
val retrofitCyclone =
Retrofit.Builder().baseUrl("https://api.myjson.com/")
.addConverterFactory(GsonConverterFactory.create()).build()
apiRetriever =retrofitCyclone.create(WeatherWunderGroundAPI::class.java)
}
fun getCyclones(callback: Callback<Cyclones>) {
val call = apiRetriever.getCyclones()
call.enqueue(callback)
}
}
Right now I'm just trying to get a good response and print that to the console. Eventually, I will take the JSON data and dump it into a RecyclerView.
I can get a good response if I do the following Println, but it does not return anything of use:
println(response?.body()?.currenthurricane
But once I try to dig further into .currenthurricane, onFailure() is called.
According to some JSON docs, this should get me what I want: $.currenthurricane.[stormInfo].stormName As an example.
But I cannot figure out how to get this working in my code. I was gonna give Klaxxon a try, but have not quite figured out how to get that working either.
I'm using Retrofit2 and GSON plugins in the code above. I'm fairly confident the issue is my JSON query.
Finally figured out the problem. I was on the right track, but needed a little tweaking on my JSON queries and need to change my classes.
I'm still working on it, and I'm sure there is a much better way to do this, but here is the quick and dirty.
Changed my classes and added a couple data classes:
class Cyclones(val currenthurricane: List<CurrentHurricane>)
class CurrentHurricane(val stormInfo: StormInfo, val Current: Current)
data class StormInfo(val stormName: String)
data class Current(val lat: Double, val lon: Double, val
SaffirSimpsonCategory: Int, val Category: String, val WindSpeed: WindSpeed,
val Movement: Movement)
data class WindSpeed(val Kts: Int, val Mph: Int, val Kph: Int)
data class Movement(val kts: Int, val mph: Int, val kph: Int)
And changed my JSON queries to the following:
val cycloneInfo =
response?.body()?.currenthurricane?.get(0)?.stormInfo?.stormName
val cycloneCurrentLat =
response?.body()?.currenthurricane?.get(0)?.Current?.lat
val cycloneCurrentLon =
response?.body()?.currenthurricane?.get(0)?.Current?.lon
val cycloneCurrentSSCat =
response?.body()?.currenthurricane?.get(0)?.Current?.SaffirSimpsonCategory
val cycloneCurrentCategory =
response?.body()?.currenthurricane?.get(0)?.Current?.Category
val cycloneCurrentWindKts =
response?.body()?.currenthurricane?.get(0)?.Current?.WindSpeed?.Kts
val cycloneCurrentMovement =
response?.body()?.currenthurricane?.get(0)?.Current?.Movement?.kts