Android Room and Kotlin Boolean Array - android

I’ve reviewed a lot of Stack answers related to this and I’m still not getting it. I have an #Embedded class, but I’d rather this be a Boolean array with three elements. Would someone please hit me with a clue stick and help me redesign this entity to handle this or provide the TypeConverter I need? I’d rather not use a JSON/GSON converter if I can avoid it.
data class Bools (val a: Boolean = true,
val b: Boolean = true,
val c: Boolean = false)
#Entity(tableName = "people_table")
data class Person (#ColumnInfo(name = "first_name") val firstName: String,
#ColumnInfo(name = "last_name") val lastName: String,
#Embedded
val bool: Bools
){
#PrimaryKey(autoGenerate = true)
var id: Int = 0
}
Thank you.

In general, I do not recommend that Room entities, Retrofit responses, and similar things be considered your in-memory data model. They are data transfer objects, as they are subject to limitations that your UI and in-app logic should not need to deal with. In the case of something like Retrofit, the way the data is organized and delivered by the server may bear little resemblance to how you want to work with the data in the app. In your case, it's that you want three distinct columns, which means three distinct Kotlin properties, whether in the entity or in an #Embedded object, as you have it.
A typical approach is to have a PersonDTO or PersonEntity or something be what Room uses, which you convert to/from Person objects that have your desired structure:
data class Person (val firstName: String,
val lastName: String,
val boolsheet: BooleanArray)
#Entity(tableName = "people_table")
data class PersonEntity (#ColumnInfo(name = "first_name") val firstName: String,
#ColumnInfo(name = "last_name") val lastName: String,
val a: Boolean = true,
val b: Boolean = true,
val c: Boolean = false
){
constructor(somebody: Person): this(
somebody.firstName,
somebody.lastName,
somebody.boolsheet[0],
somebody.boolsheet[1],
somebody.boolsheet[2]
)
#PrimaryKey(autoGenerate = true)
var id: Int = 0
fun toPerson(): Person = Person(firstName, lastName, booleanArrayOf(a, b, c))
}
Now, Person and everything that deals with it knows nothing about Room, and you have the API that you want. PersonEntity would be used by your repository, hiding the details. And, if someday you need to have the repository also talk to a server, the repository can normalize between Person and the representation that you need for your Web service API.
If you don't like that, stick with your Person and #Embedded, and add a val boolsheet = booleanArrayOf(bools.a, bools.b, bools.c) to it, to get your Boolean values in a iterable structure.

Related

Android - How to fetch sub list based on WHERE conditions from Room DB?

