Android Room - insert list of sealed classes .. or anything - android

I have a list of items like the below that I would like to enter into a database using room.
data class MyRotasDayItem(
#PrimaryKey
#SerializedName("id")
val id: Long,
#SerializedName("date")
val date: String,
#Embedded
#SerializedName("dayEvents")
val dayEvents: List<SealedObj>
)
However I cant seem to add dayEvents. Even if I made the type List I get...
Entities and POJOs must have a usable public constructor
Do i have to use a type converter?
What if in the list Type is a Sealed class that contain other data objects like...
sealed class MySealedExample(
open val foo: Long,
open val bar: Long
) {
#PrimaryKey(autoGenerate = true)
var id: Int = 0
#Entity
data class AnExample1(
#Ignore override val foo: Long,
#Ignore override val bar: Long,
val something:String
) : MySealedExample(foo, bar)
#Entity
data class AnExample2(
#Ignore override val foo: Long,
#Ignore override val bar: Long,
val somethingElse:List<SomeObj>
) : MySealedExample(foo, bar)
}
Anyway to insert that into the database?
Thankyou

Use type converters, I ran into a similar problem and fixed it using type converters.
To convert sealed classes into string and vice versa, I used Gson extension from this gist.
#JvmStatic
#TypeConverter
fun sealedClassToString(sealedClass: SealedClass) : String = GsonExtension.toJson(sealedClass)
#JvmStatic
#TypeConverter
fun sealedClassFromString(sealedClass: String) : SealedClass = sealedClass.let { GsonExtension.fromJson(it) }

Related

Problem in TypeConverters In Room Database

