How to create Entity and data classes by ROOM in android?
I have JSON structure:
data class ListResponse(val item: ListItem)
data class ListItem(
#SerializedName("id")
val id: List<CheckUnCheckItem>
)
data class CheckUnCheckItem(
#SerializedName("check")
val check: CheckItem,
#SerializedName("unCheck")
val UnCheck: UnCheckItem
)
data class CheckItem(
#SerializedName("url")
val url: String,
#SerializedName("text")
val text: String,
#SerializedName("color")
val color: String
)
data class UnCheckItem(
#SerializedName("url")
val urlUnCheck: String,
#SerializedName("text")
val textUnCheck: String,
#SerializedName("color")
val colorUnCheck: String
)
But How can I create such ROOM Entity?
Do I need to use #TypeConverter?
#Entity(tableName = TABLE_NAME)
data class ListEntity(
#PrimaryKey #SerializedName("id")
val id: CheckUnCheckItem,
#SerializedName("check")
val check: CheckItem,
#SerializedName("unCheck")
val unCheck: UnCheckItem,
#SerializedName("url")
val url: String,
#SerializedName("text")
val text: String,
#SerializedName("size")
val size: String
){
companion object{
const val TABLE_NAME = "db_table"
}
class RoomTypeConverters{
#TypeConverter
fun convertCheckItemListToJSONString(checkList: CheckItem): String = Gson().toJson(checkList)
#TypeConverter
fun convertJSONStringToCheckItemList(jsonString: String): CheckItem = Gson().fromJson(jsonString,CheckItem::class.java)
}
}
is my data and entity classes are correct?
Do I need class witch extends RoomDatabase?
Or better I need to separate db and create for check and uncheck another db?
Or better I need to separate db and create for check and uncheck another db?
As database implies it is able to store data not just one but many. As such a single database is all that would be required. SQLite is a relational database and is designed to store related data. Related data is typically stored in multiple tables. So again a single database will very likely be sufficient.
Do I need to use #TypeConverter?
You never actually need Type Converters. However, for any Object, other than those directly handled (e.g. String, Int, Long, Double, Float, ByteArray) then you either need to break these down into such handled objects or have a types converter that will convert the object to and from such an object.
For example, based upon your #Entity annotated ListEntity class then:-
field id would need a TypeConverter as the type CheckUnCheckItem is not an object type that can be directly handled by Room. So you would need two TypeConverters that could convert from a CheckUncheckItem to and from a type that can be handled by Room.
fields check and uncheck would need two TypeConverters (and it looks as though the Type Converters you have coded will handle the conversion).
fields url,text and size, as they are all String types do not need Type Converters as Room handles strings.
Room has to know about the Type Converters. So you need an #TypeConverters annotation. It's placement defines the scope. Using the annotation to preced the #Database annotation has the most far reaching scope.
Do I need class witch extends RoomDatabase?
Yes. However it has to be an abstract class and should have an abstract function to retrieve an instance of each #Dao annotated interface (or abstract class, in which case the functions have to be abstract, there is no need for an abstract class with Kotlin as functions in an interface can have bodies)).
This class should be annotated with the #Database annotation, the entities parameter of the annotation should include the list of classes for each each table (#Entity annotated class). e.g.
#TypeConverters(value = [ListEntity.RoomTypeConverters::class])
#Database(entities = [ListEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase(){
}
However, using the above along with your classes results in a build error as per:-
ListEntity.java:11: error: Cannot figure out how to save this field into database. You can consider adding a type converter for it. private final a.a.so74708202kotlinroomentitydesign.CheckUnCheckItem id = null;
as explained CheckUnCheckItem cannot be handled by Room.
Amending the RoomTypeConverters class to be:-
class RoomTypeConverters{
#TypeConverter
fun convertItemListToJSONString(invoiceList: Item): String = Gson().toJson(invoiceList)
#TypeConverter
fun convertJSONStringToItemList(jsonString: String): Item = Gson().fromJson(jsonString,Item::class.java)
#TypeConverter
fun convertCheckUnCheckItemToJSONString(cuc: CheckUnCheckItem): String = Gson().toJson(cuc)
#TypeConverter
fun convertJSONStringToCheckUnCheckItem(jsonString: String): CheckUnCheckItem = Gson().fromJson(jsonString,CheckUnCheckItem::class.java)
}
Resolves the build issue and in theory you have a potentially usable database.
However, you obviously need code to access the database. As such you would very likely want to have. as previously mentioned, an #Dao annotated interface e.g
#Dao
interface TheDAOs {
#Insert
fun insert(listEntity: ListEntity): Long
#Query("SELECT * FROM ${TABLE_NAME}")
fun getAll(): List<ListEntity>
}
This will suffice to allow rows to be inserted into the database and for all the rows to be extracted from the database into a List<ListEntity).
From the built database you need to get an instance of the TheDAOs and thus the #Database annotated class could then be
:-
#TypeConverters(value = [ListEntity.RoomTypeConverters::class])
#Database(entities = [ListEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase(){
abstract fun getTheDAOsInstance(): TheDAOs
}
To demonstrate actual use of the above then consider the following code in an activity:-
class MainActivity : AppCompatActivity() {
lateinit var roomDBInstance: TheDatabase
lateinit var theDAOs: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
roomDBInstance = Room.databaseBuilder(this,TheDatabase::class.java,"The_database_name.db")
.allowMainThreadQueries() /* NOTE ADDED FOR CONVENIENCE AND BREVITY */
.build()
/* Note the database itself does not yet exist, it's creation is delayed until an attempt is made to access it. So:- */
theDAOs = roomDBInstance.getTheDAOsInstance() /* Still the database is not created/accessed */
showData(theDAOs.getAll()) /* No data has been added BUT the database will now exist */
theDAOs.insert(
ListEntity(
id = CheckUnCheckItem(
check = Item (
url ="URL001",
text = "TEXT001",
color = "RED"
),
unCheck = Item(
url ="URL002",
text = "TEXT002",
color = "BLUE"
)
),
check = Item(url = "URL003", text ="TEXT003", color ="WHITE"),
unCheck = Item(url = "URL004", text = "TEXT004", color = "BLACK"),
url = "URL005", text = "TEXT005", size = "BIG"
)
)
showData(theDAOs.getAll())
}
fun showData(listEntities: List<ListEntity>) {
for (li in listEntities) {
Log.d(
"DBINFO",
"id is $li.id.check.url${li.id.check.text}.... " +
"\n\tcheck is ${li.check.url} .... " +
"\n\tuncheck is ${li.unCheck.url} ...." +
"\n\turl is ${li.url} text is ${li.text} size is ${li.size}"
)
}
}
}
The output to the log being:-
D/DBINFO: id is ListEntity(id=CheckUnCheckItem(check=Item(url=URL001, text=TEXT001, color=RED), unCheck=Item(url=URL002, text=TEXT002, color=BLUE)), check=Item(url=URL003, text=TEXT003, color=WHITE), unCheck=Item(url=URL004, text=TEXT004, color=BLACK), url=URL005, text=TEXT005, size=BIG).id.check.urlTEXT001....
check is URL003 ....
uncheck is URL004 ....
url is URL005 text is TEXT005 size is BIG
The Database via App Inspection being"-
So finally
is my data and entity classes are correct?
From a database aspect yes, they work after a few amendments. However, I suspect that your classes are probably not what you intended.
An Alternative Approach
If this were to be approached from a database perspective and normalised and without bloat and without the need for type converters then consider the following:-
The embedded Item's (uncheck and check) are basically repetition, so could probably be a table (related to the db_table). Hence 2 tables. One for the ListEntity (Alternative) and another for the Items (AlternativeItem) so the 2 #Entity annotated classes could be:-
/* Alternative Approach */
#Entity(
/* Foreign Keys NOT REQUIRED, they enforce Referential Integrity */
foreignKeys = [
ForeignKey(
entity = AlternativeItem::class,
parentColumns = ["alternativeItemId"],
childColumns = ["unCheckIdMap"]
/* OPTIONAL within a Foreign Key, they help automatically maintain Referential Integrity*/,
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = AlternativeItem::class,
parentColumns = ["alternativeItemId"],
childColumns = ["checkIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Alternative(
#PrimaryKey
val id: Long?=null,
#ColumnInfo(index = true)
val unCheckIdMap: Long, /* map to the id of the related Item (AlternativeItem) for the uncheck */
#ColumnInfo(index = true)
val checkIdMap: Long, /* map to the id of the related Item (AlternativeItem) for the uncheck */
val url: String,
val text: String,
val size: String
)
#Entity
data class AlternativeItem(
#PrimaryKey
val alternativeItemId: Long?=null,
val alternativeItemUrl: String,
val alternativeItemText: String,
val alternativeItemColor: String
)
As you would typically want the Alternative along with it's related AlternativeItems then a POJO that caters for the togetherness :-
data class AlternativeWithUncheckAndCheck(
#Embedded
val alternative: Alternative,
#Relation(entity = AlternativeItem::class, parentColumn = "unCheckIdMap", entityColumn = "alternativeItemId")
val unCheck: AlternativeItem,
#Relation(entity = AlternativeItem::class, parentColumn = "checkIdMap", entityColumn = "alternativeItemId")
val check: AlternativeItem
)
There would be a need for some extra functions in the #Dao annotated interface, so :-
#Insert
fun insert(alternative: Alternative): Long
#Insert
fun insert(alternativeItem: AlternativeItem): Long
#Transaction
#Query("")
fun insertAlternativeAndUncheckAndCheck(alternative: Alternative, uncheck: AlternativeItem, check: AlternativeItem): Long {
var uncheckId = insert(uncheck)
var checkId = insert(check)
return insert(Alternative(null,url = alternative.url, text = alternative.text, size = alternative.size, unCheckIdMap = uncheckId, checkIdMap = checkId ))
}
#Transaction
#Query("SELECT * FROM alternative")
fun getAllAlternativesWithRelatedUnCheckAndCheck(): List<AlternativeWithUncheckAndCheck>
note that the insertAlternativeAndUncheckAndCheck does what it says (note that it is overly simple and could need some enhancements to expand upon the principle)
To demonstrate this, all that is then required is to add the new entities to the entities parameter and to then add some code to the activity.
The amended #Database annotation:-
#Database(entities = [ListEntity::class, /* for the alternative approach */ Alternative::class, AlternativeItem::class], exportSchema = false, version = 1)
The activity code (that caters for both approaches in a similar/equivalanet way of storing and retrieving the data) :-
class MainActivity : AppCompatActivity() {
lateinit var roomDBInstance: TheDatabase
lateinit var theDAOs: TheDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
roomDBInstance = Room.databaseBuilder(this,TheDatabase::class.java,"The_database_name.db")
.allowMainThreadQueries() /* NOTE ADDED FOR CONVENIENCE AND BREVITY */
.build()
/* Note the database itself does not yet exist, it's creation is delayed until an attempt is made to access it. So:- */
theDAOs = roomDBInstance.getTheDAOsInstance() /* Still the database is not created/accessed */
showData(theDAOs.getAll()) /* No data has been added BUT the database will now exist */
theDAOs.insert(
ListEntity(
id = CheckUnCheckItem(
check = Item (
url ="URL001",
text = "TEXT001",
color = "RED"
),
unCheck = Item(
url ="URL002",
text = "TEXT002",
color = "BLUE"
)
),
check = Item(url = "URL003", text ="TEXT003", color ="WHITE"),
unCheck = Item(url = "URL004", text = "TEXT004", color = "BLACK"),
url = "URL005", text = "TEXT005", size = "BIG"
)
)
showData(theDAOs.getAll())
/* Alternative equivalent */
theDAOs.insertAlternativeAndUncheckAndCheck(
Alternative(url = "URL005", size = "BIG", text = "TEXT005", checkIdMap = -1, unCheckIdMap = -1),
check = AlternativeItem(alternativeItemUrl = "URL001", alternativeItemText = "TEXT001", alternativeItemColor = "RED"),
uncheck = AlternativeItem(alternativeItemUrl = "URL002", alternativeItemText = "TEXT002", alternativeItemColor = "BLUE" )
)
showAlternativeData(theDAOs.getAllAlternativesWithRelatedUnCheckAndCheck())
}
fun showData(listEntities: List<ListEntity>) {
for (li in listEntities) {
Log.d(
"DBINFO",
"id is $li.id.check.url${li.id.check.text}.... " +
"\n\tcheck is ${li.check.url} .... " +
"\n\tuncheck is ${li.unCheck.url} ...." +
"\n\turl is ${li.url} text is ${li.text} size is ${li.size}"
)
}
}
fun showAlternativeData(listAlternatives: List<AlternativeWithUncheckAndCheck>) {
for (la in listAlternatives) {
Log.d("DBALTINFO",
"id is ${la.alternative.id} URL is ${la.alternative.url} TEXT is ${la.alternative.text} SIZE is ${la.alternative.size} " +
"\n\t UNCHECK id is ${la.unCheck.alternativeItemId} url is ${la.unCheck.alternativeItemUrl} text is ${la.unCheck.alternativeItemText} color is ${la.unCheck.alternativeItemColor}" +
"\n\t CHECK id is ${la.check.alternativeItemId} url is ${la.check.alternativeItemUrl} text is ${la.check.alternativeItemText} color is ${la.check.alternativeItemColor}")
}
}
}
Note that the Alternative code is probably more along the lines of what you probably want according to the interpretation of the shown JSON.
When run then the result is now:-
D/DBINFO: id is ListEntity(id=CheckUnCheckItem(check=Item(url=URL001, text=TEXT001, color=RED), unCheck=Item(url=URL002, text=TEXT002, color=BLUE)), check=Item(url=URL003, text=TEXT003, color=WHITE), unCheck=Item(url=URL004, text=TEXT004, color=BLACK), url=URL005, text=TEXT005, size=BIG).id.check.urlTEXT001....
check is URL003 ....
uncheck is URL004 ....
url is URL005 text is TEXT005 size is BIG
D/DBALTINFO: id is 1 URL is URL005 TEXT is TEXT005 SIZE is BIG
UNCHECK id is 1 url is URL002 text is TEXT002 color is BLUE
CHECK id is 2 url is URL001 text is TEXT001 color is RED
it is suspected that BLACK/WHITE or RED/BLUE is superfluous in your interpretationof the JSON to data classes (and hence excluded in the alternative).
The database, via App Inspection (in regards to the alternative approach) is:-
and :-
i.e. only the actual data is store the BLOAT (field/type descriptions, separators, enclosing data) is not stored thus
the database will hold more data in less space.
the handling of the data will thus be more efficient (e.g. a buffer can hold more actual data instead of BLOAT).
querying the data such as for example searching for all BLUE's is directly a search for that, whilst with converted data you may have issues distinguishing between BLOAT and actual data
However, the negative, is that more code and thought is required.
Note this answer is intended to deal with the basic principles and is most certainly not fully comprehensive.
I have a collection containing Group objects with the following schema:
class Group : BaseEntity(), RealmObject {
class UserInfo : EmbeddedRealmObject {
var userId: Id? = null
var username: String? = null
var userBalance: Double = 0.0
}
#PrimaryKey
override var _id: Id = createId()
var groupName: String? = null
var users: RealmList<Id> = realmListOf()
var userInfo: RealmList<UserInfo> = realmListOf()
}
Group.users and Group.usersInfo is mimicking a dictionary with userId as key and UserInfo containing nickname, balance, etc. as value. Apparently the Kotlin SDK doesn't support RealmDictionary nor querying on embedded objects according to Kotlin RQL docs, that's why the schema is a little messy. My Sync subscription for the Groups collection is:
val config =
SyncConfiguration.Builder(realmUser, setOf(Group::class, Group.UserInfo::class))
.initialSubscriptions(rerunOnOpen = true) { realm ->
add(realm.query<Group>("$0 IN users", userId))
}
.name("groupRealm")
.build()
val realm: Realm by lazy { Realm.open(config) }
where the subscription query
add(realm.query<Group>("$0 IN users", userId))
is attempting to sync all Group objects that a userId belongs to, basically, through the Group.users and Group.userInfo fields I described earlier.
All the read and write operations into the local realm are fine, but when I go into the collections in Atlas, I don't see the updates in the Group collection. Also, when I add .waitForInitialRemoteData() to the config, my code times out. I also get an OtherSessionError: operation canceled (ProtocolErrorCode=201) in the App Services logs, and an empty query when it should show the subscription query mentioned above. Any help on this issue is greatly appreciated!
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.
I am trying to insert values inside my room database but the App Inspection is showing nothing. I have a complex app structure with lot of tables and one database. I am able to perform CRUD operations on all of the tables except for one. I am not able to identify what I am doing wrong.
Code -->
Entity
#Entity(tableName = "add_time")
data class TimeEntity(
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
#ColumnInfo(name = "start_time")
var startTime: String? = "",
#ColumnInfo(name = "end_time")
var endTime: String? = "",
#ColumnInfo(name = "running_time")
var runningTime: String? = "",
)
DAO
#Dao
interface FloorTimeDao {
//Insert time
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTimeForScheduling(timeEntity: TimeEntity) {
}
}
REPOSITORY
class AddTimeRepository(private val timeDao: FloorTimeDao) {
//insert
#WorkerThread
suspend fun insertTime(time: TimeEntity) = timeDao.insertTimeForScheduling(time)
}
VIEWMODEL
class AddTimeViewModel(private val repository: AddTimeRepository) : ViewModel() {
//insert
fun insertTime(timeEntity: TimeEntity) = viewModelScope.launch {
repository.insertTime(timeEntity)
}
}
VIEWMODEL FACTORY
#Suppress("UNCHECKED_CAST")
class AddTimeViewModelFactory(private val repository: AddTimeRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
if (modelClass.isAssignableFrom(AddTimeViewModel::class.java)) {
return AddTimeViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel Class")
}
}
CODE INSIDE FRAGMENT FOR ADDING VALUE
//Inserting new time in the table
binding.btnAddTime.setOnClickListener {
try {
timeRunning = timeEnd - timeStart
timeRunning = abs(timeRunning)
Timber.d("Final Time start : $timeStart")
Timber.d("Final Time end : $timeEnd")
Timber.d("Final Time running : $timeRunning")
val timeEntity = TimeEntity(
startTime = timeStart.toString(),
endTime = timeEnd.toString(),
runningTime = timeRunning.toString()
)
Timber.d("Time Entity: $timeEntity")
addTimeViewModel.insertTime(timeEntity)
} catch (e: Exception) {
Timber.d("Exception : $e")
}
}
Everything seems to be added correctly and I still cannot seem to find out what I am doing wrong here. Any help would be appreciated. If you need more code I can provide just let me know.
NOTE: With this code basically I am trying to store the time in room database which is taken from the user using Time Picker Dialog.
Edit 1: Found something which I don't know if it is related to the issue or anything. But for the tables in which I am able to insert and read the data the function says something like this :
and for the table (TimeEntity) the function says this :
The difference is that (for the one in which it is working) the functions says Choose Implementation and have a green symbol on left side. But for the table for which it is not working the function says Choose overridden method.
Update: I was able to fix the issue by creating a new Dao Interface for the table. I am still not very sure what might have been the issue as both DAO files looks same. But I have some doubt that it might be due to the room automatic implementations when we create new DAO's or table.
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
Try to change id Int? - To non nullable type Int or Long.
Why do you need to have predefined (initialized) values in entity constructor?
I was able to fix the issue by creating a new Dao file with same functions. The issue may have been due to the automatic implementation of DAO's provided by Room Library.
Modifying simple values and data classes using EditText is fairly straight forward, and generally looks like this:
data class Person(var firstName: String, var lastName: Int)
// ...
val (person, setPerson) = remember { mutableStateOf(Person()) }
// common `onChange` function handles both class properties, ensuring maximum code re-use
fun <T> onChange(field: KMutableProperty1<Person, T>, value: T) {
val nextPerson = person.copy()
field.set(nextPerson, value)
setPerson(nextPerson)
}
// text field for first name
TextField(
value = person.firstName,
onChange = { it -> onChange(Person::firstName, it) })
// text field for last name name
TextField(
value = person.lastName,
onChange = { it -> onChange(Person::lastName, it) })
As you can see, the code in this example is highly reusable: thanks to Kotlin's reflection features, we can use a single onChange function to modify every property in this class.
However, a problem arises when the Person class is not instantiated from scratch, but rather pulled from disk via Room. For example, a PersonDao might contain a `findOne() function like so:
#Query("SELECT * FROM peopleTable WHERE id=:personId LIMIT 1")
fun findOne(personId: String): LiveData<Person>
However, you cannot really use this LiveData in a remember {} for many reasons:
While LiveData has a function called observeAsState(), it returns State<T> and not MutableState<T>, meaning that you cannot modify it with the TextFields. As such this does not work:
remember { personFromDb.observeAsState()}
You cannot .copy() the Person that you get from your database because your component will render before the Room query is returned, meaning that you cannot do this, because the Person class instance will be remembered as null:
remember { mutableStateOf(findPersonQueryResult.value) }
Given that, what is the proper way to handle this? Should the component that contains the TextFields be wrapped in another component that handles the Room query, and only displays the form when the query is returned? What would that look like with this case of LiveData<Person>?
I would do it with a copy and an immutable data class
typealias PersonID = Long?
#Entity
data class Person(val firstName: String, val lastName: String) {
#PrimaryKey(autoGenerate = true)
val personID: PersonID = null
}
//VM or sth
object VM {
val liveData: LiveData<Person> = MutableLiveData() // your db call
val personDao: PersonDao? = null // Pretending it exists
}
#Dao
abstract class PersonDao {
abstract fun upsert(person: Person)
}
#Composable
fun test() {
val personState = VM.liveData.observeAsState(Person("", ""))
TextField(
value = personState.value.firstName,
onValueChange = { fName -> VM.personDao?.upsert(personState.value.copy(firstName = fName))}
)
}