I am building an Android application in which I would like to fetch the list of active devices under the project manager.
Trying to put it in different way for better understanding
Project Manager table has list of employees
Employee table has list of devices
Now, we need the list of Project Managers with list of employees with device status either with 1 or 0 based on UI selection.
Entities
#Entity(tableName = TABLE_PROJECT_MANAGER)
data class ProjectManager(
#PrimaryKey
val id: String,
val firstName: String?,
val middleName: String?,
val lastName: String?,
#TypeConverters(EmployeesConverter::class)
var employees: List<Employee>
)
#Parcelize
data class Employee(
val id: String,
val name: String?,
#TypeConverters(DeviceListTypeConverter::class)
val devices : List<Device>? = null
)
#Parcelize
data class Device(
#ColumnInfo(name = "device_id")
#SerializedName("id")
val id: String,
val manufacturer: String?,
val model: String?,
val status: Int,
) : Parcelable
Type Converters:
EmployeesConverter
class EmployeesConverter {
private val moshi = Moshi.Builder().build()
private val membersType = Types.newParameterizedType(List::class.java, Employee::class.java)
private val membersAdapter = moshi.adapter<List<Employee>>(membersType)
#TypeConverter
fun stringToMembers(member: String?): List<Employee>? {
return member?.let {
membersAdapter.fromJson(member)
}
}
#TypeConverter
fun membersToString(members: List<Employee>?): String? {
return members?.let {
membersAdapter.toJson(members)
}
}
}
DeviceListTypeConverter
class DeviceListTypeConverter {
private val moshi = Moshi.Builder().build()
private val membersType = Types.newParameterizedType(List::class.java, Device::class.java)
private val membersAdapter = moshi.adapter<List<Device>>(membersType)
#TypeConverter
fun stringToMembers(member: String?): List<Device>? {
return member?.let {
membersAdapter.fromJson(member)
}
}
#TypeConverter
fun membersToString(members: List<Device>?): String? {
return members?.let {
membersAdapter.toJson(members)
}
}
}
I am little confused on how to achieve this. Please help me out on this.
With what you currently have, you will need a query that has a WHERE clause that will find the appropriate status within the employees column. This is dependant upon how the Type Converter converts the List and the List.
This could be along the lines of:-
#Query("SELECT * FROM $TABLE_PROJECT_MANAGER WHERE instr(employees,'status='||:status)")
fun findProjectManagerWithDevicesAccordingToDeviceStatus(status: String): List<ProjectManager>
NOTE the above will very likely not work as is, you will very likely have to change 'status='||:status according to how the TypeConverter converts the employee list and the device list into the single employees column.
You would call the function with "0" or "1" respectively.
Of course you could use Int for status (Room will convert it to an SQLite string anyway)
In short you are embedding a List with an embedded List into a single value and thus finding the anecdotal needle in that haystack is complicated.
If this were approached from a database perspective then you would have tables (#Entity annotated classes) for each of the List's and as the relationships are probably many-many then tables that map/reference/associate/relate.
So rather than just the ProjectManager table, you would have an Employee table and a Device table and then a table for the mapping of a ProjectManager to the Employee(s) and a table for mapping the Employee to the Device(s). In which case you would have columns with specific values that can be queried relatively efficiently rather than an inefficient search through a complex single relatively large value bloated by the inclusion of data needed solely for the conversion to/from the underlying objects.

Is it ok to use interfaces as domain models instead of data classes on Android using Kotlin? (Clean Architecture and Domain Driven Design)

I'm trying to apply clean architecture on my android projects, but it is quite complex, now I'm stuck on this, according to Uncle Bob you should use a model for every layer (domain, data, presentation - in this case), so, typically we create a data class for the domain layer, let's say "Movie":
data class Movie(
private val id: Int = 0,
private val name: String = "",
private val imageUrl: String = ""
)
So, later in our presentation layer we should have a model to be able to retrieve data from the network, we'll name it MovieDto and it looks like this:
data class MovieDto(
#Json(name = "id")
private val id: Int = 0,
#Json(name = "name")
private val name: String = "",
#Json(name = "image_url")
private val imageUrl: String = ""
)
Then every time we fetch data using Retrofit we should be getting a MovieDto object in return, but our MovieRepository in our domain layer looks this way:
interface RepositoryMovies {
suspend fun getMovie(id: Int): Movie
}
With the previous code we already stablished a contract agreeing to always return a Movie object (remember that inner layers should not know anything about outter layers, so our repository doesn't even know a MovieDto class exists), in order to be able to return a Movie object the repository should use a mapper to convert/map the MovieDto object comming from Retrofit to a Movie object. To avoid all of that I used an interface as my domain model, let me show you:
interface IMovie {
val id: Int,
val name: String,
val imageUrl: String
}
The only thing we have to do next is to implement the IMovie interface on our MovieDto class:
data class MovieDto(
#Json(name = "id")
private val id: Int = 0,
#Json(name = "name")
private val name: String = "",
#Json(name = "image_url")
private val imageUrl: String = ""
) : IMovie
Now our repository should return a IMovie interface instead:
interface RepositoryMovies {
suspend fun getMovie(id: Int): IMovie
}
With no need of mappers but still forcing "domain rules" by implementing the IMovie interface. I ask you to point me in the right direction, maybe I'm not seeing that this approach is not as flexible or scalable as I tought, what approach would you use? please feel free to comment any pros and cons of each approach.
Thank you for your time.

Many to many android room ref extra variable

I have a Many to Many relationship set up on my Room database denoted by the following diagram:
eveything works great but I would like to make the following addition:
My question is how do I go about having RecipeWithIngredients get this unit:String variable from the RecipeIngredientRef Entity when a recipeWithIngredients is constructed on a query?
#Entity(indices = [Index(value = ["name"],unique = true)])
data class Recipe(
var name: String,
val image: String?
) {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "recipeID")
var ID: Long = 0
}
#Entity(indices = [Index(value = ["name"],unique = true)])
data class Ingredient(
val name: String,
val image: String?,
val amount: Int
) {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "ingredientID")
var ID: Long = 0
}
#Entity(primaryKeys = ["recipeID", "ingredientID"])
data class RecipeIngredientRef(
val recipeID: Long,
val ingredientID: Long,
val unit: String
)
data class RecipeWithIngredients(
#Embedded
val recipe: Recipe,
#Relation(
parentColumn = "recipeID",
entity = Ingredient::class,
entityColumn = "ingredientID",
associateBy = Junction(
value = RecipeIngredientRef::class,
parentColumn = "recipeID",
entityColumn = "ingredientID"
)
)
val ingredients: List<Ingredient>
)
As far as I know there is no built-in way using current many-to-many Relations in Room for that. So I just want to save your time for researches.
This RecipeIngredientRef table is used internally just for two other tables' join and for now adding there additional fields will not help to get these fields with #Relations/Junction mechanism.
You can try workarounds, but they are no so elegant and they needed to dig deeper into Room's result's processing:
Don't use #Relation/Junction mechanism for RecipeWithIngridients at all, write your own query with two JOINs (since you should get data from 3 tables). As a result you'll get some raw set of records and then you should use loops and methods of Kotlin collections to turn result into needed form.
Use #Relation/Junction mechanism, but after getting result - make additional processing of result - make one more query to RecipeIngredientRef table and set missing unit value manually (again with loops and methods of Kotlin collections).

