Problem with #Ignore and immutable fields - android

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

Related

Model class implementations for a generic solution

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.

RuntimeException: Exception inflating kotlin:navigation/nav_graph

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 {
.
.
.
.
}

Why does the author add #field:SerializedName annotation to a field in Room database?

I'm learning Room with a sample code at https://github.com/googlecodelabs/android-paging
The Code A is to defind a #Entity of a table, you can see it.
Normally it add the #ColumnInfo annotation to a field, such as #ColumnInfo(name = "first_name") val firstName: String?, ... .
Why does the author add #field:SerializedName annotation to a field in Room database?
Code A
#Entity(tableName = "repos")
data class Repo(
#PrimaryKey #field:SerializedName("id") val id: Long,
#field:SerializedName("name") val name: String,
#field:SerializedName("full_name") val fullName: String,
#field:SerializedName("description") val description: String?,
#field:SerializedName("html_url") val url: String,
#field:SerializedName("stargazers_count") val stars: Int,
#field:SerializedName("forks_count") val forks: Int,
#field:SerializedName("language") val language: String?
)
The #SerializedName annotation is part of Gson and is used to define the names of these properties in Json.
The #ColumnInfo annotation is part of Room and is used to define the column names of these properties in SQLite.
If neither of these annotation is defined, the property/field name is used as a name for the Json property or column.
Why does the author add #field:SerializedName annotation to a field in Room database?
Because the Room entity is reused as a Gson DTO.

How to use a simple data class in kotlin

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.

How to parcelise member variable other than constructor in data class while using #Parcelize

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
}
}
}

Categories

Resources