Ignore elements when certain attribute is null when mapping objects - android

I am using the following function to map a network object to a domain one.
Mapping function
fun getLocationLocalModel(input: LocationSearchResponse): List<Location> {
return input.locations.map { location ->
return#map location.bbox?.let {
Location(
name = location.name,
countryCode = location.countryCode,
north = location.bbox.north,
south = location.bbox.south,
west = location.bbox.west,
east = location.bbox.east
)
}
}.filterNotNull()
}
Network DTOs
data class LocationSearchResponse(
#SerializedName("geonames")
val locations: List<Location>
)
data class Location(val bbox: Bbox?, val countryCode: String, val countryName: String,
val geonameId: Int, val lat: String, val lng: String, val name: String)
Domain Model
#Parcelize
data class Location(val name: String, val countryCode: String, val north: Double, val south: Double, val east: Double, val west: Double) : Parcelable
What I want is to ignore the objects where bbox is null so they are not added to the resulting list of locations.
This function works, but there must be a better/simpler way to do this.
Any help would be appreciated.

As Animesh mentioned in the comments, simply change your statement to mapNotNull -- there's no need to use the return# syntax there:
return input.locations.mapNotNull { loc ->
loc.bbox?.let { bbox ->
Location(loc.name, loc.countryCode, bbox.north, bbox.south, bbox.west, bbox.east)
}
}
Alternatively you could filter first, then use !! to dereference:
return input.locations
.filter { it.bbox != null }
.map { loc->
val bbox = loc.bbox!!
Location(loc.name, loc.countryCode, bbox.north, bbox.south, bbox.west, bbox.east)
}
Personally the former seems more readable to me.

Related

How to get Array type data in Firebase in Kotlin?

I have this type of array in firebase but how to fetch it and use in kotlin
I was able to get as String but how to get its as a data class Like this
data class Comment(
val uid: String,
val comment: String,
val stamp: Timestamp
)
and here's the code of getting string
var text by remember { mutableStateOf("loading...") }
FirebaseFirestore.getInstance().collection("MyApp")
.document("Something").get().addOnSuccessListener {
text = it.get("Comments").toString()
}
Firebase has a toObject method that can be used to turn your document into a custom object.
db.collection("Comments")
.get()
.addOnSuccessListener { documents ->
for (document in documents) {
val comment = document.toObject<Comment>()
}
}
The Comment data class should also define default values. So, it should be like...
data class Comment(
val uid: String = "",
val comment: String = "",
#ServerTimeStamp val stamp: Date? = null
)
I got ArrayLists with HashMaps represents my entities entities just using this:
val cleanAnswers: List<Answer> = (document.get(FIELD_ANSWERS)
as ArrayList<HashMap<String, Any>>).map {
Answer(
it[FIELD_VARIANT] as String,
it[FIELD_IS_CORRECT] as Boolean
)
}
My entity:
class Answer(val answer: String,
val isCorrect: Boolean) : Serializable

Compare two array lists of obejcts with different ids

