I have two JSON files which are connected using foreign key/field (in my case one is Post( id, ...) and Comments ( postId, ...)). I need to display total number of comments per post (in my case it's always 5).
My data classes are as follows:
data class Posts(val userId: Int,
val id: Int,
val title: String,
val body: String)
data class Comments(val postId: Int,
val id: Int,
val name: String,
val email: String,
val body: String)
And here is the function which I use to get json data (I use okhttp for setting up the client and gson for getting the data):
private fun fetchCommentJson() {
val postId = intent.getIntExtra(POST_ID, -1)
val commentJsonData = commentsJSON
val client = OkHttpClient()
val request = Request.Builder().url(commentJsonData).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call?, e: IOException?) {
toast("Something went wrong fetching your data")
}
override fun onResponse(call: Call?, response: Response?) {
val body = response?.body()?.string()
val gson = GsonBuilder().create()
val commentsDetail = gson.fromJson(body, Array<Comments>::class.java)
val postDetail = gson.fromJson(body, Array<Posts>::class.java)
runOnUiThread {
for (commentSizeList in 0 until commentsDetail.size) {
val listOfComments = commentsDetail[commentSizeList] // List of all comments by Index
val post = postDetail[postId - 1].id // Post Id
// If postId is equal to original post id
if (listOfComments.postId == post) {
// Print it out
println(listOfComments)
}
}
}
}
})
}
When I print out the list, it really does print out all the comments per particular post based on it's ID (since comparison it's done). However it prints out each comment separately (and setting listOfComments.size prints out 1 for each comment).
displaying list of all comments per post
My question is how to combine them all, so it displays total number of comments per post (in this case -> 5)?
So the way I would do this is like this:
val totalNumberOfCommentsForPost = commentsDetail?.filter {
it.postId == post
}?.size ?: 0
.filter only selects the comments with the same post id.
Then .size gets hows many are in the new filtered list.
And finally as a safety precaution ?: 0 if any of these functions return null then return 0.
If I understand what you need correctly, you could replace your entire for loop with this code using filter:
// Post Id, the way you've been calculating it
val post = postDetail[postId - 1].id
// a list of comments only containing the ones that have the ID you need
val filteredComments: List<Comments> = commentsDetail.filter { it.postId == post }
From here, you can print either the entire list:
println(filteredComments)
Or the size of the list:
println(filteredComments.size)
Related
I'm doing a lot of requests to get all players from a dummy API test.
https://balldontlie.io/api/v1/players?page=0
But when it reaches iteration 60, or 67 or something like that, the app crash with a null
suspend fun getAllPlayers(): PlayersNetworkModel {
return withContext(Dispatchers.IO) {
val initMetaInfoResponse = api.getAllPlayers()
var data: MutableList<DataNetworkModel> = mutableListOf()
var meta = initMetaInfoResponse.body()?.meta
while(meta?.currentPage!! < meta?.totalPages!! && meta?.nextPage != null){
val recursiveResponse = api.getAllPlayers(meta?.nextPage!!)
recursiveResponse.body()?.data?.let { results -> data.addAll(results) }
meta = recursiveResponse.body()?.meta!!
}
PlayersNetworkModel(data, meta)
}
}
the error is:
Process: com.victormartin.archmvvm, PID: 14551 java.lang.NullPointerException
at com.victormartin.archmvvm.data.network.ApiService$getAllPlayers$2.invokeSuspend(ApiService.kt:28 undefined)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33 undefined)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106 undefined)
at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42 undefined)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95 undefined)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570 undefined)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750 undefined)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677 undefined)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664 undefined)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}#c2e4a7e, Dispatchers.Main.immediate]
Why is it hanging if I do a lot of requests?
There are two possibilities in my eyes. First option is that this crash occurs because of the mess with using the !! operator and somehow the api.getAllPlayers(meta?.nextPage!!) is getting null. I have just implemented some dummy code but it looks much clearer, see below.
data class PlayersData(
val data: List<Data>,
val meta: Meta
) {
data class Data(
val id: Int,
val first_name: String
// add more fields if needed
)
data class Meta(
val total_pages: Int,
val current_page: Int,
val next_page: Int?,
val per_page: Int,
val total_count: Int
)
}
And the get method:
private suspend fun getAllPlayers() = withContext(Dispatchers.IO) {
val resultsList = mutableListOf<PlayersData.Data>()
var nextPage: Int? = 0
while (nextPage != null) {
val response = playersService.getAllPlayers(nextPage)
resultsList.addAll(response.data)
nextPage = response.meta.next_page
}
}
I don't get any NullPointerException, however I get the:
retrofit2.HttpException: HTTP 429 Too Many Requests
which occurs after doing 60 requests - that's the second option. You probably just posted wrong error message. Try adjusting the code to avoid using the !! operator and then check the logs for the HTTP 429 error.
Finally, you shouldn't spam so many requests to get all of the data at once. It's paginated for some reason. You should rather load the next pages on demand, for example when the user will scroll the players list all the way down.
My User object has a List<Post>, each Post has a List<Tag>.
Now I would like to add a Tag item to the List<Tag>.
User:
data class User(
val id: String,
val name: String,
val posts: List<Post> = listOf()
)
Post:
data class Post(
val id: Int,
val name: String
val tags: List<Tags> = listOf()
)
Now I would like to update my MutableStateFlow containing the User object:
private val _userStateFlow: MutableStateFlow<User?> = MutableStateFlow(User())
val userStateFlow: StateFlow<User?> = _userStateFlow
To find the correct Post (containing the List<Tag> I want to update) I have the id of it (passedPostId)
val postsList = userStateFlow.value?.posts!!.toMutableList()
val tagsList = postsList.find { it.id == passedPostId }?.tags?.toMutableList()
tagsList.add(Tag("test"))
Now I got an updated tagsList but I need an updated postsList to pass it to my MutableStateFlow.
I am not sure how to do it.
This is how I update my MutableStateFlow:
_userStateFlow.update { it?.copy(posts = newPosts)
If I have the index of the post I want to update everything works fine, but In my Case I only can call .find because all i have is the id of the post.
val posts = userStateFlow.value?.posts!!.toMutableList()
posts[index] = posts[index].copy(tags = posts[index].tags.plus(Tag("test")
userStateFlow.value = userStateFlow.value?.copy(posts = posts)
You can create a function to add a tag to your immutable Post object - may as well stick it inside the class:
data class Post(
val id: Int,
val name: String
val tags: List<Tag> = listOf()
) {
// copy this object, replacing the tag list with a copy that has the new one appended
fun addTag(newTag: Tag) = copy(tags = tags + newTag)
}
Then you can do:
// alternative to putting it in the User class - whatever feels better
fun User.addTag(postId: Int, tag: Tag) =
// copy this User, including copying its list of Posts
// but any with a matching ID get the tag added
copy(posts = posts.map { post ->
if (post.id == postId) post.addTag(tag) else post
})
and update with
userStateFlow.value = userStateFlow.value!!.addTag(passedPostId, Tag("test"))
Try it here if you like
I fixed it by simply getting the index of the Post so I could use the code that worked before:
val postPosition = postsList.indexOfFirst {
it.id == passedPostId
}
postsList[postPosition] = postsList[postPosition].copy(tags = tagsList)
_userStateFlow.update { it?.copy(posts = postsList)
I feel pretty stupid to not have thought of that in the first place.
I am fairly new to Kotlin and I am trying to parse the JsonObject that I get from my HTTPRequest into data class. But I cannot get the Instance from the data class.
val jsonObjectRequest = JsonObjectRequest(Request.Method.GET, url, null,
Response.Listener { response ->
val text = "Response: %s".format(response.toString())
print(text)
},
Response.ErrorListener { error ->
// TODO: Handle error
}
)
#Throws(AuthFailureError::class)
#Override
fun getHeaders(): Map<String, String> {
var params = HashMap<String, String>()
if (params == null) params = HashMap()
val basicAuth = "Basic " + Base64.encodeToString("username:password".toByteArray(), Base64.NO_WRAP);
params["Authorization"] = basicAuth
//..add other headers
return params
}
TempJsonObject.instance?.addToRequestQueue(jsonObjectRequest)
Data Class
data class TempJsonObject(
val content: List<Content>) {
data class Content(
val kioskId: Int,
val location: Location,
val stats: Stats,
val settings: Settings,
val modules: String,
val visibility: String
) {
data class Stats(
val allLockers: Int,
val emptyLockers: Int,
val malfunctionLockers: Int,
val forCustomer: Int,
val forCourier: Int,
val bySize: BySize,
val incoming: Any
) {
data class BySize(
val XXSMALL: Xxsmall,
val XSMALL: Xsmall,
val SMALL: Small,
val MEDIUM: Medium,
val LARGE: Large,
val XLARGE: Xlarge
) {
data class Xxsmall(
val empty: Int,
val hasContent: Int
)
...
Is the way I am trying to do it the right approach and what am I doing wrong?
Looking at instance?.addToRequestQueue, it's really puzzling me. I don't know any Kotlin API like this for parsing JSONs. I thought that you use some library that adds such API, but you also wrote in the comment that the code doesn't compile because of lack of instance field. It looks like it exposes some singleton instance of the data class. Could you point me to any working example using such API, or any other source that you used?
If the above turns out to be incorrect, I would just use some Kotlin or Java library to parse the JSON into an instance of the data class. One option is the Kotlin library Klaxon: https://github.com/cbeust/klaxon. Quoting one of its examples that should fit your use case:
val result = Klaxon().parse<Person>(""" { "name": "John Smith", } """)
assert(result?.name == "John Smith")
assert(result.age == 23)
I'm new to kotlin so this maybe a very easy issue to resolve.
What I'm trying to do is filter the json response that I receive using Retrofit2 before I display the images in a grid with a RecyclerView.
instagram.com/explore/tags/{hashtag}/?__a=1&max_id= Using Retrofit2 I'm able to get the data response fine and also display the given url images in a RecyclerView.
I have not been successful in using the filter, map, loops and conditions to remove elements from the Arraylist. I do not understand these to the fullest extent but I have searched looking for solutions and those are what I came apon.
Interface
interface InstagramDataFetcher
{
#GET("tags/{tag}/?__a=1&max_id=")
fun getInstagramData(#Path("tag") hashtag: String) : Call <InstagramResponse>
}
Where I get my response from and also get StringIndexOutOfBoundsException
class InstagramFeedFragment : Fragment()
{
private fun onResponse()
{
val service = RestAPI.retrofitInstance?.create(InstagramDataFetcher::class.java)
val call = service?.getInstagramData("hashtag")
call?.enqueue(object : Callback<InstagramResponse>
{
override fun onFailure(call: Call<InstagramResponse>, t: Throwable)
{
Log.d("FEED", " $t")
}
override fun onResponse(
call: Call<InstagramResponse>, response: Response<InstagramResponse>
)
{
//for ((index, value) in data.withIndex())
if (response.isSuccessful)
{
var data: ArrayList<InstagramResponse.InstagramEdgesResponse>? = null
val body = response.body()
data = body!!.graphql.hashtag.edge_hashtag_to_media.edges
for ((index, value) in data.withIndex())
{
if(value.node.accessibility_caption[index].toString().contains("text") ||
value.node.accessibility_caption[index].toString().contains("person"))
{
data.drop(index)
}
}
recyclerView.adapter = InstagramGridAdapter(data, parentFragment!!.context!!)
}
}
})
}
}
This is my model class
data class InstagramResponse(val graphql: InstagramGraphqlResponse)
{
data class InstagramGraphqlResponse(val hashtag: InstagramHashtagResponse)
data class InstagramHashtagResponse(val edge_hashtag_to_media: InstagramHashtagToMediaResponse)
data class InstagramHashtagToMediaResponse(
val page_info: InstagramPageInfo,
val edges: ArrayList<InstagramEdgesResponse>
)
data class InstagramPageInfo(
val has_next_page: Boolean,
val end_cursor: String
)
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val __typename: String,
val shortcode: String,
val display_url: String,
val thumbnail_src: String,
val thumbnail_resources: ArrayList<InstagramThumbnailResourceResponse>,
val is_video: Boolean,
val accessibility_caption: String
)
data class InstagramThumbnailResourceResponse(
val src: String,
val config_width: Int,
val config_height: Int
)
}
Simply again, I want to just remove elements from the arraylist that match certain things what I don't want. For instance. the "is_video" value that comes from the json. I want to go through the arraylist and remove all elements that have "is_video" as true.
Thanks
If you asking how to filter the list then below is the demo.
You just need to use filter on your data which is an ArrayList. I've tried keeping the same structure for the models so that you can get a better understanding.
fun main() {
val first = InstagramNodeResponse(
title = "first",
is_video = true
)
val second = InstagramNodeResponse(
title = "second",
is_video = false
)
val list: ArrayList<InstagramEdgesResponse> = arrayListOf(
InstagramEdgesResponse(node = first),
InstagramEdgesResponse(node = second)
)
val itemsWithVideo = list.filter { it.node.is_video == true }
val itemsWithoutVideo = list.filter { it.node.is_video == false }
println(itemsWithVideo.map { it.node.title }) // [first]
println(itemsWithoutVideo.map { it.node.title }) // [second]
}
// Models
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val title: String,
val is_video: Boolean
)
I am working on updating the parsing of an API response that uses a Serialized Data Class to parse the JSON response. The serialization works perfectly fine right now, but the new data that I'm attempting to parse into data class is not fully reliant on data in the json. Here is what I mean by that:
The data class is Career, and the new data I need to parse is a set of skills and each have a rating. The json data is very simple and contains the skills as such:
{
// other career data
...
"mathematics_skill": 8,
"critical_thinking_skill": 6
... // the remaining skills
}
Using straight serialization, I would only be able to store the data as such:
data class Career(
// Other career data
#serializableName("mathematic_skill") val mathSkill: Int,
#serializableName("critical_thinking_skill") val mathSkill: Int,
// remaining skills
)
However, I would like to store all skills in an array variable of a custom skills class that not only contains the rating, but also the name of the skill and a color. Basically, when I access the skills data of a career, I would like to access it like such:
val careerMathSkill = career.skills[0]
val mathRating = careerMathSkill.rating
val mathColor = careerMathSkill.color
Is it possible to use the serialized data from the data class to add non-serialized data to the same data class? (Sorry for the weird wording, not sure how else to explain it)
EDIT: Here is what I have:
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
)
}
Here is what I am hoping to do in a way
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
// skills array that will need to be filled out based on the data I got in the json
var skills: List<Skill>
)
}
EDIT: The suggested solution
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
#SerializedName("math_skill") val mathSkill: Int
#SerializedName("other_skill") val mathSkill: Int
) {
var skills: List<Skill> = {
val mathSkill = Skill(name: "Math", rating: mathSkill, color: /**some color*/)
val otherSkill = Skill(name: "Other", rating: otherSkill, color: /**some color*/)
return listOf(mathSkill, otherSkill)
}
}
}
Yes, you can create a custom JsonDeserializer to modify how the JSON is parsed.
Here is a basic example of what that would look like.
class CareerDeserializer : JsonDeserializer<Career> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Career {
val obj = json.asJsonObject
// standard career data
val id = obj.get("id")?.asString
val name = obj.get("name").asString
// making a Skill object
val skill = Skill(
obj.get("mathematic_skill").asInt,
obj.get("critical_thinking_skill").asInt,
obj.get("swimming_skill").asInt
// etc
)
return Career(id, name, skill)
}
}
And make sure to register that within your GsonBuilder.
val gson = GsonBuilder()
.registerTypeAdapter(Career::class.java, CareerDeserializer())
.create()
Note, you'll also have to create a JsonSerializer if you want to go the other way too.
Edit:
However, if you're just looking to change the syntax of how you're accessing that data, you can do something like this.
data class Career(
// Other career data
val mathSkill: Int,
val thinkSkill: Int
// remaining skills
) {
val skills: List<Int>
get() = listOf(mathSkill, thinkSkill)
}
This would give you a skills list back whenever you needed it, and it would be created when you accessed it, so you won't have to worry about the data being out of sync. This would allow you to access your data as such.
career.skills[0] // get the math skill.
And you can take this another step further by adding a get operator to your Career class.
data class Career(
// Other career data
val mathSkill: Int,
val thinkSkill: Int
// remaining skills
) {
...
operator fun get(pos: Int) = skills[pos]
}
Now, you can simply do
career[0] // get the math skill.
Warning, this is dangerous because you're accessing an Array so you could get OutOfBoundsExceptions. Use constants to help you out.
Edit 2:
val skills = {
listOf(Skill("Math", mathSkill, /** some color */ ),
Skill("Other", otherSkill, /** some color */ ))
}