I am trying to use type converters in Android (Kotlin) so i am using the type converters class but i am getting confused like inside of the clouds i am having a single variable so i have returned it but
#Entity(tableName = "WeatherDb")
data class WeatherDTO(
val base: String,
val clouds: Clouds,
val cod: Int,
val coord: Coord,
val dt: Int,
#PrimaryKey(autoGenerate = true)
val id: Int,
val main: Main,
val name: String,
val sys: Sys,
val timezone: Int,
val visibility: Int,
val weather: List<Weather>,
val wind: Wind
)
class TypeConverters {
#TypeConverter
fun fromCloudsToDouble(clouds: Clouds): Int {
return clouds.all
}
fun fromCoordToDouble(coord: Coord): Double {
}
}
In coord class here are multiple with different datatypes how to covert this?
data class Main(
val feels_like: Double,
val grnd_level: Int,
val humidity: Int,
val pressure: Int,
val sea_level: Int,
val temp: Double,
val temp_max: Double,
val temp_min: Double
)
Clouds.kt
data class Clouds(
val all: Int
)
Coord.kt
data class Coord(
val lat: Double,
val lon: Double
)
Main.kt
data class Main(
val feels_like: Double,
val grnd_level: Int,
val humidity: Int,
val pressure: Int,
val sea_level: Int,
val temp: Double,
val temp_max: Double,
val temp_min: Double
)
Sys.kt
data class Sys(
val country: String,
val id: Int,
val sunrise: Int,
val sunset: Int,
val type: Int
)
Weather.kt
data class Weather(
val description: String,
val icon: String,
val id: Int,
val main: String
)
Wind.kt
data class Wind(
val deg: Int,
val gust: Double,
val speed: Double
)
WeatherViewModel.kt
#HiltViewModel
class WeatherViewModel #Inject constructor(
private val repo:WeatherRepository,
private val application: Application,
private val WeatherDb:WeatherDB,
private val fusedLocationProviderClient: FusedLocationProviderClient
) :ViewModel(){
private val _resp = MutableLiveData<WeatherDTO>()
val weatherResp:LiveData<WeatherDTO>
get() = _resp
private val _cord = MutableLiveData<Coord>()
val cord:LiveData<Coord>
get() = _cord
var locality:String = ""
fun getWeather(latitude:Double,longitude:Double) =
viewModelScope.launch {
repo.getWeather(latitude,longitude).let { response->
if(response.isSuccessful){
Log.d("response","${response.body()}")
WeatherDb.WeatherDao().insertWeather(response.body()!!)
_resp.postValue(response.body())
}else{
Log.d("Weather Error","getWeather Error Response: ${response.message()}")
}
}
}
fun fetchLocation():Boolean{
val task = fusedLocationProviderClient.lastLocation
if(ActivityCompat.checkSelfPermission(application,android.Manifest.permission.ACCESS_FINE_LOCATION)
!=PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(application,android.Manifest.permission.ACCESS_COARSE_LOCATION)
!=PackageManager.PERMISSION_GRANTED
){
return true
}
task.addOnSuccessListener {
if(it!=null){
getWeather(it.latitude,it.longitude)
getAddressName(it.latitude,it.longitude)
Log.d("localityname", locality)
}
}
return true
}
private fun fetchLocationDetails(){
}
private fun getAddressName(lat:Double,long:Double):String{
var addressName = " "
val geoCoder = Geocoder(application, Locale.getDefault())
val address = geoCoder.getFromLocation(lat,long,1)
if (address != null) {
addressName = address[0].adminArea
}
locality = addressName
Log.d("subadmin",addressName.toString())
Log.d("Address", addressName)
return addressName
}
fun getCoordinates(cord:String){
val geocoder = Geocoder(application,Locale.getDefault())
val address = geocoder.getFromLocationName(cord,2)
val result = address?.get(0)
if (result != null) {
getWeather(result.latitude,result.longitude)
getAddressName(result.latitude,result.longitude)
}
}
}
Here is my converter in the Kotlin:
class Converters {
#TypeConverter
fun valueFromDomainToStorage(value: Value): String {
return value.convertToJson()
}
#TypeConverter
fun valueFromStorageToDomain(str: String): Value {
// we can not create an empty instance of value as TypeDecoder.java should call non-empty constructor
return Value(
"just a stub",
BigInteger.valueOf(0),
BigInteger.valueOf(0),
false,
BigInteger.valueOf(0)
)
.fromJson(str)
}
}
where .convertToJson() and .fromJson(str) implemented as extensions within Value class:
fun Value.convertToJson(): String {
val result = JSONObject()
result.put(ValueConst.OFFER_FIELD, offer)
result.put(ValueConst.AVAILABLE_SINCE, availableSince.toLong())
result.put(ValueConst.AVAILABLE_END, availabilityEnd.toLong())
result.put(ValueConst.IS_CONSUMED, isConsumed)
result.put(ValueConst.LOCKED_UNTIL, lockedUntil)
return result.toString()
}
fun Value.fromJson(json: String): Value {
val subj = JSONObject(json)
return Value(
subj.optString(ValueConst.OFFER_FIELD),
BigInteger.valueOf(subj.optLong(ValueConst.AVAILABLE_SINCE)),
BigInteger.valueOf(subj.optLong(ValueConst.AVAILABLE_END)),
subj.optBoolean(ValueConst.IS_CONSUMED),
BigInteger.valueOf(subj.optLong(ValueConst.LOCKED_UNTIL))
)
}
You should implement Converter class for each non-native class type. Do not forget to register your converters on database:
#Database(entities = [ChainTransaction::class], version = 1, exportSchema = false)
#TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
When you have compile the code and later introduce new changes, you have to increase version parameter too to make changes to take effect:
#Database(entities = [ChainTransaction::class], version = 2, exportSchema = false)
#TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
Here is official documentation and even training on this topic:
https://developer.android.com/training/data-storage/room
so i am using the type converters class but i am getting confused
SQLite (the database around which Room is an object orientated wrapper) is not an object orientated (or aware) database. It is a database that can store primitive types of data which are one of
INTEGER (such as Int or Long), REAL
REAL (such as Float or Double)
TEXT (such as String)
BLOB (such as ByteArray)
NULL
Therefore to store a type of Coord, Cloud or Weather .... you have three options:-
to embed the class, in which case the fields are copied from the embedded class (would be complicated if the embedded classes contained unsupported types). not covered in the answer
to have the class as a table in it's own right with a relationship between it and the parent (WeatherDTO). not covered in the answer
to convert the class to one of the SQLite types (of which either TEXT or BLOB would probably only be practical).
Considering option 3 (TyepConverters) converting the data is of little, if any, use just storing the data as you would not be able to retrieve the data.
As such type converters should always be paired.
One of the pair will be to convert from the class to a type that can be stored.
The other will be to convert from the stored type to the class.
As such you will need quite a few type Converters, that is 2 each for fields:-
clouds (class Clouds)
coord (class Coord)
main (class Main)
sys (class Sys)
weather (class List)
wind (class Wind)
It is the Class of the field that Room looks at to locate the respective type converter.
One of the simplest ways to convert objects (aka classes) is to convert the object to a JSON representation. Although a complexity with this is that there are many JSON libraries and they will often have differences.
For the examples that follow Googles JSON library has been used. However, use of this library with Room doesn't appear to directly support the use of List<the_class> e.g. List.
The dependency for this being (as an example) implementation 'com.google.code.gson:gson:2.10'
As a get around a new class WeatherList has ben used as per:-
data class WeatherList(
val weatherList: List<Weather>
)
and the WeatherDTO class has been changed to use it as per :-
....
//val weather: List<Weather>,
val weather: WeatherList,
....
As such the TypeConverters class could then be:-
class TypeConverters {
#TypeConverter
fun fromCloudsToJSONString(clouds: Clouds): String = Gson().toJson(clouds)
#TypeConverter
fun toCloudsFromJSONString(jsonString: String): Clouds = Gson().fromJson(jsonString,Clouds::class.java)
#TypeConverter
fun fromCoordToJSONString(coord: Coord): String = Gson().toJson(coord)
#TypeConverter
fun toCoordFromJSONString(jsonString: String): Coord = Gson().fromJson(jsonString,Coord::class.java)
#TypeConverter
fun fromMaintoJSONString(main: Main): String = Gson().toJson(main)
#TypeConverter
fun toMainFromJSONString(jsonString: String): Main = Gson().fromJson(jsonString,Main::class.java)
#TypeConverter
fun fromSysToJSONString(sys: Sys): String = Gson().toJson(sys)
#TypeConverter
fun toSysFromJSONString(jsonString: String): Sys = Gson().fromJson(jsonString,Sys::class.java)
#TypeConverter
fun fromWeatherListFromJSONString(weatherList: WeatherList): String = Gson().toJson(weatherList)
#TypeConverter
fun toWeatherListFromJSOnString(jsonString: String): WeatherList = Gson().fromJson(jsonString,WeatherList::class.java)
#TypeConverter
fun fromWindToJSONString(wind: Wind): String = Gson().toJson(wind)
#TypeConverter
fun toWindFromJSONString(jsonString: String): Wind = Gson().fromJson(jsonString,Wind::class.java)
}
As such the all the types/classes/objects that are not directly supported are converted to/from a JSON string representation of the type/class/object.
Note that you need to add the #TypeConverters(#TypeConverters( value = [<????>.TypeConverters::class]). Where has to distinguish between your projects TypeConverters class from Room's (TypeConverters is probably not the best name for the class, renaming it, would overcome the need to distinguish)
Working Example
The following puts the above into action.
As the question does not include the underlying classes, the following have been used:-
data class Coord(
val longitude: Double,
val latitude: Double
)
data class Clouds(
val cover: Double,
val type: String
)
data class Main(
val main: Double
)
data class Sys(
val sys: Double
)
data class WeatherList(
val weatherList: List<Weather>
)
data class Weather(
val weather: Double
)
data class Wind(
val wind: Double
)
The #Dao annotated interface was also made up and is simply:-
#Dao
interface AllDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(weatherDTO: WeatherDTO)
#Query("SELECT * FROM weatherdb")
fun getAllFromWeatherDB(): List<WeatherDTO>
}
Also the #Database annotated abstract class was made up it being:-
#TypeConverters( value = [a.a.so74384736typeconverterconfusion.TypeConverters::class])
#Database(entities = [WeatherDTO::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDao(): AllDao
companion object {
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Note the package name used to distinguish the TypeConverters class from Room's TypeConverters class
the package name cannot be used elsewhere, so if the above is copied then it would have to be changed. There is no expectation that the code in it's entirety would be copied and used. The code is designed solely to demonstrate the TypeConverters.
Last some activity code to actually do something (store and retrieve some data):-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDao()
dao.insert(
WeatherDTO(
"base001",
Clouds(25.5,"cumulus"),10,
Coord(10.567,30.345),
11,
12,
Main(12345.67890),
"thename",
Sys(9.87654321),
14,
1000,
WeatherList(listOf(Weather(5.1234),Weather(6.5432), Weather(7.6543))),
Wind(23.12)
)
)
for (wdto in dao.getAllFromWeatherDB()) {
Log.d("DBINFO","base = ${wdto.base} longitude = ${wdto.coord.longitude} latitude = ${wdto.coord.latitude} etc ....")
}
}
}
RESULT
When run the log contains, as expected:-
D/DBINFO: base = base001 longitude = 10.567 latitude = 30.345 etc ....
Using App Inspection then the database looks like:-
The fields converted to a JSON string have been highlighted.
Obviously the data will very likely not exactly be as you would expect due to the made up classes.
Follow on from the previous answer #Embedded versus Type Converters
As can be seen from the previous answer, there are some issues in regard to using TypeConverters. From a database perspective the TypeConverters will inevitably contain bloat/unecessary data which is contrary to normalisation (not needlessly storing repetitive data).
As an example the JSON representation will for every row contain exactly the same the field names wasting storage, all rows will have the additional overhead of storing the delimiters ([s and ]s, {s and }s, :s ,s). Furthermore actually using the stored data can become complex due to the bloat and also due to multiple values being stored in a single column and as such can be restrictive.
It would be more efficient to not store the bloat and it could eliminate complexities and enhance the usability of the stored data from a database perspective (querying the data for retrieval) to not store multiple values in a single column.
Using the #Embedded annotation can very easily eliminate the bloat. Consider the following (an alternative version of the WeatherDTO class/entity):-
#Entity(tableName = "WeatherDbAlternative1")
data class WeatherDTOAlternative1(
val base: String,
#Embedded
val clouds: Clouds,
val cod: Int,
#Embedded
val coord: Coord,
val dt: Int,
#PrimaryKey(autoGenerate = true)
val id: Int,
#Embedded
val main: Main,
val name: String,
#Embedded
val sys: Sys,
val timezone: Int,
val visibility: Int,
//val weather: List<Weather>,
/* Unable to embed directly so not embedding */
val weather: WeatherList,
#Embedded
val wind: Wind
)
Bar the weather field all that has been done is add the #Embedded annotation. Noting that the classes of the fields all have fields of types directly supported by Room.
Adding this entity to the #Database annotation and adding a couple of additional functions in the #Dao annotated class as per:-
#Query("SELECT * FROM weatherdbalternative1")
fun getAllFromWeatherDBAlternative1(): List<WeatherDTOAlternative1>
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(weatherDTOAlternative1: WeatherDTOAlternative1)
And then amending the Activity code to include :-
/*ALTERNATIVE 1 All but WeatherList embedded */
dao.insert(
WeatherDTOAlternative1(
"base001A",
Clouds(25.5, "cumulus"),
10,
Coord(10.567, 30.345),
11,
12,
Main(12345.67890),
"thenameA1",
Sys(9.87654321),
14,
1000,
WeatherList(listOf(Weather(5.1234), Weather(6.5432), Weather(7.6543))),
Wind(23.12)
)
)
for (wdto in dao.getAllFromWeatherDBAlternative1()) {
Log.d(
"DBINFO",
"base = ${wdto.base} longitude = ${wdto.coord.longitude} latitude = ${wdto.coord.latitude} etc ...."
)
}
Now results in the Log including:-
D/DBINFO: base = base001 longitude = 10.567 latitude = 30.345 etc ....
D/DBINFO: base = base001A longitude = 10.567 latitude = 30.345 etc ....
i.e. effectively the same data is stored and retrievable
However the data is now stored in the database as (ignoring the weather field) as :-
i.e. the data stored is much cleaner but at the expense of additional columns (which can be advantageous).
additionally although not apparent, the fields that have the #Embedded annotation do not need the TypeConverters.