What is the most optimized way to compare two ArrayLists of different object type? Let's say I have an Array Lists of Remedies and Adherences and them being:
ObjectA
data class ObjectA(val data: List<Objecta> = emptyList()) {
data class Objecta(
val _id: String,
val remedy_id: String,
val patient_id: String,
val date_created: Long,
val is_ongoing: Boolean?,
val start_date: Long,
val medicine_id: String,
val instruction: String,
val medicine_name: String,
val medicine_type: String,
val end_date: Long,
val repeat_cycle: Int,
val allow_edit: Boolean,
//val schedule: List<Schedule>?,
val is_current: Boolean,
//val medicine: Medicine?,
val price: Float
)
}
ObjectB
data class ObjectB(val data: List<Objectb> = emptyList()) {
data class Objectb(
val _id: String,
val adherence_id: String,
val patient_id: String,
val remedy_id: String,
val alarm_time: String,
val action: String,
val action_time: String,
val dose_discrete: String,
val dose_quantity: Int,
val note: String
)
}
How can I compare both those two array lists "remedyList" and "adherencesList" where remedy_id is the same and remove items in adherencesList when remedy_id is not present in remedyList.
This should work:
Prepare the input
data class Rdata (val r_id:String)
data class Adata (val a_id:String,val r_id:String)
val rList = (1..10).map { Rdata("$it")}
val aList = (5..20).map { Adata("foo", "$it")}
Do the filtering:
val rIds = rList.map { it.r_id }.toSet()
val resultList = aList.filter { it.r_id in rIds}
After this, the resultList contains objects with r_id 5-10. (that is, 11-20 have been removed)
Try this code:
val allRemedies = remedies.flatMap { it.data }.associateBy { it.remedy_id }
val allAdherences = adherences.flatMap { it.data }.associateBy { it.remedy_id }
val allPairs = allAdherences.mapValues { allRemedies[it.key] to it.value }
Playground example
remedies is the List<Remedies> and adherences is the List<Adherences>
First we flatten the List<Remedies> to List<Remedy> and associate each Remedy to its remedy_id. allRemedies is Map<String, Remedy>.
Similarly for the adherences, we prepare a Map<String, Adherence>
Then we map each value of allAdherences map to a pair of Remedy? and Adherence. If there is no Remedy for that remedy_id it will be null.
If all you care about is filtering Adherences that don't have a matching Remedy, you can create a Set of remedy_ids from the Remedy List, and use that to efficiently check if they exist and filter on that.
val validRemedyIds: Set<String> = remedies.data.mapTo(mutableSetOf(), Remedy::remedy_id)
val filteredAdherences = Adherences(adherences.data.filter { it.remedy_id in validRemedyIds })
If you need to match them up later, you should use a map instead of set:
val remediesById = remedies.data.associateBy(Remedy::remedy_id)
And then there are several ways you can combine to compare. One example:
val adherencesToRemedies: List<Pair<Adherence, Remedy>> =
adherences.data.mapNotNull { remediesById[it.remedy_id]?.run { it to this } }

how to flatten nested JSON into single class using retrofit and gson converter?