Room Livedata returns incorrect values

I have an audio recorder app, where I enable the user to mark certain points in his recordings with predefined markers. For that purpose I have a MarkerEntity, which is the type of Marker, and a MarkTimestamp, the point at which the user marks a given recording. These entities are connected via a Relation, called MarkAndTimestamp.
#Entity(tableName = "markerTable")
data class MarkerEntity(
#PrimaryKey(autoGenerate = true) val uid: Int,
#ColumnInfo(name = "markerName") val markerName: String
)
#Entity(tableName = "markerTimeTable")
data class MarkTimestamp(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "mid") val mid: Int,
#ColumnInfo(name = "recordingId") val recordingId: Int,
#ColumnInfo(name = "markerId") val markerId: Int,
#ColumnInfo(name = "markTime") val markTime: String
)
data class MarkAndTimestamp(
#Embedded val marker: MarkerEntity,
#Relation(
parentColumn = "uid",
entityColumn = "markerId"
)
val markTimestamp: MarkTimestamp
)
The insertion of this data works flawlessly, I checked this via DB Browser for SQLite and Android Debug Database. The problem arises, when I want to display all marks for a recording. I fetch the entries with the following SQL statement.
#Transaction
#Query("SELECT * FROM markerTimeTable INNER JOIN markerTable ON markerTimeTable.markerId=markerTable.uid WHERE markerTimeTable.recordingId = :key")
fun getMarksById(key: Int): LiveData<List<MarkAndTimestamp>>
What ends up happening is, that if the user uses a Marker more than once, all marks created with that Marker have the same MarkerTimestamp row attached to them, specificially, the last row to be inserted with that Marker. The weird thing is, this only happens in the app using Livedata. Using the same query in DB Browser for SQLite returns the correct and desired data.
This is the stored data (correct)
MarkTimestamps
MarkerEntities
And this is the Livedata returned at this point (incorrect)
[
MarkAndTimestamp(marker=MarkerEntity(uid=1, markerName=Mark), markTimestamp=MarkTimestamp(mid=6, recordingId=2, markerId=1, markTime=00:05)),
MarkAndTimestamp(marker=MarkerEntity(uid=2, markerName=zwei), markTimestamp=MarkTimestamp(mid=5, recordingId=2, markerId=2, markTime=00:03)),
MarkAndTimestamp(marker=MarkerEntity(uid=1, markerName=Mark), markTimestamp=MarkTimestamp(mid=6, recordingId=2, markerId=1, markTime=00:05))
]
I also get the following build warning
warning: The query returns some columns [mid, recordingId, markerId, markTime] which are not used by de.ur.mi.audidroid.models.MarkAndTimestamp. You can use #ColumnInfo annotation on the fields to specify the mapping. You can suppress this warning by annotating the method with #SuppressWarnings(RoomWarnings.CURSOR_MISMATCH). Columns returned by the query: mid, recordingId, markerId, markTime, uid, markerName. Fields in de.ur.mi.audidroid.models.MarkAndTimestamp: uid, markerName. - getMarksById(int) in de.ur.mi.audidroid.models.MarkerDao
Why does Room return the wrong data and how do I fix this?
So, I still don't know what caused the behaviour described in my original post. My guess is, that the realtion data class and the SQL query interfered in some way, producing the cinfusing and incorrect outcome.
I solved my problem nonetheless. I needed to change
data class MarkAndTimestamp(
#Embedded val marker: MarkerEntity,
#Relation(
parentColumn = "uid",
entityColumn = "markerId"
)
val markTimestamp: MarkTimestamp
)
to
data class MarkAndTimestamp(
#Embedded val marker: MarkerEntity,
#Embedded val markTimestamp: MarkTimestamp
)
This makes sure, that all fields returned by the query are included in the data class.

