Room database with Kotlin inline class as an Entity field - android

I am trying to get Room(https://developer.android.com/topic/libraries/architecture/room) work with Kotlin's inline classes as described in Jake Whartons article Inline Classes Make Great Database IDs:
#Entity
data class MyEntity(
#PrimaryKey val id: ID,
val title: String
)
inline class ID(val value: String)
When compiling this Room complains that
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).
Looking into the generated Java code I find:
private MyEntity(String id, String title) {
this.id = id;
this.title = title;
}
// $FF: synthetic method
public MyEntity(String id, String title, DefaultConstructorMarker $constructor_marker) {
this(id, title);
}
Mysteriously the default constructor is private now.
When using String as a type for id (or a typealias), the generated Java class constructor looks like expected:
public MyEntity(#NotNull String id, #NotNull String title) {
Intrinsics.checkParameterIsNotNull(id, "id");
Intrinsics.checkParameterIsNotNull(title, "title");
super();
this.id = id;
this.title = title;
}
Does somebody now how to keep the default constructor public while using Inline Classes as data entity properties?

I believe the reason is that the ID class will be represented as String in runtime. So the $constructor_marker additional parameter is to guarantee the uniqueness of the MyEntity(String id, String title) constructor signature, cause this constructor could already have been defined. But I'm just speculating here.
Could you try to explicitly define this constructor in MyEntity class and see if it works?

Kotlin inline classes use name mangling.
So I believe your Room database cannot find the getter and setter for you ID field.
Try to add:
...
#get:JvmName("getID")
#set:JvmName("setID")
#PrimaryKey val id: ID,
before your ID parameter declaration to disable mangling.
It helps to me

With the answer from Lyubomyr Ivanitskiy and some tinkering it can be done.
#Entity
class Test(
#PrimaryKey(autoGenerate = true)
#get:JvmName("getId")
#set:JvmName("setId")
var id: ID,
) {
constructor(): this(ID(0)) // This is required as replacement for
constructor with actual fields
}
When trying to load this entity using a dao it will fail due to the getter method not being generated. It does not work for me using the inner class ID. So it needs to be tricked like this:
#Dao
interface TheDao {
#Deprecated(
"This is just for the generated Dao_Impl",
level = DeprecationLevel.WARNING,
replaceWith = ReplaceWith("getByIdRealId(theID)")
)
#Query("select * from test where id = :theID")
fun getByIdLongType(theID: Long): Test
}
fun TheDao.getByIdRealId(theID: ID): Test = getByIdLongType(theID.id)
This will not prevent using the getById with Long parameter but generate at least a warning about it.
TestCode:
#Test
fun createAndLoadTest() {
val toBeSaved = Test(ID(42))
dao.save(toBeSaved)
val fromDB = dao.getByIdRealId(ID(42))
fromDB shouldNotBe null
fromDB.id shouldNotBe 42
fromDB.id shouldBe ID(42)
}

Related

How to resolve "Entities and POJOs must have a usable public constructor"

I have seen this question several times on SO. however the solution doesn't seem to apply to my problem.
I have a Kotlin data-class that is used as an Entity in Room
#Entity(tableName = "training_session")
data class SessionEntity(
#PrimaryKey(autoGenerate = false) val id: Long,
#ColumnInfo(name = "current_state_marker") val currentState: Short,
#Embedded val states: List<Int>
)
It is producing
> Task :training-infrastructure:kaptDebugKotlin FAILED
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). - java.util.List
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). - java.util.List
In the same project I have a very similar entity which also has a list and that doesn't produce any errors.
Tried out the answer provided by MikeT, for me it required a small change in the way the converters were defined
data class SessionStateList (val stateList : List<Int>)
class SessionStateListConverter {
#TypeConverter
fun fromArraySessionStateList(sh: List<Int>?): String? {
return Gson().toJson(sh)
}
#TypeConverter
fun toArraySessionStateList(sh: String?): List<Int>? {
val listType: Type = object : TypeToken<ArrayList<Int?>?>() {}.type
return Gson().fromJson(sh,listType)
}
}
A quick follow-up. I had mentioned that I have another Entity that has an Embedded val something: List<Int> and I had not noticed any compiler errors.
The reason, I had not noticed any compiler errors was because the entity was not included in the #Database annotation.
You cannot have a List/Array etc as a column type. So your issue is centred on #Embedded val states: List<Int>
You could have a POJO e.g. StatesHolder :-
data class StatesHolder(
val stateList: List<Int>
)
and then have
#Entity(tableName = "training_session")
data class SessionEntity(
#PrimaryKey(autoGenerate = false) val id: Long,
#ColumnInfo(name = "current_state_marker") val currentState: Short,
val states: StatesHolder
)
Note that you cannot Embed StatesHolder as then that just inserts List. If you want to Embed then you have to Embed a wrapper that uses a StatesHolder.
You will then need TypeConverters to convert to and from a StatesHolder object to a type that can be stored. Probably a String and Probably a JSON respresentation of the StatesHold object e.g.
class Converters {
#TypeConverter
fun fromStatesHolder(sh: StatesHolder): String {
return Gson().toJson(sh)
}
#TypeConverter
fun toStatesHolder(sh: String): StatesHolder {
return Gson().fromJson(sh,StatesHolder::class.java)
}
}
You additionally need to use #TypeConverters annotation that defines the Converts::class. If coded at the #Database level the converters have full scope.
So after #Database(.....) you could have :-
#TypeConverters(Converters::class)