I have a nested JSON like this from Server, as you can see there is a nested data in location
{
"id": "18941862",
"name": "Pizza Maru",
"url": "https://www.zomato.com/jakarta/pizza-maru-1-thamrin?utm_source=api_basic_user&utm_medium=api&utm_campaign=v2.1",
"location": {
"address": "Grand Indonesia Mall, East Mall, Lantai 3A, Jl. M.H. Thamrin No. 1, Thamrin, Jakarta",
"locality": "Grand Indonesia Mall, Thamrin",
"city": "Jakarta",
"city_id": 74,
"latitude": "-6.1954467635",
"longitude": "106.8216102943",
"zipcode": "",
"country_id": 94,
"locality_verbose": "Grand Indonesia Mall, Thamrin, Jakarta"
},
"currency": "IDR"
}
I am using retrofit and using gson converter. usually I need to make 2 data class for something like this to map JSON into POJO. so I need to make Restaurant class and also Location class, but I need to flatten that json object into single Restaurant class, like this
data class Restaurant : {
var id: String
var name: String
var url: String
var city: String
var latitude: Double
var longitude: Double
var zipcode: String
var currency: String
}
how to do that if I am using retrofit and gson converter ?
java or kotlin are ok
This solution is a silver bullet for this problem and cannot be appreciated enough.
Take this Kotlin file first:
/**
* credits to https://github.com/Tishka17/gson-flatten for inspiration
* Author: A$CE
*/
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD)
annotation class Flatten(val path: String)
class FlattenTypeAdapterFactory(
private val pathDelimiter: String = "."
): TypeAdapterFactory {
override fun <T: Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
val delegateAdapter = gson.getDelegateAdapter(this, type)
val defaultAdapter = gson.getAdapter(JsonElement::class.java)
val flattenedFieldsCache = buildFlattenedFieldsCache(type.rawType)
return object: TypeAdapter<T>() {
#Throws(IOException::class)
override fun read(reader: JsonReader): T {
// if this class has no flattened fields, parse it with regular adapter
if(flattenedFieldsCache.isEmpty())
return delegateAdapter.read(reader)
// read the whole json string into a jsonElement
val rootElement = defaultAdapter.read(reader)
// if not a json object (array, string, number, etc.), parse it
if(!rootElement.isJsonObject)
return delegateAdapter.fromJsonTree(rootElement)
// it's a json object of type T, let's deal with it
val root = rootElement.asJsonObject
// parse each field
for(field in flattenedFieldsCache) {
var element: JsonElement? = root
// dive down the path to find the right element
for(node in field.path) {
// can't dive down null elements, break
if(element == null) break
// reassign element to next node down
element = when {
element.isJsonObject -> element.asJsonObject[node]
element.isJsonArray -> try {
element.asJsonArray[node.toInt()]
} catch(e: Exception) { // NumberFormatException | IndexOutOfBoundsException
null
}
else -> null
}
}
// lift deep element to root element level
root.add(field.name, element)
// this keeps nested element un-removed (i suppose for speed)
}
// now parse flattened json
return delegateAdapter.fromJsonTree(root)
}
override fun write(out: JsonWriter, value: T) {
throw UnsupportedOperationException()
}
}.nullSafe()
}
// build a cache for flattened fields's paths and names (reflection happens only here)
private fun buildFlattenedFieldsCache(root: Class<*>): Array<FlattenedField> {
// get all flattened fields of this class
var clazz: Class<*>? = root
val flattenedFields = ArrayList<Field>()
while(clazz != null) {
clazz.declaredFields.filterTo(flattenedFields) {
it.isAnnotationPresent(Flatten::class.java)
}
clazz = clazz.superclass
}
if(flattenedFields.isEmpty()) {
return emptyArray()
}
val delimiter = pathDelimiter
return Array(flattenedFields.size) { i ->
val ff = flattenedFields[i]
val a = ff.getAnnotation(Flatten::class.java)!!
val nodes = a.path.split(delimiter)
.filterNot { it.isEmpty() } // ignore multiple or trailing dots
.toTypedArray()
FlattenedField(ff.name, nodes)
}
}
private class FlattenedField(val name: String, val path: Array<String>)
}
Then add it to Gson like this:
val gson = GsonBuilder()
.registerTypeAdapterFactory(FlattenTypeAdapterFactory())
.create()
Retrofit.Builder()
.baseUrl(baseUrl)
...
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
Using your example, you can get the pojo parsed like this:
// prefer constructor properties
// prefer val over var
// prefer added #SerializedName annotation even to same-name properties:
// to future proof and for easier proguard rule config
data class Restaurant(
#SerializedName("id") val id: String,
#SerializedName("name") val name: String,
#SerializedName("url") val url: String,
#Flatten("location.city") val city: String,
#Flatten("location.latitude") val latitude: Double,
#Flatten("location.longitude") val longitude: Double,
#Flatten("location.zipcode") val zipcode: String,
#SerializedName("currency") var currency: String
)
You can even write a path down an array, e.g #Flatten("friends.0.name") = get first friend's name.
For more info, visit this repo
Note, however, that I stripped down the TypeAdapter to only read/consume json objects. You can implement write() if you want to use it to write json too.
You are welcome.

Moshi crashing at float values

I have this model class
data class RvRange(val low: Int?, val high: Int?)
My JSON response is
{"low":60799.999999999985,"high":168800.00000000003}
I want Moshi to automatically convert those float/Double values to Int but it crashes.
is there any solution to this problem?
One possible solution would be creating your own adapter that internally converts Double to Int. Example:
object RvRangeAdapter : JsonAdapter<RvRange>() {
#FromJson
override fun fromJson(reader: JsonReader): RvRange? {
reader.beginObject()
var low: Int? = null
var high: Int? = null
while (reader.hasNext()) {
when (reader.nextName()) {
"low" -> low = reader.nextDouble().roundToInt() // or any other kind of rounding logic
"high" -> high = reader.nextDouble().roundToInt()
else -> reader.skipValue() // let's ignore unknown fields
}
}
reader.endObject()
return RvRange(low, high)
}
#ToJson
override fun toJson(writer: JsonWriter, value: RvRange?) {
writer.beginObject()
.name("low").value(value?.low)
.name("high").value(value?.high)
.endObject()
}
}
Once that's done, you'll need to register that adapter so that Moshi knows it can be used:
val moshi: Moshi = Moshi.Builder()
.add(RvRange::class.java, RvRangeAdapter)
// any other configuration here
.build()
When parsing the JSON you provided, the deserialised value will be:
RvRange(low=60800, high=168800)
Note that it doesn't really work with null values, so it would need some more tweaks if null is a valid value for "high" and/or "low".

Serialized Data Class combined with built-in modifications

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 */ ))
}

Categories

Resources