I'm using Kotlin objects to work with my Firebase Database models, as described in the guide. I have many fields that are stored as strings, but really are enums, so to be type-safe I have enum fields in the models, plus a string delegated property that returns the firebase stored value (as suggested in a question I asked some time ago). Now, these fields work if I get/set the string delegate in code, but firebase libs seem to skip them when converting to/from database's json format.
A simple example:
abstract class BaseModel {
#Exclude
open var path: String? = null // fails even if I delete this field!
}
class Weight() : BaseModel() {
constructor(v: Double, u: WeightUnit) : this() {
value = v
unitEnum = u
}
var value: Double = 0.0
#Exclude
var unitEnum: WeightUnit = WeightUnit.KG
var unit: String by EnumStringLowercaseConverter(WeightUnit::class.java).getDelegate(Weight::unitEnum)
}
[...]
val testWeight = Weight(7.0, "kg")
db.getReference("/valid/path/to/save/testWeight").setValue(testWeight)
.addOnSuccessListener { r -> Log.d(LOG_TAG, "set successful") }
.addOnFailureListener { e -> Log.e(LOG_TAG, "set error", e) }
The setValue always gives a Permission Denied error, but works, if I delete unitEnum field and make unit a normal String property.
It's similar for reading: Firebase gives no errors when getting a Weight object, but the weightUnit field is never set to anything else than the default. But, if I manually do weight.unit = "lb", the unitEnum field properly returns WeightUnit.LB.
I'm using firebase libs v10.0.1
Now, the questions:
What can I do to make the delegated properties work correctly with firebase? I can try a different approach to the delegated enum fields, as long as the points from my original question are satisfied (readable, concise and type-safe code).
is there any way to see how exactly do firebase libs convert objects to/from json? Or at least see the converted json? Maybe then I could tweak things myself. Unfortunately, everything firebase-related shows as /* compiled code */ in AndroidStudio.
UPDATE: I could of course add a toMap() method to each model, where I would construct a map containing all the properties needed in firebase, but it would be tiresome to do this for every model, and it solves the saving issue only, the enum fields still wouldn't be set when getting.
The delegated props are also skipped when serializing with GSON. So maybe is there a generic way to make the delegated properties look like regular fields?
Try this code, it should work.
#get:Exclude #set:Exclude
var unitEnum: WeightUnit = WeightUnit.KG
var unit: String
get() = unitEnum.name
set(v) { unitEnum = WeightUnit.valueOf(v) }
Related
While writing code for RecyclerView to get data I figured out there's a data class in Kotlin.
Following codes are taken from two different projects which are linked above.
#Serializable
data class MarsPhoto(
val id: String,
#SerialName(value = "img_src")
val imgSrc: String
)
class Contacts {
#SerializedName("country")
private val country:String? = null
fun getCountry():String?{
return country
}
}
I know that both classes are doing same job. So what does differentiate them? I also wonder in the MarsPhoto data class how they can get the id without declaring SerialName just the way they did for imgSrc. (I am just on the way to learning kotlin now, so I'm absolute beginner).
Basically for "data" class the compiler automatically derives the following members from all properties declared in the primary constructor:
equals()/hashCode() pair
toString() of the form "MarsPhoto(id=1, imgSrc=asdf)"
componentN() functions corresponding to the properties in their order of declaration.
copy()
You can read a lot more at enter link description here
On the SerializedName part of your question. if you are dealing with Gson lib by default it is using fields name as "SerializedName". And only if you want to use something different then field name, you can use SerializedName annotation and pass your custom value there. But usually, everybody just writes #SerializedName() with duplication of field names as value for every field.
It's a good idea if you are receiving and Serializing data from server from Json. Because Backend developers can use a bad keys in response, which you don't want to use in your code, so #SerializedName will be the only place where you will have to see this key, and you can name your fields however you like.
#Serializable used to mark class as serializable to disk or like into a file( alternative is Parcel able in android) special useful in case of process death or configuration changes and #SerializedName("country") used for json parsing when u receive the response from server
You get the id without #SerializedName because the JSON property field is the same as your variable name, but imgSrc and img_src is not. Still, even if they are the same, you should always use #SerializedName, because your variable names could be converted to random letters during code optimization, and obfuscation.
I'm trying to implement JSON parsing in my Android application written in Kotlin using com.squareup.moshi (v1.10.0).
Within the JSON file there are some properties that are not interesting in my case. Let's say, I only need the position to be able to mark the place on a map and the JSON looks like this:
"location":{
"address":{
"country":"..."
},
"position":{
"lat":47.469866,
"lon":19.062435
}
}
If I'm right, the data class in Kotlin should look like this if I'd like to parse that JSON:
#Parcelize
data class Location(
val address: Address,
val position: Position
): Parcelable
#Parcelize
data class Address(
val country: String
): Parcelable
#Parcelize
data class Position(
val lat: Double,
val lon: Double
): Parcelable
In Moshi's documentation I could find the transient keyword to skip values which in Kotlin works as an annotation (#Transient). As the documentation says:
Transient fields are omitted when writing JSON. When reading JSON, the field is skipped even if the JSON contains a value for the field. Instead it will get a default value.
Does it mean that if I don't want to have the address object, I should use the following code?
#Parcelize
data class Location(
#Transient val address: Address? = null,
val position: Position
): Parcelable
Also, what about in general terms? What if I have huge list of properties within a JSON object but I know I only need the 'position' object? Do I still have to create null values to parse the JSON file field-by-field?
I think you are looking for something similar to GSON's #Expose annotations, wherein all model fields are excluded from parsing except those annotated.
This functionality is currently not available in Moshi, so your current implementation using the #Transient annotation seems to be the most optimal solution. (See Moshi issues conversation here.)
Extra food for thought:
You may also wish to use #IgnoredOnParcel on your transient fields since you are implementing the parcelable interface. (Have a look here for some implementation pointers.)
Alternatively you could separate your data model into 2 models - one for use in your app and one which reflects the server (JSON) schema (just as you have done above). The main data model for your app (which could implement parcelable) would contain only the fields you use (for example, the position field). When you parse your data, you then convert that data to your primary data model using some simple adapter. (This is often good practice anyhow, since server-side schemas are inherent to change. This way, any changes in the JSON schema wouldn't end having any ripple effect throughout your code.)
https://github.com/square/moshi#omit-fields-with-transient
Omit fields with transient
Some models declare fields that shouldn’t be included in JSON. For example, suppose our blackjack hand has a total field with the sum of the cards:
public final class BlackjackHand {
private int total;
...
}
By default, all fields are emitted when encoding JSON, and all fields are accepted when decoding JSON. Prevent a field from being included by adding Java’s transient keyword:
public final class BlackjackHand {
private transient int total;
...
}
Transient fields are omitted when writing JSON. When reading JSON, the field is skipped even if the JSON contains a value for the field. Instead it will get a default value.
I wanna duplicate realm object and then change the second, without reassigning all the keys. How can I do this? RealmObject does not have .copy() or .clone() methods.
// Money is not data class and has ∞ fields which will be boring to re-assign
val money = Money()
money.amount = 1000
...
val anotherMoney = money
anotherMoney.amount = 500
println(money.amount) // prints 500
can you please provide more context and appropriate information as I do not see and array's in your code statement. Thank you.
EDIT
Because Money is not a data class, you do not have the auto-generated copy() function available, that leaves you with two options:
Create an custom copy() function in the Money class. This could be mundane if there are huge amount of fields in the class.
Use 3rd party libraries, in which case you'll add external dependency to your RealmObject.
What I will suggest is a no brainer: Try to convert your Money.class to a Data class. You will get auto generated functions and idiomatically it will work as RealmObjects should be key-values pairs.
EDIT
You can use GSON library's serialization/deserialization and hack your way to solve your problem (although this is a hacky way but will do its job):
fun clone(): Money {
val stringMoney = Gson().toJson(this, Money::class.java)
return Gson().fromJson<Money>(stringMoney, Money::class.java)
}
usage:
val originalMoney = Money()
val moneyClone = originalMoney.clone()
I am having trouble in understanding the way #Parcelize working in Kotlin. According to documentations
only the primary constructor properties will be serialized.
But when I serialize and deserialize classes with empty primary contractors, it is still serializing and deserializing all the fields. For example, below class
#Parcelize
class Node(): Parcelable {
var field: String? = null
}
As primary contractor doesn't have any field, according to documentations I should have field = null always after ser/des. But whenever I run below codes
val before = Node()
before.field = "someField"
val bundle = Bundle().apply{ putParcelable("someKey", before) }
val after = bundle.getParcelable<Node>("someKey")
field is successfully serialized and deserialized and will have value of someField.
Am I missing something or did Parcelize got updated but they didn't update documentation?
By the way if I leave Node declaration as above, Android Studio gives me warning that field will not be serialized into Parcel. But it is.
You should define it in the constructor itself and it will work just fine.
#Parcelize
class Node( var field: String? = null) : Parcelable
And to use empty constructors in kotlin you can add this in app gradle file.
apply plugin: 'kotlin-noarg'
With this you can use classes with empty constructors.
I hope this helps.
I have simple data classes. I mean, they are data classes logically, but not data class, because I need inheritance and other constructors. They only have fields (of basic types Int?, String?, or List<String>?, etc), and constructors.
I need to pass them (all of their fields need to be passed) from Activity to Activity, so I need to make them parcellisable (or is there a better way?). I first created them as data class and just added #Parcelize. Even though there was a warning and red line that said "CREATOR" or something, I could ignore them and the app worked as intended.
But, now for the reasons above, I changed them to normal classes, and suddenly there is a compilation error.
Error:java.util.NoSuchElementException: Collection contains no element matching the predicate.
at org.jetbrains.kotlin.android.parcel.ParcelableCodegenExtension.getPropertiesToSerialize(ParcelableCodegenExtension.kt:374)
....too long...
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Error:Execution failed for task ':app:kaptGenerateStubsDebugKotlin'. Internal compiler error. See log for more details
How can I solve this problem? Should I use data class? But I need to parse a JSON object to create them. The classes look like these (not actual classes, but simplified for illustration purposes). Is there a better way than implementing that boring, bulky parcellable code by hand?
#Parcelize
open class Dog():Parcelable
{
var someField1;
var someField2;
constructor(data:JSON):this()
{
//parse data to set the fields.
}
}
#Parcelize
class Doge:Dog
{
var someField3;
var someField4;
constructor():super(); //I do not use it, but for parcellable
constructor(data:JSON):super(data)
{
//parse data to set the fields.
}
}
PS. I had to switch to PaperParcel. It was very similar to Kotlin's, but it did not require a primary constructor. It only required the same thing to be any constructor, so I could just create a secondary constructor with the same argument names as those of fields, and it worked. Although, I wonder why the CREATOR could not be created automatically.
For example,
#PaperParcel
class Doge:Dog
{
var someField3;
var someField4;
//Needed only for PaperParcel
constructor(someField3, someField4)
{
this.someField3 = someField3;
this.someField4 = someField4;
}
companion object
{
#JvmField val CREATOR = PaperParcelDoge.CREATOR
}
//end of PaperParcel stuff.
constructor(data:JSON):super(data)
{
//parse data to set the fields.
}
}
As stated here, your properties should be declared inside your primary constructor.
Parcelable support
Android Extensions plugin now includes an automatic
Parcelable implementation generator. Declare the serialized properties
in a primary constructor and add a #Parcelize annotation, and
writeToParcel()/createFromParcel() methods will be created
automatically:
#Parcelize
class User(val firstName: String, val lastName: String) : Parcelable