ROOM constructors failed to match on build

When building my app, I am getting an error with two entity classes. It's basically saying it cannot find the setter for the constructors I am giving. Originally I had vals, I converted these to vars and that seems to fix the issue.
But I don't like this fix... The documentation uses vals in their examples of how to build entities. So why does it not work for certain entities I define? I feel like the coding I'm doing is going to be prone to some sort of error because I am only bandaging the problem rather than actually fixing it.
https://developer.android.com/training/data-storage/room/defining-data
data class DirectMessage(
#PrimaryKey
#ColumnInfo(name = "dm_id") override val objectId: String,
//Author of message
#Ignore override val author: User,
//Recipient of message, usually user of the app
#Ignore override val recipient: User,
//Content of message
override val content: String,
//Date of creation
override val timestamp: Long
) : DirectMessage {
//Place identifier into database instead of Entity
#ColumnInfo(name = "author") val _author : String = author.uid
#ColumnInfo(name = "recipient") val _recipient : String = recipient.uid
/**
* Get author's user thumbnail
*/
override fun getThumbnail() {
TODO("Not yet implemented")
}
}
#Entity
data class Comment (
#PrimaryKey
#ColumnInfo(name = "cid") override val objectId: String,
//Author of the comment
#Ignore override val author: User,
//Moment ID this comment is attached to
override var momentId: String,
//Time comment was created
override val timestamp: Long,
//Content of the comment
override var content: String
) : Comment {
//Place identifier into database instead of User entity
#ColumnInfo(name = "author") val _author = author.uid
/**
* Get thumbnail of User object
*/
override fun getThumbnail() {
TODO("Not yet implemented")
}
}
interface Comment : Message {
val momentId : String
}
public final class Comment implements com.example.barrechat192.data.entities.Comment {
^
Tried the following constructors but they failed to match:
Comment(java.lang.String,com.example.barrechat192.data.entities.User,java.lang.String,long,java.lang.String) -> [param:objectId -> matched field:objectId, param:author -> matched field:unmatched, param:momentId -> matched field:momentId, param:timestamp -> matched field:timestamp, param:content -> matched field:content]C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\messages\Comment.java:13: error: Cannot find setter for field.
private final java.lang.String _author = null;
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\messages\Comment.java:17: error: Cannot find setter for field.
private final java.lang.String objectId = null;
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\messages\Comment.java:23: error: Cannot find setter for field.
private final long timestamp = 0L;
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\messages\DirectMessage.java:7: 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 DirectMessage implements com.example.barrechat192.data.entities.DirectMessage {
^
Tried the following constructors but they failed to match:
DirectMessage(java.lang.String,com.example.barrechat192.data.entities.User,com.example.barrechat192.data.entities.User,java.lang.String,long) -> [param:objectId -> matched field:objectId, param:author -> matched field:unmatched, param:recipient -> matched field:unmatched, param:content -> matched field:content, param:timestamp -> matched field:timestamp]C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\messages\DirectMessage.java:10: error: Cannot find setter for field.
private final java.lang.String _author = null;
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\messages\DirectMessage.java:13: error: Cannot find setter for field.
private final java.lang.String _recipient = null;
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\messages\DirectMessage.java:17: error: Cannot find setter for field.
private final java.lang.String objectId = null;
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\messages\DirectMessage.java:25: error: Cannot find setter for field.
private final java.lang.String content = null;
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\messages\DirectMessage.java:26: error: Cannot find setter for field.
private final long timestamp = 0L;
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\implementations\mapobjects\MapObject.java:10: error: The name "map_objects" is used by multiple entities or views: com.example.barrechat192.data.entities.implementations.mapobjects.MapObject, com.example.barrechat192.data.entities.implementations.mapobjects.MapObject
public final class MapObject implements com.example.barrechat192.data.entities.MapObject {
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\AppDatabase.java:8: error: The name "map_objects" is used by multiple entities or views: com.example.barrechat192.data.entities.implementations.mapobjects.MapObject, com.example.barrechat192.data.entities.implementations.mapobjects.MapObject
public abstract class AppDatabase extends androidx.room.RoomDatabase {
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\geocache\GeoTableWithMapObjects.java:12: error: Cannot find the child entity column `geohash` in com.example.barrechat192.data.entities.implementations.mapobjects.MapObject. Options: objectId, timestamp, thumbnailUrl, thumbnailPath, objectType, local, views, viewed, geoHash, latitude, longitude
private final java.util.List<com.example.barrechat192.data.entities.implementations.mapobjects.MapObject> mapObjects = null;
^C:\Users\Anon\AndroidStudioProjects\Barrechat192\app\build\tmp\kapt3\stubs\debug\com\example\barrechat192\data\entities\geocache\GeoTableWithMapObjects.java:6: 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).
I also don't understand why I can inherit a val and change it to a var in a child class in Kotlin. That also seems to make the code work, changing the val to var in the entity class, but leaving it as a val in the interface.
Hi you have created constructors which is used when it is created by you.
You should create constructor passing params all database columns. Room is trying to create an instance but can not find setters for these mapped fields
You Need create setter for your model, the database cannot set the data.
See this: Getter and Setter

Is there a simple way to get an object of data calss with default value which from string resource file in Kotlin?

I hope to get an object of data calss with default value which from string resource file.
The code A will not be compiled because I have not to pass a Context paramter for data class MVoice(), I don't think it's a good way.
I there a simple way to get an object of data calss with default value which from string resource file in Kotlin?
Added Content:
If I use Code B, is it a good way ?
Code A
#Entity(tableName = "voice_table", indices = [Index("createdDate")])
data class MVoice(
#PrimaryKey #ColumnInfo(name = "id") var id: Long = 0,
var name: String = getString(R.String.Name)
)
<string name="Name">Untitled</string>
Code B
#Entity(tableName = "voice_table", indices = [Index("createdDate")])
data class MVoice(
#PrimaryKey #ColumnInfo(name = "id") var id: Long = 0,
var name: String
)
{
companion object {
fun getDefaultMVoice(mContext: Context): MVoice {
return MVoice(name = mContext.getString(R.string.name))
}
}
}
I assume you want the default value of the name variable to be displayed somewhere in your app's UI. I would create an extension function on MVoice object:
fun MVoice.nameOrDefault(ctx: Context) =
if (name == null || name.trim().isEmpty()) {
MVoice.defaultName(ctx)
} else {
name
}
data class MVoice(...) {
// ...
companion object {
#JvmStatic
fun defaultName(ctx: Context) = ctx.getString(R.string.name)
}
}
We can use that function to set text, for example, to TextView:
val voice: MVoice = ...
val textView: TextView = ...
textView.text = voice.nameOrDefault(context)
You should not add logic or Android specific code to your data classes which makes unit testing impossible.
You should instead use a business logic layer(usecase/inteactor) or ViewModel that has interface and that interface should have concrete implementation that takes context that returns a default string. It's like injecting context to db if you are familiar with dagger.
You can also use AndroidViewModel class either for accessing context.
You cannot get string resources without a context, however you can use the application context and make it static. You have to declare in manifest:
<application android:name="com.xyz.App">
</application>
And then make the application context static:
class App: Application() {
companion object {
lateinit var appContext: WeakReference<Context>
}
override fun onCreate() {
super.onCreate()
appContext = WeakReference(applicationContext)
}
}
Then you can use the context everywhere you need it by calling:
App.appContext.get()!!.getString(R.string.example)
Yes, we can access resources without using Context
To get a value from string resource file for data class in Kotlin:
Resources.getSystem().getString(R.string.app_name)
you can use them everywhere in your application, even in static constants declarations!
eg:
#Entity(tableName = "Support")
data class Support(
#PrimaryKey #ColumnInfo(name = "support_id") val support_id: String,
#ColumnInfo(name = "support_count") val support_count: Int,
#ColumnInfo(name = "support_visit") val support_visit: String= Resources.getSystem().getString(R.string.app_name)
)
I just gave a solution for your answer, but I do recommended you to use direct string value.
I think what you're trying to do is not a good idea on it's own, so even if you can, the question is if you should. It basically means that your data class needs to know about Android resources and UI (R.string, Context), which is questionable design
I'd go with
#Entity(tableName = "voice_table", indices = [Index("createdDate")])
data class MVoice(
#PrimaryKey #ColumnInfo(name = "id") var id: Long = 0,
var name: String
)
as it doesn't have the default value you always have to provide it, then you just call
MVoice(requireContext().getString(R.string.mvoicedefault))

Error: The Columns returned by the query does not have the fields

I am implementing Room Database. Here is my POJO Class
public class Task implements Serializable {
#PrimaryKey(autoGenerate = true)
private int id;
#ColumnInfo(name = "task_name")
private String task;
#ColumnInfo(name = "description")
private String desc;
#ColumnInfo(name = "finish_by")
private String finishBy;
#ColumnInfo(name = "finished")
private boolean finished;
#ColumnInfo(name="no_of_days")
private String no_of_days;
public String getNo_of_days() {
return no_of_days;
}
public void setNo_of_days(String no_of_days) {
this.no_of_days = no_of_days;
}
}
This is the DAO Class
#Dao
public interface TaskDao {
#Update
void update(Task task);
#Query("SELECT task_name,description,no_of_days FROM task")
List<Task> getTasksandDescription();
}
While running my code, I am getting the following error
com.example.myapplication1.model.Task has some fields [id, finish_by, finished] which are not returned by the query. If they are not supposed to be read from the result, you can mark them with #Ignore annotation. You can suppress this warning by annotating the method with #SuppressWarnings(RoomWarnings.CURSOR_MISMATCH).
Columns returned by the query: task_name, description, no_of_days. Fields in com.example.myapplication1.model.Task: id, task_name, description, finish_by, finished, no_of_days.
This is not an error but a warning, it's telling you that you are mapping an SQL to a Pojo but you are not returning all the fields required to set up the Pojo. So your class has more fields than is returned from the query, you can fix this by doing the following.
I. Do a select * to return all the fields
#Query("SELECT * FROM task")
II. Add #Ignore annotation to the fields you're not interested in
III. Create another Java class that contains only the fields you're interested in and return in from the query instead.

Room not finding lombok generated constructor

I'm using lombok to generate constructors, getters and setters for my models. When i try to use lombok to generate the constructor for my entity class, I get this error
Error:(14, 8) 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).
Tried the following constructors but they failed to match:
Region(int,java.lang.String,java.lang.String) -> [param:arg0 -> matched
field:unmatched, param:arg1 -> matched field:unmatched, param:arg2 ->
matched field:unmatched]
but writing the constructor manually works. Can anyone help me figure out what's wrong?
My entity class is shown below
#Value
#Entity
public class Region {
#PrimaryKey
private int regionId;
private String name;
private String code;
}
Room version: 1.1.0
Lombok version: 1.16.20
The matching seems to fail because the constructor parameter names are not available at runtime.
Since version 1.16.20 lombok does not generate #ConstructorProperties annotations any more (which would carry those names).
Try adding lombok.anyConstructor.addConstructorProperties = true to your lombok.config, and lombok will generate a #ConstructorProperties annotation for your constructor. (See https://projectlombok.org/features/configuration for details on how to configure lombok.)
EDIT: The problem is the annotation processing during compilation. Both Room and lombok hook into javac as annotation processors, and they do not work nicely in combination. So at the moment, the only stable solution is to delombok first.
You can use the following setup:
#Entity
#Getter
#Setter
#AllArgsConstructor(onConstructor = #__({#Ignore}))
#NoArgsConstructor
public class Region {
#PrimaryKey
private int regionId;
private String name;
private String code;
}
This will make Room use the default constructor and set the value via the provided setters. Additionally you have a constructor that accepts all arguments for object instantiation, but will be ignored by Room.
Note: Object won't be immutable that way
Please try this as below with #Data annotation.
#Value
#Entity
#Data
public class Region {
#PrimaryKey
private int regionId;
private String name;
private String code;
}

Categories

Resources