Parsing JSON Strings with moshi and retrofit2 - android

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>>
}

Related

error : "Not enough information to infer type variable T", I get this error while parsing Json

error : "Not enough information to infer type variable T", I get this error while parsing Json
Here is my code:
package com.example.chatopenai
import android.os.Bundle
import android.util.Log
import android.widget.EditText
import android.widget.ImageButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.HeaderMap
import retrofit2.http.POST
class MainActivity : AppCompatActivity() {
private lateinit var questionEditText: EditText
private lateinit var answerTextView: TextView
private lateinit var generateButton: ImageButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Get references to the UI elements
questionEditText = findViewById(R.id.message_input)
answerTextView = findViewById(R.id.textAns)
generateButton = findViewById(R.id.sendBtn)
// Set up the generate button click listener
generateButton.setOnClickListener {
// Get the user's question from the EditText
val question = questionEditText.text.toString()
// Send the request to the API
generateText(question)
}
}
// Service interface for the OpenAI API
interface OpenAIService {
#POST("v1/images/generations")
fun generateText(
#HeaderMap headers: Map<String, String>,
#Body request: Map<String, Any>
): Call<ResponseBody>
}
// Data class to hold the API response
data class ResponseData(val data: Data)
data class Data(val text: String)
private fun generateText(prompt: String) {
// Create a logger for debugging purposes
val logger = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
// Add the logger to the HTTP client
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.build()
// Configure the Retrofit object
val retrofit = Retrofit.Builder()
.baseUrl("https://api.openai.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
// Create an instance of the OpenAIService
val service = retrofit.create(OpenAIService::class.java)
// Set the request parameters
// Set the request parameters
val request = mapOf(
"prompt" to prompt,
"model" to "text-davinci-002"
)
// Set the request headers
val headers = mapOf(
"Content-Type" to "application/json",
"Authorization" to "Bearer MY_API_KEY"
)
// Make the API request
service.generateText(headers, request).enqueue(object : Callback<ResponseBody> {
override fun onResponse(
call: Call<ResponseBody>,
response: Response<ResponseBody>
) {
if (response.isSuccessful) {
// Parse the JSON response
val gson = Gson()
val responseString = response.body()!!.string()
val type = object : TypeToken<ResponseData>() {}.type
//I am getting error on below line on fromJson
val responseData = gson.fromJson(response.body()?.string(), type)
Log.d("JSON response", responseString)
Log.d("Type", type.toString())
// Display the response text in the TextView
answerTextView.text = responseData.data.text
} else {
// Display the error message in the TextView
answerTextView.text = response.errorBody()?.string()
}
}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
// Display the error message in the TextView
answerTextView.text = t.message
}
})
}
}
Please help me to fix this bug or let me know I missed any dependencies. I have mentioned where is the bug on the code.
Aplogize for my bad code
I tried to modify the code but not resolved. May be I missed some dependencies I don't know. Please help me to solve and fix this error

Can I return the if statement in a Repository for in-memory cache?

so I decided to use this documentation about he in-memory cache for the repository pattern here, without using the Coroutines in the first place. For what I have done I created a response from the server to make a query, however I want this data to be in cache. I've done as the documentation is explained, but however once I put the Boolean parameter, it just won't return anything, so I thought it's going to be the return type, but it says that it found a unit within the if-else statement. My objective is I want to return the cache if there is any data in it, if it doesn't have any data, to call from the server the latest data and write it in the cache and then return it to the application. Any ideas or hints would do nicely, if there is something that it didn't made sense of my question, feel free to correct me about it. Thank you in advance.
The DTO Model:
package com.example.spaceflightnews.network.model
data class ArticleResource(
val title: String,
val imageUrl: String,
val summary: String,
val publishedAt: String,
)
DataSource:
package com.example.spaceflightnews.network.datasource
import android.util.Log
import com.example.spaceflightnews.network.Retrofit
import com.example.spaceflightnews.network.model.ArticleResource
import com.example.spaceflightnews.network.spaceflightNewsAPI.ArticlesAPI
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class ArticleNetworkDataSource(
private val serviceApi: ArticlesAPI = Retrofit().getArticleInstance().create(ArticlesAPI::class.java)
) {
//Articles model call and callback
fun getArticleRequest(
callback: DataReadyCallback
) {
val call: Call<List<ArticleResource>> = serviceApi.getArticles()
call.enqueue(object : Callback<List<ArticleResource>> {
override fun onResponse(
call: Call<List<ArticleResource>>,
response: Response<List<ArticleResource>>
) {
response.body()?.run {
callback.onDataReady(this)
}
}
override fun onFailure(call: Call<List<ArticleResource>>, t: Throwable) {
Log.e("Error", t.localizedMessage!!)
}
})
}
interface DataReadyCallback {
fun onDataReady(data: List<ArticleResource>)
}
}
Repository:
class ArticlesRepository(
private val articlesNetworkDataSource: ArticleNetworkDataSource = ArticleNetworkDataSource()
) {
private var latestArticles: List<ArticleResource>? = null
fun getDataListObject(
mutableLiveData: MutableLiveData<List<ArticleResource>>,
isDataAddedInCache: Boolean = false
) {
if (isDataAddedInCache) {
//Calling the query
articlesNetworkDataSource.getArticleRequest(object : ArticleNetworkDataSource.DataReadyCallback {
override fun onDataReady(data: List<ArticleResource>) {
latestArticles = data
latestArticles?.run {
//Send it to the ViewModel
mutableLiveData.value = this
}
}
})
} else {
//If there is data in it, return that cache so it won't call the query again from the server.
latestArticles?.run {
mutableLiveData.value = this
}
}
}
}