Is it possible in Room to ignore a field on a basic update

I have the following entity:
#Entity
class Foo(
#PrimaryKey
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "thing1")
val thing1: String,
#ColumnInfo(name = "thing2")
val thing2: String,
#ColumnInfo(name = "thing3")
val thing3: String,
#ColumnInfo(name = "thing4")
val thing4: String
) {
#ColumnInfo(name = "local")
var local: String? = null
}
Where local is information that is not stored on the server, only local to the phone.
Currently when I pull information from the server GSON auto fills in my values, but since "local" does not come from the server it is not populate in that object.
Is there a way that when I call update I can have Room skip the update for the "local" column without writing a custom update to insert into all other columns except "local"? The pain point is that I could have many columns and each new column I add, I would have to add that to the custom insert statement.
I have also thought of a one-to-one mapping from the server entity to a new "local" entity, however now I have to deal with the pain of a join statement everywhere I get my entity since I need the local information.
I was hoping that I could do something like this:
#Entity
class Foo(
#PrimaryKey
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "thing1")
val instructions: String,
#ColumnInfo(name = "thing2")
val instructions: String,
#ColumnInfo(name = "thing3")
val instructions: String,
#ColumnInfo(name = "thing4")
val instructions: String
) {
#Ignore
var local: String? = null
}
Using the #Ignore annotation, to try and ignore the local string on a generic update. Then provide a custom update statement to just save the local info
#Query("UPDATE foo SET local = :newLocal WHERE foo.id = :id")
fun updateLocal(id: Long, newLocal: String)
However ROOM seems to be smart enough to check that I used #Ignore on the local property and it will not compile with that update statement.
Any ideas?
Partial Updates got added to Room in 2.2.0
In Dao you do the following:
// Here you specify the target entity
#Update(entity = Foo::class)
fun update(partialFoo: PartialFoo)
And along your entity Foo create a PartialFoo containing the primary key and the fields you want to update.
#Entity
class PartialFoo {
#ColumnInfo(name = "id")
val id: Long,
#ColumnInfo(name = "thing1")
val instructions: String,
}
https://stackoverflow.com/a/59834309/1724097
Simple answer is NO. Room doesn't have conditional insertion or partial insertion.
You have to come up with your insertion logic. The best one I guess is call both database and server for data and just update your server response' local value with your database response' local value.
If you are comfortable with Rx, then you can do something like this
localDb.getFoo("id")
.zipWith(
remoteServer.getFoo("id"),
BiFunction<Foo, Foo, Foo> { localFoo, remoteFoo ->
remoteFoo.local = localFoo.local
remoteFoo
}
)
Another possible way is to write custom #Query that you insert all the values except local, but it's not feasible if you have lots of fields.

Categories

Resources