Subclassing Room Entities - android

Is there a way to subclass entities in Room?
I have an car entity
#Entity(tableName = "cars")
data class Car(
#PrimaryKey #ColumnInfo(name = "vin") val vin: String,
val manufacturer: String,
val model: String,
val color: String,
val modelYear: Int,
val trim: String = ""
) {
override fun toString() = String.format("%s %s %s %s", modelYear, manufacturer, model, trim)
}
But I want to move the manufacturer, model, and modelYear to a Vehicle entity and have the Car inherit from it.
I tried creating a Vehicle entity with those fields and use data class Car : Vehicle, but does not compile. The error is This type is final and cannot be inherited

In kotlin, all classes are final by default.
From Docs
The open annotation on a class is the opposite of Java's final: it
allows others to inherit from this class. By default, all classes in
Kotlin are final, which corresponds to Effective Java, 3rd Edition,
Item 19: Design and document for inheritance or else prohibit it.
so you need to add open keyword so use
open class Vehicle(..){...}
then
data class Car(...): Vehicle(..){}
Side Note: if you trying to inherit data classes then simply you cannot inherit data class in Kotlin because method like copy in data class are final which cannot be overridden by the child class (which in this case is data class so it will do it automatically) and it's hard to implement other methods like equals when class hierarchy grows exponentially with different data members though you can avoid all this collision by making non-data parent class or abstract class

Related

Saving complex Class data in Room - "Cannot figure out how to save this field into database. You can consider adding a type converter for it"

