I have following project in Github : https://github.com/alirezaeiii/TMDb-Paging
I have a generic solution in my classes using TmdbItem interface, such as :
abstract class BaseFragment<T : TmdbItem> : BaseNavTypeFragment() {
protected abstract val viewModel: BaseViewModel<T>
protected lateinit var tmdbAdapter: TmdbAdapter<T>
}
TmdbItem interface and the class implementations are as follow :
interface TmdbItem : Parcelable {
val id : Int
val overview: String
val releaseDate: String?
val posterPath: String?
val backdropPath: String?
val name: String
val voteAverage: Double
}
#Parcelize
data class Movie(
override val id: Int,
override val overview: String,
#SerializedName("release_date")
override val releaseDate: String?,
#SerializedName("poster_path")
override val posterPath: String?,
#SerializedName("backdrop_path")
override val backdropPath: String?,
#SerializedName("title")
override val name: String,
#SerializedName("vote_average")
override val voteAverage: Double) : TmdbItem
#Parcelize
data class TVShow(
override val id: Int,
override val overview: String,
#SerializedName("first_air_date")
override val releaseDate: String?,
#SerializedName("poster_path")
override val posterPath: String?,
#SerializedName("backdrop_path")
override val backdropPath: String?,
override val name: String,
#SerializedName("vote_average")
override val voteAverage: Double) : TmdbItem
As you see I have to use #SerializedName in both classes even if the value is the same such as poster_path and backdrop_path. Is there anyway that I could write them in one place such as a base class instead of both class implementations?
That is probably not easily possible with Gson. The closest you could get to avoid duplicating the name is to define the property names somewhere as const val and then refer to them in #SerializedName, e.g. #SerializedName(POSTER_PATH_NAME).
Another solution which is however quite complicated and error-prone would be:
Place #SerializedName on the getter functions of the interface, e.g. #get:SerializedName("..."). This is possible because #SerializedName supports METHOD as target, but Gson only considers it on fields by default. Then write a custom FieldNamingStrategy which does the following:
Check if the field has a #SerializedName annotation, in that case return its value
Otherwise, obtain the Kotlin property (if any) for the Java field and obtain its getter method:
field.kotlinProperty?.javaGetter
Check if the getter has a #SerializedName, in that case return its value
Otherwise, go through the superclasses and then superinterfaces to check if any of them declare the getter (which is overridden) and have a #SerializedName
This is how it might work in theory; I have not tried to fully implement this yet. And as you can see the logic would be quite complex, so I am not sure if that would really be worth it. Also keep in mind that Gson directly reads field values and does not call getters during serialization. Therefore using #SerializedName on getters (especially if they have a custom implementation) might cause confusion.
Otherwise you might have to look for external Gson extensions which support getter methods, or other JSON libraries.
Related
We had a RuntimeException while we put a safeArgs into the navigation graph and this crash didn't give us any more guide to fix it.
In the first impression, we were following this to ensure that those classes were Parcelize or not.
Therefore, those classes were parceled correctly and everything seems right.
After many searches and investigations into our codes, I found that SafeArgs class exists in a sealed-class file.
Considering to this point that we can't allocate the parceled annotation to the sealed classes, we've decided to move our class outside of that sealed .kt file.
Although, I found the main reason for that runtime crash was this cause.
Also, I've provided the wrong and correct cases below, We hope this will be helpful to others:
We've moved the UserFavorite and its subclass, outside of the Response.kt file,
sealed class Response {
.
.
#Parcelize
data class UserFavorite(
#SerializedName("title") val title: String,
#SerializedName("itemType") val itemType: String,
#SerializedName("emptyIcon") val emptyIcon: String,
#SerializedName("_texts") val texts: UserFavoriteTexts
) : Response(), Parcelable
#Parcelize
data class UserFavoriteTexts(
#SerializedName("hintMessage") val hintMessage: String,
#SerializedName("add") val add: String,
#SerializedName("remove") val remove: String,
#SerializedName("edit") val edit: String
): Parcelable
.
.
}
into an independent file for that: UserFavorite.kt
#Parcelize
data class UserFavorite(
#SerializedName("title") val title: String,
#SerializedName("itemType") val itemType: String,
#SerializedName("emptyIcon") val emptyIcon: String,
#SerializedName("_texts") val texts: UserFavoriteTexts
) : Response(), Parcelable
#Parcelize
data class UserFavoriteTexts(
#SerializedName("hintMessage") val hintMessage: String,
#SerializedName("add") val add: String,
#SerializedName("remove") val remove: String,
#SerializedName("edit") val edit: String
): Parcelable
and, Respons.kt
sealed class Response {
.
.
.
.
}
My problem is that i want one extension function that works only on data classes.
for example lets say I have multiple data classes.
data class Person(
val name: String,
val age: Int
)
data class Car(
val color: Color,
val brand: String
)
and so on.
Now i want to create an extension function that is only extended for the data classes, not like this were i have to create extension function for each class.
for example my extension functions:
fun Person.convertToJson() : String {
return Gson().toJson(this)
}
fun Car.convertToJson() : String {
return Gson().toJson(this)
}
I want only one function that does the magic, also i don't want to use generic since it will be available to all objects. This is the generic function example:
fun <T> T.convertToJson() : String {
return Gson().toJson(this)
}
I want something equivalent to the generic function that will only work for data classes type.
There is no feature in the language to define an extension function on all data classes. if you can modify your data classes to implement an interface then you can use a marker interface for this as
/* define a marker interface and have all your data classes implement it */
interface Jsonable
data class Person(val name: String, val age: Int): Jsonable
data class Car(val color: Color, val brand: String): Jsonable
Now you can define the extension function on the interface as
fun Jsonable.convertToJson(): String = Gson().toJson(this)
and you can use it as
Person("A", 50).convertToJson()
I think you couldn't make an extension function to the data class type but you can overcome this by making an interface that your data class can extend and then make an extension for any class that implements the defined interface
interface IHuman
data class Person(val name: String, val age: Int): IHuman
data class Student(val name: String, val schoolName: String): IHuman
inline fun <reified T: IHuman> T.doSomething(){
println("Which one call me: ${T::class.java.simpleName}")
}
fun main(){
Person(name = "Mohamed", age = 28).doSomething()
Student(name = "Ahmed", schoolName = "MySchool").doSomething()
}
I hope below code will help to solve your problem without any Interface
ExtensionFunction:
inline fun <reified T : Any> T.objectToString(): String = Gson().toJson(this, T::class.java)
inline fun <reified T : Any> String.stringToObject(): T = Gson().fromJson(this, T::class.java)
Call to make:
Object.objectToString() // This will return string
String.stringToObject<Object>() // This will return Object
I am new to Kotlin and learning it, I am having a simple data class
data class Country{
#SerializedName("name")
val countryName: String?,
#SerializedName("capital")
val capital: String?,
#SerializedName("flagPNG")
val flag: String?
}
Errors I am facing:
]
Your data class should look like this:
data class Country(
#SerializedName("name")
val countryName: String?,
#SerializedName("capital")
val capital: String?,
#SerializedName("flagPNG")
val flag: String?
)
The difference is, like mentioned in the comments: I used normal parentheses around the fields while you used curly braces
Data Class in Kotiln must have a variable/value parameter in it's constructor declaration.
Official doc states that :
To ensure consistency and meaningful behavior of the generated code,
data classes have to fulfill the following requirements:
The primary constructor needs to have at least one parameter;
All primary constructor parameters need to be marked as val or var;
Data classes cannot be abstract, open, sealed or inner;
(before 1.1) Data classes may only implement interfaces.
So, your data class should be something similar like below :
data class Foo(
val bar: Any
)
Note: In Kotlin, you can declare class constructor just by placing '()' following by class name to make it as primary constructor.
You class declaration should be something like below :
data class Country(
#SerializedName("name")
val countryName: String?,
#SerializedName("capital")
val capital: String?,
#SerializedName("flagPNG")
val flag: String?
)
Refer here for more info.
I have a problem with room.
I'm using retrofit with Gson converter for the rest api, and I'd like to share the pojos with room. In general it works, but in some cases I need to ignore some fields, because I have list of objects. I tried to use the #Ignore annotation, but using it the build process fails with the following errors:
error: Entities and Pojos must have a usable public constructor. You
can have an empty constructor or a constructor whose parameters match
the fields (by name and type). public final class Service {
^ error: Cannot find setter for field.
private final java.lang.String id = null;
^ error: Cannot find setter for field.
private final java.lang.String name = null;
^ error: Cannot find setter for field.
private final java.lang.String description = null;
So, using this class, everything works:
#Entity(tableName = "services")
data class Service(
#PrimaryKey val id: String,
val name: String,
val description: String,
val parentId: String?
)
With this, fails:
#Entity(tableName = "services")
data class Service(
#PrimaryKey val id: String,
val name: String,
val description: String,
val parentId: String?,
#Ignore val test: String
)
I'm using this version of room:
implementation 'androidx.room:room-runtime:2.1.0-alpha06'
kapt 'androidx.room:room-compiler:2.1.0-alpha06'
I know that the problem could be fixed using var instead of val and adding a secondary constructor, but I don't want to do that, I prefer to preserve the immutable state of my fields.
Is it a bug of the ignore annotation? Why without it everything works?
Any help is appreciated :)
In your second example Service will be translated to a Java class which has a single constructor with four parameters. Room sees the #Ignore annotations and knows that it needs to bind 3 fields and so it needs a constructor with 3 parameters matching those fields types. As it doesn't find such a constructor(or the default one) it fails.
Try to make the last property optional and use the #JvmOverloads annotation on the constructor:
#Entity(tableName = "services")
data class Service #JvmOverloads constructor(
#PrimaryKey val id: String,
val name: String,
val description: String,
val parentId: String?,
#Ignore val test: String? = null
)
This will make the Kotlin compiler to generate a 3 parameter constructor and make Room happy again.
I'm using retrofit with Gson converter for the rest api, and I'd like
to share the pojos with room.
You should probably avoid doing this and use two sets of classes to model the API response and the database data. Even in this initial moment you need to make some "hacks" to make everything work, if in the future the API or the database changes you'll need to make more complex changes in more places.
In fact, you can retain immutability with a single class and without the #JvmOverloads trick. You simply need the only visible (to Room) constructor to match the fields of the table. For your example, it would be:
#Entity(tableName = "services")
data class Service #Ignore constructor(
#PrimaryKey val id: String,
val name: String,
val description: String,
val parentId: String?,
#Ignore val test: String
) {
constructor(id: String, name: String, description: String, parentId: String?) :
this(id, name, description, parentId, /* some default value for test */)
}
I am using Room and Kotlin data class. Such as,
#Entity(tableName = "Person")
#Parcelize
class Test(#ColumnInfo(name = "name") var name:String) : Parcelable{
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "ID")
var id: Long? = null
}
I can create the instance using the constructor and insert the data. I am also getting a warning "property would not be serialized into a 'parcel'". When I was trying to send the object through a bundle, the id is missing, which is expected as the warning says so. How can I add that member ID in the parcel? I am not keeping the ID in the constructor as I want them to be generated automatically.
You can find this information with the documentation:
#Parcelize requires all serialized properties to be declared in the primary constructor. Android Extensions will issue a warning on each property with a backing field declared in the class body. Also, #Parcelize can't be applied if some of the primary constructor parameters are not properties.
If your class requires more advanced serialization logic, you can write it inside a companion class:
#Parcelize
data class User(val firstName: String, val lastName: String, val age: Int) : Parcelable {
private companion object : Parceler<User> {
override fun User.write(parcel: Parcel, flags: Int) {
// Custom write implementation
}
override fun create(parcel: Parcel): User {
// Custom read implementation
}
}
}