Get data from certain tag in response (Kotlin, Retrofit)

I am trying to make a simple GET API call from Android app via Retrofit and Kotlin.
What I am struggling with is getting the data from a certain tag of response.
I am using Django rest framework with pagination, therefore results are enclosed in tag results. I can't figure out how to look into this results tag i.o. the whole response from retrofit.
My response:
{
"count": 13,
"next": null,
"previous": null,
"results": [
{
"id": 2,
"passport_number": 11233546,
"first_name": "Egor",
"last_name": "Wexler",
"email": "string",
"age": 0,
"city": "city"
},
...
{ <other customers> },
]
}
Customer.kt
import com.google.gson.annotations.SerializedName
data class Customer(
#SerializedName("passport_number") val passportNumber: Int,
#SerializedName("first_name") val firstName: String,
#SerializedName("last_name") val lastName: String,
#SerializedName("email") val email: String,
#SerializedName("age") val age: Int,
#SerializedName("city") val city: String
)
ApiInterface.kt
interface ApiInterface {
#GET("customers/")
fun getCustomers() : Call<List<Customer>>
companion object {
fun create() : ApiInterface {
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
return retrofit.create(ApiInterface::class.java)
}
}
}
MainActivity.kt
val apiInterface = ApiInterface.create()
apiInterface.getCustomers().enqueue(
object : Callback<List<Customer>> {
override fun onResponse(call: Call<List<Customer>>, response: Response<List<Customer>>) {
// logic for success
}
override fun onFailure(call: Call<List<Customer>>, t: Throwable) {
t.message // java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $
}
}
)
So the code eventually went to onFailure with the message that I put in the comment.
If I disable pagination on server - it works as expected because the response turns to what is according to app expectations (list of objects Customer).
But I want the app to look into tag results since it actually contains the response.
I feel like this should be trivial to do but can't find how.
I saw a lot of answers for the similar question marked by "Expected BEGIN_ARRAY but was BEGIN_OBJECT" but none of them solves my issue.
You should create a response class that follows the same structure as your response.
In your case something like this will do:
data class PageResult(
val count: Int,
val next: Int?,
val previous: Int?,
val results: List<Customer>
)
And in your API interface use:
#GET("customers/")
fun getCustomers() : Call<PageResult>

Kotlin Retrofit response.body() returned null on arrayList

i have setup some API data to display on my Android APP
[
{"attendanceID":1,"userID":1,"date":"14 Apr","clock_in":"10:11","clock_out":"19:39","noted":"Weekday"},
{"attendanceID":2,"userID":1,"date":"15 Apr","clock_in":"23:09","clock_out":"N.A.","noted":"Weekday"},
{"attendanceID":3,"userID":1,"date":"16 Apr","clock_in":"19:19 ","clock_out":"19:42 ","noted":"Weekday"}
]
my data model is this
data class Attendance (
val userID: Int,
val date: String?,
val clock_in: String?,
val clock_out: String?,
val noted: String?
)
the interface is this
interface INodeJS {
#GET("load_attendance")
fun loadAttendance(): Call<ArrayList<Attendance>>
}
and my code is this
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.absensi.ui.retrofit.INodeJS
import com.example.absensi.ui.retrofit.RetrofitClient
import kotlinx.android.synthetic.main.activity_detail_absensi.*
import retrofit2.Call
import retrofit2.Callback import retrofit2.Response
class DetailAbsensi : AppCompatActivity() {
lateinit var myAPI : INodeJS
private val attendanceList = ArrayList<Attendance>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail_absensi)
val retrofit = RetrofitClient.instance
myAPI = retrofit.create(INodeJS::class.java)
rvAttendance.setHasFixedSize(true)
rvAttendance.layoutManager = LinearLayoutManager(this)
myAPI.loadAttendance().enqueue(object : Callback<ArrayList<Attendance>> {
override fun onResponse(
call: Call<ArrayList<Attendance>>,
response: Response<ArrayList<Attendance>>
) {
attendanceList.addAll(response.body()!!)
val adapter = AttendanceAdapter(attendanceList)
rvAttendance.adapter = adapter
}
override fun onFailure(call: Call<ArrayList<Attendance>>, t: Throwable) {
}
})
}
}
but i always get this error when i open my DetailAbsensi
E/RecyclerView: No adapter attached; skipping layout
so after some hour checking my code i found that when running code myAPI.loadAttendance().enqueue the response i get when i checking response.body() is all null
[
Attendance{userID=0,date="Null",clockin="Null",clockout="Null",noted="Noted"},
Attendance{userID=0,date="Null",clockin="Null",clockout="Null",noted="Noted"},
Attendance{userID=0,date="Null",clockin="Null",clockout="Null",noted="Noted"}
]
You need to add this on every property of the object
data class Attendance (
#SerializedName("userID")
val userID: Int,
#SerializedName("date")
val date: String?,
#SerializedName("clock_in")
val clock_in: String?,
#SerializedName("clock_out")
val clock_out: String?,
#SerializedName("noted")
val noted: String?
)
Your problem has been resolved.
Can you make sure that you are calling these statements from the "main" thread outside of a delayed asynchronous callback (for example inside the onCreate() method). As soon as I call the same statements from a "delayed" method. In my case a ResultCallback, I get the same message.
In my Fragment, calling the code below from inside a ResultCallback method produces the same message. After moving the code to the onConnected() method within my app, the message was gone...
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(LinearLayoutManager.VERTICAL);
list.setLayoutManager(llm);
list.setAdapter( adapter );
Add this code in your activity.xml file.
In the recyclerView:
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"

Kotlin API call using Retrofit

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

Categories

Resources