I fetch data from API and I would like to reuse it in data class like this (ItemStateRoom is data with String, Int etc.):
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
#ColumnInfo(name = "state")
#JsonAdapter(ItemStateAdapter::class)
var state: ItemStateRoom?
{
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
}
When I set state to null of course I have null, but program compiles well.
If it is like at the top error occurs:
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 tried to do like this:
#Ignore
#JsonAdapter(ItemStateAdapter::class)
#ColumnInfo(name = "state")
var state: ItemStateRoom?,
Then I have errors:
error: Cannot figure out how to read this field from a cursor.
.ItemStateRoom state;
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
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).
EDIT1:
Description: I make realm to room migration, realmobjects are downloaded as well and they are not null, but some of objects(data classes) I have changed to Room i.e. ItemStateRoom or ItemSubStatus. For sure it is not null, I download it from backend in postman it gives value for sure.
#TypeConverters(Converters::class)
#Entity(tableName = "itemsubstatus")
data class ItemSubStatusRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
#ColumnInfo(name = "title") val title: String = "",
#ColumnInfo(name = "description") val description: String? = null
)
TypeConverters:
class Converters {
private val gson = GsonBuilder().serializeNulls().create()
#TypeConverter
fun typeSubStatusToString(type: ItemSubStatusRoom?): String =
gson.toJson(type)
#TypeConverter
fun typeItemSubStatusFromString(value: String): ItemSubStatusRoom? =
gson.fromJson(value, ItemSubStatusRoom::class.java) as ItemSubStatusRoom?
}
My response data is simple:
class ItemsDataResponse (
#SerializedName("items")
val items: ArrayList<ItemRoom>,
val total: Int
)
in my ItemData it is also simple
#TypeConverters(Converters::class)
#Entity(tableName = "item")
data class ItemRoom #JvmOverloads constructor(#PrimaryKey(autoGenerate = true) var id: Int = 0,
[...]
#ColumnInfo(name = "sub_status")
var subStatus: ItemSubStatusRoom?,
``
When it is saying there is no "public constructor", it is saying that it does not know how to construct the ItemRoom object that does not include the state field/member.
So with state #Ignore'd you would need to have a constructor that doesn't expect the state as a parameter.
with a Data Class the definition within the parenthesises is the constructor as such.
e.g.
constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
in which case Room would always construct an ItemRoom where state is null.
However, I suspect that the above is not what you want. Rather that you want the state to be saved in the database. As such, as it is not a type that can be stored directly and hence the need for a TypeConverter (actually 2)
only String, integer (Int, Long, Byte, Boolean etc types), decimal (Float, Double etc types) or byte streams (ByteArray) can be stored directly
Then if you want the state field to be saved in the database, then you will need 2 TypeConverters.
One which will convert from the ItemStateRoom to one of the types that can be stored (so it is passed an ItemStateRoom and returns one of the types that can be stored directly).
The other will convert from the type stored to an ItemStateRoom object.
So assuming that you want to store the state you could have, something like :-
#TypeConverters(value = [Converters::class])
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
//#Ignore /*<<<<< if used a constructor is needed that doesn't require the state, as the state is not available */
/* if not used then state has to be converted to a type that room can store */
#ColumnInfo(name = "state")
#JsonAdapter(ItemStateAdapter::class)
var state: ItemStateRoom?
)
{
//constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
}
data class ItemStateRoom(
/* made up ItemStateRoom */
var roomNumber: Int,
var roomType: String
)
class ItemStateAdapter() {
/* whatever .... */
}
class Converters {
#TypeConverter
fun convertItemStateRoomToJSONString(itemStateRoom: ItemStateRoom): String = Gson().toJson(itemStateRoom)
#TypeConverter
fun convertFromJSONStringToItemStateRoom(jsonString: String): ItemStateRoom = Gson().fromJson(jsonString,ItemStateRoom::class.java)
}
note the commented out #Ignore and constructor (for if #Ignore'ing the state field).
how you implement, if at all, the #JsonAdapter, would be as you have it.
note the #TypeConverters (which you would omit if #Ignoreing the state field)
It might be preferable to include the #TypeConverters at the #Database level, where it has full scope. (see https://developer.android.com/reference/androidx/room/TypeConverters)
Example
Here's an example of storing the state (ItemStateRoom) that uses the code above, with a pretty standard #Database annotated class and an #Dao annotated interface :-
#Dao
interface ItemRoomDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(itemRoom: ItemRoom): Long
#Query("SELECT * FROM item")
fun getAllItems(): List<ItemRoom>
}
Two items are inserted and then extracted and written to the log using the following activity code :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: ItemRoomDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getItemRoomDao()
dao.insert(ItemRoom(type = "Bedroom", template = "The Template", title = "Bedroom1", state = ItemStateRoom(101,"King size - Shower - Bath - Kitchenette")))
dao.insert(ItemRoom(type = "Bedroom", template = "The Template", title = "Bedroom2", state = ItemStateRoom(102,"Queen size - Shower - Bath")))
for(i in dao.getAllItems()) {
Log.d("DBINFO","Item ID is ${i.id} Title is ${i.title} Room Number is ${i.state!!.roomNumber} Room Type is ${i.state!!.roomType}")
}
}
}
The result in the log:-
D/DBINFO: Item ID is 1 Title is Bedroom1 Room Number is 101 Room Type is King size - Shower - Bath - Kitchenette
D/DBINFO: Item ID is 2 Title is Bedroom2 Room Number is 102 Room Type is Queen size - Shower - Bath
The database via App Inspection :-
as can be seen the ItemStateRoom has been converted to (and from according to the output in the log) a JSON String.
Ignore the weatherforecast table, that was from another answer that was used for providing this answer.
Alternative Approach
Instead of converting the ItemState to a JSON representation (which is unwieldly from a database perspective) consider the potential advantages of instead embedding the ItemState. The difference in this approach is that the fields of the ItemState are each saved as individual columns.
e.g.
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
//#Ignore /*<<<<< if used a constructor is needed that doesn't require the state, as the state is not available */
/* if not used then state has to be converted to a type that room can store */
//#ColumnInfo(name = "state")
//#JsonAdapter(ItemStateAdapter::class)
//var state: ItemStateRoom?
#Embedded
var itemStateRoom: ItemStateRoom
)
{
//constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
/*
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
*/
}
Thus to update, at least from the database aspect, you are freed from trying to manipulate a representation of the data. You can update the actual data directly.
Using the above, with the example code (changed to use itemState instead of state) then the database looks like:-
You say
But to reassign values dynamically I have to use #JsonAdapter.
The JsonAdapter is NOT going to magically update the data in the database, to update the data you will have to use a function in an interface (or an abstract class) that is annotated with #Dao which will either be annotated with #Update (convenience) or #Query (with appropriate UPDATE SQL).
Related
I'm trying to implement caching of a JSON API response with Room.
The response I get in JSON follows this data class structure:
#Serializable
data class ApiDataResponse(
val success: Boolean,
val message: String? = null,
val albums: List<AlbumResponse> = emptyList()
)
#Serializable
data class AlbumResponse(
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicResponse>
)
#Serializable
data class PicResponse(
val picUrl: String,
val emojis: List<String>
)
Notes:
#Serializable is from kotlinx.serialization library to parse the JSON response.
These response data classes are only used inside my datasource layer, the view layer doesn't care about an ApiDataResponse and only knows a "pure" version of AlbumResponse called Album and a "pure" version of PicResponse called Pic (by "pure" I mean a data class without external library annotations).
So to implement this cache with Room I could discard the ApiDataResponse and save only the contents of AlbumResponse (and consequently PicResponse), having new data classes for Room entities following this idea:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>, // obstacle here
val pics: List<PicEntity> // obstacle here
)
// obstacle here
// #Entity
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
I already know how to save simple data in Room, with the simplest JSON I was able to do this task, the problem is that in this more complex scenario I have no idea how to achieve this goal. So I wish someone could guide me in this situation.
Maybe it's a little late, but I would still like to add some interesting information regarding MikeT's answer.
It is not necessary to create a new data class just to transform a custom object into a JSON with TypeConverter, for example:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicEntity> // can be converted directly
)
import kotlinx.serialization.Serializable
#Serializable // to be able to do the serialize with the kotlinx.serialization
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
With just these two data classes we can build the TypeConverters as follows:
import androidx.room.TypeConverter
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseConverter {
private val json = Json
#TypeConverter
fun convertStringListToString(strings: List<String>): String =
json.encodeToString(strings)
#TypeConverter
fun convertStringToStringList(string: String): List<String> =
json.decodeFromString(string)
#TypeConverter
fun convertPicEntityListToString(picsEntity: List<PicEntity>): String =
json.encodeToString(picsEntity)
#TypeConverter
fun convertStringToPicEntityList(string: String): List<PicEntity> =
json.decodeFromString(string)
}
Code to create an example dummy list:
object DummyAlbums {
fun createList(): List<AlbumEntity> = listOf(
AlbumEntity(
id = "0001",
title = "Album AB",
createdBy = "Created by AB",
enabled = true,
keywords = listOf("ab"),
pics = dummyPics(albumId = "0001", size = 0)
),
AlbumEntity(
id = "0002",
title = "Album CD",
createdBy = "Created by CD",
enabled = false,
keywords = listOf("cd", "c", "d"),
pics = dummyPics(albumId = "0002", size = 1)
),
AlbumEntity(
id = "0003",
title = "Album EF",
createdBy = "Created by EF",
enabled = true,
keywords = listOf(),
pics = dummyPics(albumId = "0003", size = 2)
)
)
private fun dummyPics(
albumId: String,
size: Int
) = List(size = size) { index ->
PicEntity(
picUrl = "url.com/$albumId/${index + 1}",
emojis = listOf(":)", "^^")
)
}
}
So we can have the following data in table:
I wanted to highlight this detail because maybe it can be important for someone to have a table with the cleanest data. And in even more specific cases, to have it clean, you can do the conversion manually using Kotlin functions, such as joinToString(), split(), etc.
I believe the issue is with columns as lists.
What you could do is add the following classes so the Lists are embedded within a class:-
data class StringList(
val stringList: List<String>
)
data class PicEntityList(
val picEntityList: List<PicEntity>
)
and then change AlbumEntity to use the above instead of the Lists, as per:-
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
//val keywords: List<String>, // obstacle here
val keywords: StringList, /// now not an obstacle
//val pics: List<PicEntity> // obstacle here
val emojis: PicEntityList// now not an obstacle
)
To be able to store the "complex" (single object) you need to convert this so some TypeConverters e.g.
class RoomTypeConverters{
#TypeConverter
fun convertStringListToJSON(stringList: StringList): String = Gson().toJson(stringList)
#TypeConverter
fun convertJSONToStringList(json: String): StringList = Gson().fromJson(json,StringList::class.java)
#TypeConverter
fun convertPicEntityListToJSON(picEntityList: PicEntityList): String = Gson().toJson(picEntityList)
#TypeConverter
fun convertJSONToPicEntityList(json: String): PicEntityList = Gson().fromJson(json,PicEntityList::class.java)
}
note this utilises the dependency com.google.code.gson
You then need to have the #TypeConverters annotation to cover the appropriate scope (at the #Database level is the most scope). Note the plural rather than singular, they are different.
To demonstrate the above works, First some functions in an interface annotated with #Dao :-
#Dao
interface AlbumDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(albumEntity: AlbumEntity): Long
#Query("SELECT * FROM albums")
fun getAllAlbums(): List<AlbumEntity>
}
Second an #Database annotated class (note the #TypeConverters annotation) :-
#TypeConverters(RoomTypeConverters::class)
#Database(entities = [AlbumEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAlbumDao(): AlbumDao
companion object {
#Volatile
private var instance: TheDatabase?=null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"album.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Third some activity code to actually do something (insert some Albums and then extract them writing the extracted data to the Log) :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AlbumDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAlbumDao()
dao.insert(AlbumEntity(
"Album001", "The First Album","Fred",false,
StringList(listOf("The","First","Album")),
PicEntityList(
listOf(
PicEntity("PE001", listOf("emoji1","emoji2","emoji3")),
PicEntity("PE002",listOf("emoji10")),
PicEntity("PE003", listOf("emoji20","emoji21"))
))
))
dao.insert(AlbumEntity(
"Album002","This is the Second Album","Mary", true,
StringList(listOf("keya","keyb","keyc","keyd","keye")),
PicEntityList(
listOf(
PicEntity("PE011", listOf("emoji30","emoji31")),
PicEntity("PE012", listOf("emoji1","emoji10","emoji20","emoji30"))
))
))
for (a in dao.getAllAlbums()) {
logAlbum(a)
}
}
fun logAlbum(albumEntity: AlbumEntity) {
val keywords = StringBuilder()
for(s in albumEntity.keywords.stringList) {
keywords.append("\n\t$s")
}
val pelog = StringBuilder()
for (pe in albumEntity.emojis.picEntityList) {
pelog.append("\n\tURL is ${pe.picUrl}")
for (emoji in pe.emojis) {
pelog.append("\n\t\tEmoji is ${emoji}")
}
}
Log.d(
"ALBUMINFO",
"Album id is ${albumEntity.id} " +
"Title is ${albumEntity.title} " +
"CreateBy ${albumEntity.createdBy} " +
"Enabled=${albumEntity.enabled}. " +
"It has ${albumEntity.keywords.stringList.size} keywords. " +
"They are $keywords\n. " +
"It has ${albumEntity.emojis.picEntityList.size} emojis. " +
"They are ${pelog}"
)
}
}
Run on the main thread for convenience and brevity
When run then the log contains:-
D/ALBUMINFO: Album id is Album001 Title is The First Album CreateBy Fred Enabled=false. It has 3 keywords. They are
The
First
Album
. It has 3 emojis. They are
URL is PE001
Emoji is emoji1
Emoji is emoji2
Emoji is emoji3
URL is PE002
Emoji is emoji10
URL is PE003
Emoji is emoji20
Emoji is emoji21
D/ALBUMINFO: Album id is Album002 Title is This is the Second Album CreateBy Mary Enabled=true. It has 5 keywords. They are
keya
keyb
keyc
keyd
keye
. It has 2 emojis. They are
URL is PE011
Emoji is emoji30
Emoji is emoji31
URL is PE012
Emoji is emoji1
Emoji is emoji10
Emoji is emoji20
Emoji is emoji30
i.e. the 2 albums have been extracted along with the appropriate embedded lists.
The Albums table itself (via App Inspection) consists of :-
An Alternative, and from a Database perspective, better approach, instead of embedding lists as a single value (String), would have the lists as related tables (with a one-many or a many-many relationship).
My goal is to add a default value to constructor of the Room #Entity. The default value must depend on the language settings of the user. The way suggested by android framework is to use resource strings.
Here's the code I have:
#Entity
data class ArmyEntity(
#PrimaryKey(autoGenerate = true)
val armyId: Long,
val userOwnerId: Long,
val name: String = R.string.untitled, // wrong type
val description: String = R.string.no_description_yet, // wrong type
val iconUri: String = "",
val lastEdit: Timestamp = Timestamp(System.currentTimeMillis())
)
The two lines which interest me are labelled with the "wrong type" comments. Calling R.string.resource_string_name returns resource id, rather than the content of resource (returns Int, not String).
Android documentation suggests this way to get the string:
val string: String = getString(R.string.hello)
But the issue is that the getString() function is a member of the Context class and can be used in Activity. But Room Entity annotated doesn't know about context. (Context page for reference)
I tried passing context as a constructor parameter, but unfortunately, every constructor parameter in the data class has to be val or var. As long as I know, Room creates a column for every field in the class. What should I do to provide a language-dependent default value? Thank you!
FOR LOCAL RESOURCES
Define context in your Application class
class MyApplication : Application() {
companion object {
lateinit var context: Context
private set
}
override fun onCreate() {
super.onCreate()
context = this
}
}
and then just use MyApplication.context where you need
import com.example.myapp.R
import com.example.myapp.MyApplication
#Entity
data class ArmyEntity(
#PrimaryKey(autoGenerate = true)
val armyId: Long,
val userOwnerId: Long,
val name: String = MyApplication.context.getString(R.string.untitled),
val description: String = MyApplication.context.getString(R.string.no_description_yet)
val iconUri: String = "",
val lastEdit: Timestamp = Timestamp(System.currentTimeMillis())
)
NOTE : Android studio will warn you about a memory leak. Check this question for more informations
FOR SYSTEM RESOURCES
You can import Resources to access an application's resources with Resources.getSystem(), and the R class.
import android.content.res.Resources
import com.example.yourapp.R
#Entity
data class ArmyEntity(
#PrimaryKey(autoGenerate = true)
val armyId: Long,
val userOwnerId: Long,
val name: String = Resources.getSystem().getString(R.string.untitled),
val description: String = Resources.getSystem().getString(R.string.no_description_yet)
val iconUri: String = "",
val lastEdit: Timestamp = Timestamp(System.currentTimeMillis())
)
In my opinion you should use the id of that String. Change your DB texture a bit to follow this.
#Entity
data class ArmyEntity(
#PrimaryKey(autoGenerate = true)
val armyId: Long,
val userOwnerId: Long,
val descriptionRes: Int = R.string.abc, // this is true for multi-languages
val iconUri: String = "",
)
fun getDescription(context: Context,armyEntity : ArmyEntity,textView: TextView){
textView.text = context.getString(armyEntity.descriptionRes)
}
I'm trying to write a type converter for a List<Pair<Uri,Uri?>>:
Query("SELECT $uri, $imageUrl FROM $data WHERE $color IS NULL")
abstract fun findWithMissingColor(): List<Pair<Uri, Uri?>>
#TypeConverter
fun toUriPair(cursor: Cursor?): List<Pair<Uri, Uri?>> {
return cursor?.let{ generateSequence { if (it.moveToNext()) it else null }.map {
Pair(
Uri.parse(it.getString(0)),
try { Uri.parse(it.getString(1)) } catch (e: Exception) {null}
)
}.toList() } ?: emptyList()
}
which is loosely based on information from here.
But Room doesn't recognize this as a valid type converter for this query:
error: Not sure how to convert a Cursor to this method's return type (java.util.List<kotlin.Pair<android.net.Uri,android.net.Uri>>).
public abstract java.util.List<kotlin.Pair<android.net.Uri, android.net.Uri>> findWithMissingColor();
The core principal of the answer you used as the basis is that if more than 1 column is extracted that the column names match (duplicate column names are ambiguous):-
Room will automatically bind the parameters of the method into the bind arguments. This is done by matching the name of the parameters to the name of the bind arguments.
Query
Additionally a TypeConverter can only convert a single value :-
Each converter method should receive 1 parameter and have non-void return type
TypeConverter
EXAMPLE
Without going into the possibility that you may have serialised URI's and URL's, here's an example that may assist with you extracting the data, assuming that the URI's and URL's are plain Strings. i.e. no need for TypeConverters. Would obviously be different if using serialised URI's and URL's and BLOBS. Then you need a TypeConverters for each
The example is based upon what has been included in your question but as explained may well differ.
MyDataPair (an inner class of MyDataEntity) is the equivalent of the tuple/Pair with member names of first and second these being important as the column names when extract have to match these names (first point made above).
First the Entity which I've called MyDataEntity (this will probably differ but is basically the structure bar String V Blob) :-
#Entity(tableName = MyDataEntity.MYTABLE_TABLE_NAME)
class MyDataEntity{
/*
CONSTANTS for COLUMN AND TABLE NAMES
*/
companion object {
const val ID_COLUMN_NAME: String = "_id"
const val URI_COLUMN_NAME: String = "uri"
const val IMAGEURL_COLUMN_NAME: String = "urlimage"
const val COLOR_COLUMN_NAME: String = "color"
const val MYTABLE_TABLE_NAME: String = "mydata"
}
#PrimaryKey
#ColumnInfo(name = ID_COLUMN_NAME)
var id: Long? = null
#ColumnInfo(name = URI_COLUMN_NAME)
var uri: String? = null
#ColumnInfo(name = IMAGEURL_COLUMN_NAME)
var imageUrl: String? = null
#ColumnInfo(name = COLOR_COLUMN_NAME)
var color: Long? = null
constructor()
#Ignore
constructor(id: Long?,uri: String?,imageurl: String,color: Long?) {
this.id = id
this.uri = uri
this.imageUrl = imageurl
this.color = color
}
class MyDataPair(first: String?, second: String?) {
var first: String? = first
var second: String? = second
#Ignore
/* NOT USED/COMPLETE */
fun getFirstAsUri(): URI {
/* code here to build URI */
return URI(first)
}
#Ignore
/* NOT USED/COMPLETE */
fun getSecondAsUrl(): URL {
/* code here to build URL */
return URL(second)
}
}
}
The Dao MyDataDao (including your query AND specific output column names to suit the inner MyDataPair class (first and Second) in the MyDataEntity) :-
#Dao
interface MyDataDao {
/* For inserting test data */
#Insert
fun insertMyDataRow(myDataEntity: MyDataEntity): Long
/* Efectively your query BUT WITH specific output/result column names to suit the MyDataPair class */
#Query("SELECT ${MyDataEntity.URI_COLUMN_NAME} AS first,${MyDataEntity.IMAGEURL_COLUMN_NAME} AS second FROM ${MyDataEntity.MYTABLE_TABLE_NAME} WHERE ${MyDataEntity.COLOR_COLUMN_NAME} IS NULL")
fun findWithMissingColor(): List<MyDataEntity.MyDataPair>
/* Clear the table for renability */
#Query("DELETE FROM ${MyDataEntity.MYTABLE_TABLE_NAME}")
fun clearMyDataEntityTable(): Int
}
The Database MyDatabase
#Database(entities = [MyDataEntity::class],version = 1)
abstract class MyDatabase: RoomDatabase() {
abstract fun getMyDataDao(): MyDataDao
companion object {
const val DATABASE_NAME = "mydatabase"
}
}
Finally an Activity MainActivity putting it all together :-
class MainActivity : AppCompatActivity() {
lateinit var db: MyDatabase
lateinit var dao: MyDataDao
val TAG = "MYDBINFO"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = Room.databaseBuilder(this,MyDatabase::class.java,MyDatabase.DATABASE_NAME)
.allowMainThreadQueries()
.build()
dao = db.getMyDataDao()
var deleted_rows = dao.clearMyDataEntityTable()
/* CLEAR ANY EXIST DATALOAD AND LOAD SOME DATA */
Log.d(TAG, "$deleted_rows were deleted from the table ${MyDataEntity.MYTABLE_TABLE_NAME}")
dao.insertMyDataRow(MyDataEntity(null,"uri1","imageurl1", 0x00ffffff))
dao.insertMyDataRow(MyDataEntity(null,"uri2","imageurl2",null))
dao.insertMyDataRow(MyDataEntity(null,"uri3","imageurl3",0xAAAAAA))
dao.insertMyDataRow(MyDataEntity(null,"uri4","imageurl4",null))
/* EXTRACT ROWS WHERE COLOR IS NULL */
var results = dao.findWithMissingColor()
/* WRITE SOME INFO ABOUT THE EXTRACTED DATA TO THE LOG */
for(mdp: MyDataEntity.MyDataPair in results) {
Log.d(TAG,"STRING_URI = ${mdp.first} STRING_URL = ${mdp.second}")
}
}
}
runs of the main thread for convenience and brevity
inserts 4 rows 2 with color as NULL, 2 with a color
RESULTS from a run
The database after running is (as per Database Inspector) :-
The Log includes :-
2021-04-12 12:29:18.085 D/MYDBINFO: 4 were deleted from the table mydata
2021-04-12 12:29:18.099 D/MYDBINFO: STRING_URI = uri2 STRING_URL = imageurl2
2021-04-12 12:29:18.099 D/MYDBINFO: STRING_URI = uri4 STRING_URL = imageurl4
I'm learning Android Jetpack, the following code is from a sample project at https://github.com/android/sunflower.
The GardenPlanting.kt code is to design a table, I'm very strange why the author define the table fields in two position, you see that #PrimaryKey(autoGenerate = true) #ColumnInfo(name = "id") is located the inner of the class.
I think that Code B is easy to understand, right?
GardenPlanting.kt
#Entity(
tableName = "garden_plantings",
foreignKeys = [
ForeignKey(entity = Plant::class, parentColumns = ["id"], childColumns = ["plant_id"])
],
indices = [Index("plant_id")]
)
data class GardenPlanting(
#ColumnInfo(name = "plant_id") val plantId: String,
#ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(),
#ColumnInfo(name = "last_watering_date")
val lastWateringDate: Calendar = Calendar.getInstance()
) {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
var gardenPlantingId: Long = 0
}
Code B
data class GardenPlanting(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "id") val id: String,
#ColumnInfo(name = "plant_id") val plantId: String,
#ColumnInfo(name = "plant_date") val plantDate: Calendar = Calendar.getInstance(),
#ColumnInfo(name = "last_watering_date")
val lastWateringDate: Calendar = Calendar.getInstance()
) {
var gardenPlantingId: Long = 0
}
The declaration of a property inside the data class constructor is used to :
Generate component function for destructing
Use these fields inside the toString(), equals(), hashCode(), and copy()
So if you want to avoid copying fields with the copy method then the easy way is to declare the fields inside the body of the class.
Example:
fun main() {
val user = User("Pavneet", "29k+")
user.id = kotlin.random.Random.nextInt(10, 20)
val userCopy = user.copy()
println(userCopy) // id won't be printed 'cuz it's not a part of toString method
userCopy.id = 99
print(userCopy.equals(user)) // true, 'cuz id is not being used by the equals method
//var(name, repo, id) = userCopy // error, User must have a 'component3()' function
var(name, repo) = userCopy
}
data class User(val name: String = "", val repo:String="0"){
var id:Int = 0
}
Advantages:
Create a copies of the object excluding specific fields
Exclude specific fields to compare two objects with equals
Exclude specific fields in destructuring declarations
Note: copy and component methods cannot be provided explicitly(inside data class). In example B, gardenPlantingId is replaced with id so can be removed.
The answer is very simple:
Because in this sample code, the author wants to show that we can use both #PrimaryKey and #ColumnInfo annotation to ANY member inside an entity class, regardless its position (it can be in the inside the constructor or at the outside).
To experiment with this, you can just do exactly what you did on Code B. It is also valid, but in that case, gardenPlantingId will not have a custom column name because we don't use #ColumnInfo annotation. Also, it is not necessary to declare #PrimaryKey outside the constructor (like the given example). You can just declare your the primary key annotation inside the constructor.
I am trying to build an app to help me track some of the tasks we have to do in the game.
I have a Firebase Firestore database that store all the tasks and I download at the application launch the data and add only the one I don't have.
Here is my entry model:
#Entity(tableName = "entry_table")
data class Entry(
#PrimaryKey(autoGenerate = true) var uid: Long?,
#ColumnInfo(name = "title") val title: String,
#ColumnInfo(name = "description") val description: String,
#ColumnInfo(name = "target") val target: Int = 0,
#ColumnInfo(name = "position") val position: Int = 0,
#ColumnInfo(name = "starred") val starred: Boolean = false
) {
constructor(): this(null, "", "", 0, 0, starred = false)
}
Since I download the document from the firestore database I cannot set an ID before inserting the entries in my SQLite database.
This means that I cannot use the "contains" method on my livedata list (since the entries I recieve has a "null" id and the one from the database has an id). I need to loop though all the data, here is the code:
#WorkerThread
suspend fun insertEntry(entry: Entry) {
for (doc in entriesList.value!!){
if (doc.description == entry.description && doc.title == entry.title) {
Log.d("MAIN_AC", "Entry already saved $entry")
return
}
}
entryDAO.insertEntry(entry)
}
My code works but I am not satisfied with it, is there a better way to make this happen? I was hoping that the contains method could ignore some arguments (in my case the autogenerated ID)
One way you can go about, assuming you are using Room, it is to annotate your insert function (in the relevant DAO) with OnConflictStrategy.IGNORE.
e.g.
#Dao
interface EntryDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(list: List<Entry>)
// or (if you want the inserted IDs)
// fun insert(list: List<Entry>) : LongArray
}
Be sure to also annotate your entity with the relevant unique index.
e.g.
#Entity(tableName = "entry_table",
indices = [Index(value = ["title", "description"], unique = true)]
)
data class Entry(
#PrimaryKey(autoGenerate = true) var uid: Long,
#ColumnInfo(name = "title") val title: String,
#ColumnInfo(name = "description") val description: String
//...
)
Primary keys should not be null-able, you can .map to Entry wit uid = 0. If you are using the same entity model both locally and remotely that is probably not the best idea.