Im trying to use Volley to get a JSON object from an API and then display it in a recyclerview using the MVVM Android.
Using Volley jsonObjectRequest Get request in the ModelView (SchoolViewModel) to get my data (JSON Object) parsed through Gson and pass it into my LiveData, then set up an observer in my View (MainFragment) to observe that data and keep updating it in the recycler.
Now the issue is that Volley is only invoking Error, "com.android.volley.ParseError: org.json.JSONException: Value [{"dbn":"02M260","school_name":"Clinton School.... "
I am not sure what is causing the parsing error, as it seems to be retrieving the JSON object as seen following by the error.
Any ideas why it's always jumping to VolleyError?
My View:
class MainFragment : Fragment() {
private lateinit var schoolViewModel: SchoolViewModel
private lateinit var linearLayoutManager: LinearLayoutManager
private var mainRecyclerAdapter: MainRecyclerAdapter = MainRecyclerAdapter()
//private var list: MutableList<MutableLiveData<SchoolsData>> = mutableListOf()
private var schoolsData = MutableLiveData<List<SchoolsData>>()
val API_Schools: String = "https://data.cityofnewyork.us/resource/s3k6-pzi2.json"
val API_SAT: String = "https://data.cityofnewyork.us/resource/f9bf-2cp4.json"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) : View? {
// Inflate the layout for this fragment
val binding = FragmentMainBinding.inflate(layoutInflater)
schoolViewModel = ViewModelProvider(this).get(SchoolViewModel::class.java)
linearLayoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL,false)
context?.let { schoolViewModel.getData(it) }
schoolViewModel._schoolsData.observe(viewLifecycleOwner, Observer{
mainRecyclerAdapter.setData(it)
} )
binding.recyclerMain.apply {
layoutManager = linearLayoutManager
adapter = mainRecyclerAdapter
}
return binding.root
}
}
ViewModel
class SchoolViewModel: ViewModel() {
private var schoolsData = MutableLiveData<List<SchoolsData>>()
val _schoolsData: LiveData<List<SchoolsData>> = schoolsData
private var satData = MutableLiveData<SATData>()
val _satData: LiveData<SATData> = satData
lateinit var jsonObjectRequest: JsonObjectRequest
val API_Schools: String = "https://data.cityofnewyork.us/resource/s3k6-pzi2.json"
val API_SAT: String = "https://data.cityofnewyork.us/resource/f9bf-2cp4.json"
fun getData(context: Context) {
val requestQueue: RequestQueue = Volley.newRequestQueue(context.applicationContext)
jsonObjectRequest = JsonObjectRequest(Request.Method.GET,API_Schools, null,
{
try {
val gson = Gson()
schoolsData = gson.fromJson(
it.toString(),
MutableLiveData<List<SchoolsData>>()::class.java
)
Log.e("RECEIVED", schoolsData.toString())
}catch (e: JSONException){
Log.e("JSONException", e.toString())
}
}, {
Log.e("ERROR",it.toString())
})
requestQueue.add(jsonObjectRequest)
}
}
Model
import com.google.gson.annotations.SerializedName
data class SchoolsData(
#SerializedName("dbn")
val id: String,
#SerializedName("school_name")
val school_name: String,
#SerializedName("total_students")
val students_num: Int,
#SerializedName("graduation_rate")
val graduation_rate: Float,
#SerializedName("primary_address_line_1")
val street: String,
#SerializedName("city")
val city: String
)
The JSON you are fetching is a JSONArray [{},{}] not a JSONObject, so you need to use JsonArrayRequest instead of JsonObjectRequest. Once you make that change, the volley error will go away, but you also need to update the parsing code - you should deserialize it to a List<SchoolsData>, not a MutableLiveData, like this:
val gson = Gson()
val listType = object : TypeToken<List<SchoolsData>>() {}.type
val schoolList = gson.fromJson<List<SchoolsData>>(
it.toString(),
listType
)
schoolsData.postValue(schoolList)
Related
I am trying to "dynamically" create CardView and then fill TextView with data from an API call. The thing is, to me it seems like the request is not getting called.
I created an adapter class based on Create dynamically lists with RecyclerView which is working fine and I am adding as many windows as needed. And then in the MainActivity, I am trying to do an API request with Volley. Before I created manually TextView and fed the results manually into it - which worked fine. But now with this dynamically created CardView nothing is happening. Could anyone try to guide me please?
MainActivity:
class MainActivity : AppCompatActivity() {
// private var message: TextView = findViewById<TextView>(R.id.jsonParse)
lateinit var weatherRV: RecyclerView
lateinit var weatherModelArray: ArrayList<WeatherViewModel>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar?.hide()
weatherRV = findViewById(R.id.mainView)
weatherModelArray = ArrayList()
val model1 = WeatherViewModel("Open weather", "")
val model2 = WeatherViewModel("Yr.no", "")
weatherModelArray.add(model1)
weatherModelArray.add(model2)
val weatherAdapter = WeatherAdapter(this, weatherModelArray)
val linearLayoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
weatherRV.layoutManager = linearLayoutManager
weatherRV.adapter = weatherAdapter
apICall(this#MainActivity, model1)
}
}
apiCall:
#RequiresApi(Build.VERSION_CODES.O)
internal fun apICall(context: Context, jsonView: WeatherViewModel) {
var lat: Double? = null
var lon: Double? = null
var url: String? = null
val apiKey: String = "XXXX"
lat = YYY.Y
lon = YYY.Y
url = "https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric&exclude=minutely,hourly"
// Instantiate the RequestQueue.
val queue = Volley.newRequestQueue(context)
val gsonPretty = GsonBuilder().setPrettyPrinting().create()
// Request a string response from the provided URL.
val stringRequest = StringRequest(
Request.Method.GET, url,
{response ->
val gson = Gson()
val openJSON = gson.fromJson(response.toString(), OpenWeatherJSON::class.java)
val prettyJsonString = gsonPretty.toJson(openJSON)
try {
val dt = openJSON.daily[0].dt
val sdf = SimpleDateFormat("dd-MM-yyyy")
val date = Date(dt * 1000) // snackbar.dismiss()
sdf.format(date) // jsonView.apiResult = sdf.format(date) // jsonView.apiResult.append("\n")
jsonView.apiResult = openJSON.daily[0].temp.toString()
}
catch (e: Exception) {
jsonView.apiResult = e.toString()
}
},
{
jsonView.apiResult = "Error"
})
// Add the request to the RequestQueue.
queue.add(stringRequest) }
Now in the image below you can see, that Result1 is not getting changed into the request response. I can change it with jsonView.text = "something" outside the stringRequest
Update1:
class WeatherAdapter(context: Context, weatherModelArray: ArrayList<WeatherViewModel>):
RecyclerView.Adapter<WeatherAdapter.ViewHolder>() {
private val weatherModelArray: ArrayList<WeatherViewModel>
// Constructor
init {
this.weatherModelArray = weatherModelArray
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeatherAdapter.ViewHolder {
val view: View = LayoutInflater.from(parent.context).inflate(R.layout.card_layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: WeatherAdapter.ViewHolder, position: Int) {
val model = weatherModelArray[position]
holder.apiName.text = model.apiName
holder.apiResult.text = model.apiResult
}
override fun getItemCount(): Int {
return weatherModelArray.size
}
inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val apiName: TextView
val apiResult: TextView
init {
apiName = itemView.findViewById(R.id.idApiName)
apiResult = itemView.findViewById(R.id.idApiResult)
}
}
}
You need to notify the RecyclerView adapter that the data has changed after you change the data. This means calling notifyDataSetChanged from within the callback. That would look something like this:
internal fun apICall(context: Context, jsonView: WeatherViewModel, adapter: WeatherAdapter) {
//...
val stringRequest = StringRequest(
Request.Method.GET, url,
{response ->
//...
jsonView.apiResult = openJSON.daily[0].temp.toString()
adapter.notifyDataSetChanged()
},
{
jsonView.apiResult = "Error"
adapter.notifyDataSetChanged()
})
// Add the request to the RequestQueue.
queue.add(stringRequest)
}
I am having an issue with reloading data in RecyclerView after the data is updated with a separate URL call. The FristFragment calls LoadDataFromUrl and displays the data in the Recycler View. Each Recycler's view item has a button with OnSetClickListener that calls another URL to update item's name data. After the user updates the item's name and a successful response is received, then the original myResponse data is updated with the new item's name. When the data is updated, I need to reload data in the Recycer View` but I can not figure it out. I spent two days trying to fix it but I can't get it running. Any help would be greatly appreciated!
Data Model:
DataModel.kt
class MyResponse (var Status: String = "", val items: List<Items> = emptyList())
class Items(var ItemId: String = "", var ItemName: String = "")
Code for Recycler View:
Adapter.kt
var recyclerView: RecyclerView? = null
class MainAdapter(val myResponse: MyResponse): RecyclerView.Adapter<CustomViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val cellForRow = layoutInflater.inflate(R.layout.my_custom_cell, parent,false)
return CustomViewHolder(cellForRow)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
val item = myResponse.items.get(position)
//there is a button for each item in the list that will call a function ButtonPressed from Frist Fragment
holder.view.button.setOnClickListener {
val firstFragment = FirstFragment()
firstFragment.ButtonPressed(item.ItemId,item.ItemName, position)
}
holder.view.textView_ItemID = item.itemId
holder.view.textView_Item_Name = item.itemName
}
class CustomViewHolder(val view: View, var Items: Item? = null): RecyclerView.ViewHolder(view){
}
}
Then I have a fragment Fragment.kt
FirstFragment.kt
class FirstFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.first_fragment, container, false)
return rootView
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
LoadDataFromUrl()
recyclerView.layoutManager = LinearLayoutManager(this.context,
LinearLayoutManager.HORIZONTAL, false)
}
//this function loads data on create view
fun LoadDataFromUrl(){
val url = "some_url"
val payload = "some_data_to_send"
val requestBody = payload.toRequestBody()
val request = Request.Builder()
.method("POST",requestBody)
.url(url)
.build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("error")
}
override fun onResponse(call: Call, response: Response) {
val body = response?.body?.string()
val gson = GsonBuilder().create()
myResponse = gson.fromJson(body, MyResponse::class.java)
activity?.runOnUiThread {
if (myResponse.Status== "200"){
recyclerView.adapter = MainAdapter(myResponse)
}
}
}
})
}
fun ButtonPressed(itemId: String, itemName: String, position: Int){
val url = "some_another_url"
val payload = "some_another_data_to_send"
val requestBody = payload.toRequestBody()
val request = Request.Builder()
.method("POST",requestBody)
.url(url)
.build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("error")
}
override fun onResponse(call: Call, response: Response) {
val body = response?.body?.string()
val gson = GsonBuilder().create()
val buttomPressedResponse = gson.fromJson(body, ButtonPressedResponse::class.java)
if (buttonPressedResponse.Status== "200") {
myResponse.response[position].Status = buttomPressedResponse.Status //will update existing object myResponse with a new item's name
//this is where I have a problem
recyclerView.adapter.notifyDataSetChanged()
}
}
}
}
I tried the following changes but I still get an error.
//I get this error: Fatal Exception: OkHttp Dispatcher Process. recyclerView must not be null. Then the app crashes and the view reloads. If I clcik the Button again I get an error saying: RecyclerView: No adapter atatched. Skipping layout.
activity?.runOnUiThread {
myResponse.response[position].Status = checkInOutResponse.Status //will update existing object myResponse with updated data
recyclerView.adapter?.notifyDataSetChanged()
}
I also tried to run on it runOnUiTHread but nothing happens with this code
activity?.runOnUiThread {
myResponse.response[position].Status = checkInOutResponse.Status //will update existing object myResponse with updated data
recyclerView.adapter?.notifyDataSetChanged()
}
Create var myResponse: MyResponse variable in Adapter
Adapter.kt
var recyclerView: RecyclerView? = null
class MainAdapter(val myResponseInit: MyResponse): RecyclerView.Adapter<CustomViewHolder>(){
var myResponse: MyResponse
myResponse = myResponseInit
fun submitMyResponse(data: MyResponse) {
myResponse = data
}
//Rest of the code onCreateViewHolder etc.
}
Call submitMyResponse() function and notifyDataSetChanged() on adapter everytime you receive response.
I try to write an quiz app for Android in Kotlin.
I take all data from API and create data classes
Quiz
data class Quiz(
val answerIds: List<Any>,
val groupCodes: List<Any>,
val questionList: List<Question>
)
Question
data class Question(
val answers: List<Answer>,
val groupCode: String,
val hasSimilarQuestions: Boolean,
val id: Int,
val text: String
)
Answer
data class Answer(
val addsGroupCodes: List<String>,
val id: Int,
val questionId: Int,
val text: String
)
I use Volley to make http request
My problem is:
I try to display only text from answers to specific question in ListView.
I create ArrayAdapter but I can not display only text of certain answer
class MainActivity : AppCompatActivity()
{
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val url = ""
showQuiz(url);
}
private fun showQuiz(url:String)
{
val requestQueue = Volley.newRequestQueue(this)
val pytanie : TextView = findViewById(R.id.pytanie)
val jsonObjectRequest = JsonObjectRequest(Request.Method.GET, url, null, object : Response.Listener<JSONObject?>
{
override fun onResponse(response: JSONObject?) {
try
{
val jsonArray = response?.getJSONArray("questionList")
if (jsonArray != null)
{
val quiz:Quiz = Gson().fromJson(response.toString(), Quiz::class.java)
val odp = findViewById<ListView>(R.id.odpowiedzi)
for (question in quiz.questionList)
{
val arrayAdapter: ArrayAdapter<Answer> = ArrayAdapter(this#MainActivity, android.R.layout.simple_list_item_1, question.answers)
odp.adapter = arrayAdapter
}
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
},
object : Response.ErrorListener
{
override fun onErrorResponse(error: VolleyError)
{
error.printStackTrace()
Log.d("JsonObjectErr",error.toString());
}
}
)
requestQueue.add(jsonObjectRequest)
}
}
What about access to other elements in ArrayAdapter, because I need to remember what user choose.
ArrayAdapter shows value from toString() of an item.
So override toString() in Answer and return only value you want to show.
I want to take data from API using AsyncHttpClient(), after that, I show the data to recycle view. but when trying to debug my code is run show the data to recycle view which is no data because not yet finish setData using AsyncHttpClient(). my question is how to run setData recycle view adapter after my setLeague is finish getting data? Sorry, I am a newbie and I put my code below. Please help, I already try using async-await but doesn't work.
This is my ViewModel Class
val footballLeagueList = MutableLiveData<ArrayList<league>>()
internal fun setLeague(){
val client = AsyncHttpClient()
val client2 = AsyncHttpClient()
val listItems = ArrayList<league>()
val leagueListUrl = "https://www.thesportsdb.com/api/v1/json/1/all_leagues.php"
val leagueDetailUrl = "https://www.thesportsdb.com/api/v1/json/1/lookupleague.php?id="
client.get(leagueListUrl, object : AsyncHttpResponseHandler(){
override fun onSuccess(
statusCode: Int,
headers: Array<out Header>?,
responseBody: ByteArray?
) {
try{
val result = String(responseBody!!)
val responseObject = JSONObject(result)
for(i in 0 until 10){
val list = responseObject.getJSONArray("leagues")
val leagues = list.getJSONObject(i)
val leaguesItems = league()
val detailUrl = leagueDetailUrl + leagues.getString("idLeague")
leaguesItems.id = leagues.getString("id")
leaguesItems.name = leagues.getString("strLeague")
leaguesItems.badgeUrl = leagues.getString( "https://www.thesportsdb.com/images/media/league/badge/dqo6r91549878326.png")
listItems.add(leaguesItems)
}
footballLeagueList.postValue(listItems)
}catch (e: Exception){
Log.d("Exception", e.message.toString())
}
}
override fun onFailure(
statusCode: Int,
headers: Array<out Header>?,
responseBody: ByteArray?,
error: Throwable?
) {
Log.d("Failed", "On Failure")
}
})
}
internal fun getLeague(): LiveData<ArrayList<league>> {
return footballLeagueList
}
And this is my MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var mainViewModel: MainViewModel
private lateinit var adapterLeague: leagueAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapterLeague = leagueAdapter()
adapterLeague.notifyDataSetChanged()
verticalLayout {
lparams(matchParent, matchParent)
recyclerView {
layoutManager = GridLayoutManager(context, 2)
adapter = adapterLeague
}
}
mainViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
.get(MainViewModel::class.java)
mainViewModel.setLeague()
mainViewModel.getLeague().observe(this, Observer { leaguesItems ->
if(leaguesItems != null){
adapterLeague.setData(leaguesItems)
}
})
}
}
I created a gridView that has an ArrayAdapter, the gridView contains only photos, I am fetching the image url in an Array and I am observing the array through my activity. Here is my viewmodel
class ProfileViewModel constructor(): ViewModel() {
var first_name: String? = null
var last_name: String? = null
var dob: String? = null
var address: String? = null
var organization: String? = null
var hobby: String? = null
var bio: String? = null
var imagePath: String = ""
private val imageList : MutableLiveData<ArrayList<ProfileViewModel>> = MutableLiveData()
constructor(photo : Photo) : this() {
this.imagePath = photo.imageUrl
}
fun getImageUrl() : String {
return imagePath
}
companion object {
#BindingAdapter("imageUrl")
#JvmStatic
fun loadImage(imageView: ImageView, imageUrl: String) {
Glide.with(imageView.context)
.load(imageUrl)
.apply(RequestOptions.centerCropTransform())
.placeholder(R.drawable.ic_add_icon)
.into(imageView)
}
}
val profileViewModels : MutableLiveData<ArrayList<ProfileViewModel>>
get() {
val profileViewModels = ArrayList<ProfileViewModel>()
val photo1 = Photo("")
val profileVM = ProfileViewModel(photo1)
repeat(6) {
profileViewModels.add(profileVM)
}
imageList.value = profileViewModels
return imageList
}
}
}
Here is my activity where I am observing the data
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityProfileBinding =
DataBindingUtil.setContentView(this, R.layout.activity_profile)
val viewModel = ViewModelProvider(this).get(ProfileViewModel::class.java)
viewModel.profileViewModels.observe(this,
Observer<ArrayList<ProfileViewModel>> { image_paths ->
Log.d("added", "$image_paths")
val imageAdapter = ImageAdapter(this#Profile, R.layout.image_card, image_paths!!)
gridView.adapter = imageAdapter
})
}
I am getting images in the gridView but I want to update the observable value on gridView Item click in the clicked position. How do I do that?
First, you can create a function in viewmodel, that you want to do when clicked. for example:
private fun doSomethingWhenClicked(listPosition: Int){
val clickedImage = profileViewModels[position]
//do something here for clicked image
//..
}
Then, initialize the viewmodel in adapter like this. So you can update your profileViewModels in onClickListener inside the ImageAdapter
viewmodel.doSomethingWhenClicked(position)
Hope this answer you!