RoomDB Kotlin enum needs a constructor any way around this?

I am getting this error
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 enum Measurement {
the enum will simply be a way to store answers from a dropdown.
#Entity(tableName = "bottles")
data class Bottle(
#PrimaryKey (autoGenerate = true)
val consumableID: String,
#Embedded var prescription : Prescription?,
#Embedded var measurement: Measurement?,
#Embedded var reminder: Reminder?,
var quantityInBottle: Int?,
val expirationDate : Date?,
var startDate : Date?,
val cabinetID: String
)
enum class Measurement {
MILLIGRAMS,
SCOOP,
GRAMS,
OZ,
TSP,
TBS,
}
I have TypeConverters for measurement defined as
class MeasurementTypeConverters {
#TypeConverter
fun measurementToString(measurement: Measurement?) : String{
return measurement.toString()
}
#TypeConverter
fun stringToMeasurement(string: String) : Measurement{
return Measurement.valueOf(string)
}
}

Android Room: Entities and POJOs must have a usable public constructor

I'm trying to save the data class i got from json to room database and suddenly get errors like this while building the app.
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).
NewsResponse model object
data class NewsResponse(
val copyright: String,
val response: Response,
val status: String
)
Response object
data class Response(
val docs: List<Docs>
)
Docs object(this is the data class i want to save)
#Entity(
tableName = "docs"
)
data class Docs(
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
val _id: String,
val `abstract`: String,
#Embedded
val byline: Byline,
val document_type: String,
#Embedded
val multimedia: Multimedia,
#Embedded
val headline: Headline,
val lead_paragraph: String,
val news_desk: String,
val pub_date: String,
val section_name: String,
val snippet: String,
val source: String,
val subsection_name: String,
val type_of_material: String,
val uri: String,
val web_url: String,
val word_count: Int
)
Byline object
#Entity
data class Byline(
val original: String
)
Multimedia object
#Entity
data class Multimedia(
val url: String
)
Headline object
#Entity
data class Headline(
val main: String
)
Any ideas how to solve this?