I started to learn Room and I'm facing an issue:
Given two classes, one is a Car, and the other one is an Engine iside a Car.
#Entity
class Car{
#PrimaryKey
var id = 0
var name: String? = null
var engine: Engine? = null
}
...
#Entity
class Engine{
#PrimaryKey
var id = 0
var manufacturer: String? = null
}
I also have these classes initalized to tables in my AppDatabase class.
#Database(entities = [Car::class, Engine::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
//...
}
The problem is whenever I simply want to run the project I get the following error message which points to the Car's engine field:
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
Is there no simple way for this? I'm looking for something which saves my data with the least amount of code, like Firestore which do all the work with simple annotations.
Thanks in advance.
AS a car would only have a single engine and that you have a table for the engine as well as a table for the car. Then you have a 1 to many relationship. That is a car can have an engine but the same engine can be used by many cars.
So instead of trying to embed the engine within the car you make a relationship, the car (the child) referencing the engine (the parent).
This is as simple as changing the Car to be:-
#Entity
class Car{
#PrimaryKey
var id = 0
var name: String? = null
var engine: Int? = null
}
An alternative, that would not need the relationship nor a TypeConverter would be to not have the Engine as a table but to use the #Embedded annotation prior to the engine. e.g.
#Entity
class Car{
#PrimaryKey
var id = 0
var name: String? = null
#Embedded
var engine: Engine? = null
}
...
class Engine{
#PrimaryKey
#ColumnInfo(name = "engineId")
var id = 0
var manufacturer: String? = null
}
the name of the column used to store the Engine's id changed as otherwise there would be 2 columns with the same name.
Note that with this way there is no need for the #Entity annotation as you are storing the Engine values within the Car.
This is not considered good practice as if the same engine is used by many cars then you are duplicating data and thus that it is not normalised.
The third and least desirable way from a database perspective is to store a representation of the engine object in a single column. That is to convert the object into a singular storable representation. Typically a JSON string. Thus you need code (a function) to convert from the object to the single value (JSON string) and (another function) to convert from the JSON String to the Object.
With this method not only are you not normalising the data but additionally you end up storing the bloat required to enable the object to be represented. That bloat, from a database, perspective, obfuscating the actual useful stored data to some extent.
In addition there is not a single set/standard library providing the functionality of converting objects to/from JSON, so you have to select a flavour and then include that library in the project.
Here is a class that contains Type Converters that could be used (see comment re library):-
class CarAndEngineTypeConverters{
/* Using Library as per dependency implementation 'com.google.code.gson:gson:2.10.1' */
#TypeConverter
fun convertEngineToJSONString(engine: Engine): String = Gson().toJson(engine)
#TypeConverter
fun convertJSONStringToEngine(jsonString: String): Engine = Gson().fromJson(jsonString,Engine::class.java)
}
This would suit your original classes.
Room needs to be told to use these classes (it works out when) via a #TypeConverters annotation (note the plural and not singular) this it immediately before or after the #Database annotation has the highest level of scope. The annotation itself could be #TypeConverters(value = [CarAndEngineTypeConverters::class])
To demonstrate all three together consider this over the top Car class:-
#Entity
class Car{
#PrimaryKey
var id = 0
var name: String? = null
var engine: Int? = null
#Embedded
var alternativeEngine: Engine? = null
var jsonConvertedEngine: Engine? = null
}
Over the top as the engine is stored 3 times (could be different engines)
The *Engine class
#Entity
class Engine{
#PrimaryKey
#ColumnInfo(name = "engineId")
var id = 0
var manufacturer: String? = null
}
The Type Converters as above.
With the above in place and using within an activity (noting that for brevity/convenience .allowMainThreadQueries has been used):-
db = TheDatabase.getInstance(this)
carAndEngineDAO = db.getCarAndEngineDAO()
var engine1 = Engine()
engine1.manufacturer = "Ford"
engine1.id = carAndEngineDAO.insert(engine1).toInt()
var car1 = Car()
car1.name = "Escort"
car1.engine = engine1.id /* id of the engine */
car1.alternativeEngine = engine1
car1.jsonConvertedEngine = engine1
carAndEngineDAO.insert(car1)
Using Android Studios App inspection the view the database then
The Columns id and name and obviously as expected
The engine column contains the value 0, this is the id of the respective engine in the engine table (maximum 8 bytes to store the id)
The JsonConvertedEngine column stores the JSON representation of the Engine (31 bytes)
The engineId column and manufacturer column stores the respective values (12 bytes).
The Engine Table (only needed for the relationship) is :-
You should use TypeConverters:
At first add this dependency to your project to convert Engine to Json and vice versa
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
Now you should create an Object class that convert Engine to Json. This class make Engine understandable for Room :
object CommonTypeConverters {
#TypeConverter
#JvmStatic
fun stringToEngine(value: String): Engine = fromJson(value)
#TypeConverter
#JvmStatic
fun engineToString(items: Engine?): String = toJson(items)
inline fun <reified T> toJson(value: T): String {
return if (value == null) "" else Gson().toJson(value)
}
inline fun <reified T> fromJson(value: String): T {
return Gson().fromJson(value, object : TypeToken<T>() {}.type)
}
In the end Engine is not a entity and you should add #Typeconverter annotation to your database class :
#Database(entities = [Car::class], version = 1)
#TypeConverters(CommonTypeConverters::class)
abstract class AppDatabase : RoomDatabase() {
//...
}

How to access fields of a Generic type in Kotlin?

I have a Generic class.
class GenericsClassExample<T>(
var data: T
)
I have another class which I'm passing in place of T.
data class MyDataClass1(val id: String, val value: String)
And I'm instantiating Generic Class like :
val genericClass = GenericClass<MyDataClass1>()
I would like to know how to access the fields (id and value) from genericClass object without knowing the type of the class I'm passing, i.e. I could also pass another class like MyDataClass1 say MyDataClass2 which also has its own fields.

How to make base class accept generics which have different key

I am getting a response something like this where status, message & data keys will remain the same across the different API responses & only the content inside the data JSON object will change, in this example, I have a JSON object with a member key in other response I can have tournament key.
{
"status": true,
"message": "Success",
"data": {
"member": {
"id": 21,
"emailAddress": "abc#xyz.com",
"firstName": "ABC"
}
}
}
Currently, I am making use of generics and doing something like this
data class SignInResponse(
val `data`: Data<Member>
) : BaseResponse()
and BaseResponse class has common JSON keys that I am getting, Here I'm using generics in Data class passing the JSON class that is changing.
open class BaseResponse {
val status: Boolean = false
val message: String = UNDEFINED
}
#Keep
data class Data<T>(val actualData: T)
But this approach is incomplete because the above code will expect a JSON key actualData but here JSON key can be a member , tournament , or anything. How can I pass the class in Data class so that it can support the above JSON response?
Is this as a result from a network call? I've never used generics in trying to parse incoming network calls before.
I'd have a baseData class that contains any common fields between member or tournament - like id or name and then subclass that with a concrete implementation for each possibility which would hold the unique data.
then your BaseResponse class could just be
data class BaseResponse(
val status: Boolean
val message: String
val data: BaseData
)
One way to represent this would be via a class hierarchy rather than generics, because this extra wrapping is not present in the JSON, nor in your conceptual data structure.
A sealed class is what I would usually go for in this case.
However, it all depends on which JSON serialization library you're using. If you control both ends of the communication, I would definitely go for Kotlinx Serialization, which supports sealed hierarchies (and polymorphism in general) in a nice way.
If you don't control the serialization side, maybe a temporary union of all fields as #Chris mentioned is your simplest option.
I ended up using the following approach, where I create a generic for data class
data class BaseResponse<T>(
val status: Boolean, val message: String, val data: T
)
Above implementation enables me to pass model of changing JSON Object inside the data field
data class SignInResponse(
val token: String, val member: Member
)
and after summing up everything we can use it like this
interface BasicxBookingServices {
#POST(Urls.SIGNIN_URL)
suspend fun signIn(#Body body: SignInRequest): Response<BaseResponse<SignInResponse>>
}

How to implement polymorphic list deserialization with GSON in Kotlin?

I have a data feed that is returning a list that could be either of three types ( Say type A, B and C ). All of the types above share 2 properties, the rest of the properties is specific to the type. I tried using the pattern.
abstract class Parent (val type: String, val id: String)
And
data class TypeA(override val type: String ... )
data class TypeB(override val type: String ... )
I am using Retrofit and trying to deserialize the list to
List<? extends Parent>
which in Kotlin should be
List<out Parent>
However GSON is throwing a deserializing error on instantiating the parent class which is abstract.
java.lang.RuntimeException: Failed to invoke public com.XX.Parent() with no args
Any ideas how I can implement this in Kotlin?
As you have Moshi tagged in your question, I'll give you a way of doing it using MOshi's PolymorphicJsonAdapterFactory. You can basically parse something into different types, depending on the value of the object's property.
First thing you'll do is declared your parent type as a sealed class and have the other types extend from it:
sealed class Parent(val type: String){
data class TypeA(override val type: String, ... ): Parent(type)
data class TypeB(override val type: String, ... ): Parent(type)
}
now you're gonna tell Moshi how to parse Parent objects. You do that registering a PolymorphicJsonAdapterFactory:
val moshi = Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Parent::class.java, "type")
.withSubtype(TypeA::class.java, "typeA")
.withSubtype(TypeB::class.java, "typeB")
.build()
with that, if the value of the property "type" is "typeA", it will deserialize into a TypeA instance. like wise to TypeB, if property "type" is "typeB"
You can look another example here:
https://github.com/square/moshi/blob/master/adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.java

Data class inheriting from Java class

I have a data class in Kotlin that inherits from a Java class, which defines a constructor with 1 argument,
public BaseClass(String userSessionId) {
this.userSessionId = userSessionId;
}
My Kotlin class is defined as this
class DerivedClass(
userSessionId: String,
var other: Other? = null
) : BaseClass(userSessionId) {
I can't define it as a data class because of userSessionId, which Kotlin requires to be a val or var in data classes. However, if I do so, then Retrofit throws an exception because there are 2 members named userSessionId. Is there a way to have a data class inherit from a Java class with a constructor taking arguments? Note that I cannot change the base class.
A possible solution is to define a dummy val to avoid the name clash, but this is less than ideal
data class DerivedClass(
val dummy: String,
var other: Other? = null
) : BaseClass(dummy) {
You can use the transient keyword in Java to ignore a field during serialization, this can be done in Kotlin by using the #Transient annotation on the property instead.

Categories

Resources