Room - Android. How to request data from multiple entities?

I have 4 different entities with the same type of data..
class {
val Int
val String
val String
}
I am using ViewModel to request the data and right now I have for Observables which updates the list adapter.
tagsViewModel.getAllText().observe(this,
Observer<List<Text>> { t -> adapter.setTags(t!!) })
My problem is that I am having some troubles when updating the ui so I wanted to just do one request to get the 4 different types of entities but I don't know how to get one only list with all different classes.
This is a class type
#Entity(tableName = "text")
data class Text(override var content: String, override var date: Long, override var type: String = AppConstants.TYPE_TEXT) : BaseTag() {
#PrimaryKey(autoGenerate = true)
override var id: Int = 0
}
and Base interface
abstract class BaseTag {
abstract val content: String?
abstract val date: Long?
abstract val id: Int?
abstract val type: String?
}
I would like to do this request:
{ texts : [Text,Text,Text],
emails : [Email,Email,Email]...
}
So... Is there any simple way to do this?
Thanks for any help.

Cannot figure out how to save this field into database, save a list of Integer

I have this entity :
#Entity(tableName = "recipes")
data class Recipe(
#PrimaryKey val id: Int,
val name: String,
#TypeConverters(Converters::class)
val categories: List<Int>,
val picture: String,
#Embedded(prefix = "time")
val time: Time,
val people: Int,
val difficulty: Int,
val price: Int,
#Embedded(prefix = "ingredients")
#TypeConverters(Converters::class)
val ingredients: List<Ingredient>,
#Embedded(prefix = "utensils")
#TypeConverters(Converters::class)
val utensils: List<Utensil>,
#Embedded(prefix = "steps")
#TypeConverters(Converters::class)
val steps: List<Step>,
val createdAt: String,
val updatedAt: String
) : Serializable
And when I compile it, I got
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
private final java.util.List<java.lang.Integer> categories = null;
Problem is, I added the TypeConverter, it's in my class Converters :
#TypeConverter
fun fromIntList(value: List<Int>): String {
val gson = Gson()
val type = object : TypeToken<List<Int>>() {}.type
return gson.toJson(value, type)
}
#TypeConverter
fun toIntList(value: String): List<Int> {
val gson = Gson()
val type = object : TypeToken<List<Int>>() {}.type
return gson.fromJson(value, type)
}
It should work since it knows that it should transforma list of Int to strings.
Futhermore, I got this error which I dont know where it can come from, and I have it like 5 times.
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 added the nested class like Ingredient, Step, Ustensil but I dont post the code since it should not be related.

Categories

Resources