So, I'm using Room database to store courses and I'm stuck on the method that returns the course with the name(course) that I want because it's always returning null. I have diminished my database to have 2 courses with the course variable as:
As you can see in the picture above, when I try to get the CourseEnt in the Repository with course = fun, which I can see below that it exists, it returns a LiveData with a null value instead of the CourseEnt that I wanted.
Any idea on what I'm doing wrong or on what should I look into with debugger?
Here's the code:
Entity:
#Entity(tableName = "courses_table")
data class CoursesEnt (#PrimaryKey val course: String,
val location: String,
val description: String,
val difficulty: Double,
val distance: Double,
val photos: ListInt,
val category: String,
val activities: ListString)//ListString is a type converter that converts a String into a List<String> and vice-versa
DAO:
#Dao
interface CoursesDao {
#Query("SELECT * from courses_table ORDER BY course ASC")
fun getAllCourses(): LiveData<List<CoursesEnt>>
#Query("SELECT * FROM courses_table WHERE course LIKE :str")
fun getCourse(str: String):LiveData<CoursesEnt>
...
}
Repository:
class CoursesRepository(private val coursesDao: CoursesDao){
val allCourses: LiveData<List<CoursesEnt>> = coursesDao.getAllCourses()
var singleCourse: LiveData<CoursesEnt> = coursesDao.getCourse("")
#WorkerThread
fun getCourse(str: String) {
singleCourse = coursesDao.getCourse(str)
}
...
}
Read documentation, liveData will always return null for straight call, you have to observe LiveData, the result value from Room will be in block. This is some example of usage
mViewModel.getAllUsers().observe( this#YourActivity, Observer {
// it - is all users from DB
})
but if you will call
val users = mViewModel.getAllUsers()
the result will be null
Related
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.
I have a room database and I'm trying to read data from it, when reading 16k records, the operation takes 15seconds.
this is my code
#Entity(tableName = "reading_table")
data class DatabaseReading(
#PrimaryKey(autoGenerate = true)
val readingId: Int = 0,
val readingNum: Int,
val deviceId: String,
val readingTime: Long,
val topRh: Double,
val topTemp: Double,
val botRh: Double,
val botTemp: Double,
)
this is my DAO
#Query("SELECT * FROM reading_table WHERE deviceId = :id ORDER BY readingTime ASC")
suspend fun getDeviceReadings(id: String): List<DatabaseReading>
then from my viewmodel I'm running the query
fun getReadings() {
Timber.d("get readings start")
viewModelScope.launch {
withContext(Dispatchers.IO) {
val time = measureTimeMillis {
devicesRepository.database.deviceDatabaseDao.getDeviceReadings(
deviceID
)
}
Timber.d("get readings time $time")
}
}
}
getReadings is taking 15563ms for 16155 records.
how can I improve this?
it turns out that the issue was caused by the database inspector plugin on android studio.
as long as I don't use it everything runs normally.
I mean for a mobile device that's a pretty sizable amount of records, chances are you dont need and probably wont use all 16k of them.
You should probably use the Paging library for room to only load chunks of data instead of everything
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))}
)
}
I am using the similar solution to many similar topics, but still it doesn't want to work, and cant figure out why:
My Entity
#Entity(tableName = "single_item")
data class SingleItem (
#PrimaryKey(autoGenerate = true)
val id: Long? = 0L,
val name: String,
val icon: String,
val price: Int)
DAO
#Insert
suspend fun insertItem(item: SingleItem): Long
Repo
suspend fun insertItem(item: SingleItem): Long {
return myDao.insertItem(item)
}
Viewmodel
var insertedId = 0L
fun insertItem(item: SingleItem) = viewModelScope.launch {
insertedId = myRepository.insertItem(item)
}
Finally, call from Fragment
val newItem = SingleItem(null, "name","icon_name", 9999)
viewModel.insertItem(newItem)
Log.i("INSERT_ID", "Inserted ID is: ${viewModel.insertedId}")
And after i check the log, insertedId variable always returns 0. It doesn't change. What may be wrong?
Your insert method in the viewmodel is launching a new coroutine, it returns before it has executed the insert, so you get the initial value of 0. If you wait for the job to complete you will get the correct id.
Change it like this to see correct value:
var insertedId = 0L
fun insertItem(item: SingleItem) = viewModelScope.launch {
insertedId = myRepository.insertItem(item)
Log.i("INSERT_ID", "Inserted ID is: $insertedId")
}
If you want to get this value in your fragment you have to wait for the job to complete. The best way is to make your viewmodel insert method a suspend function instead of a fire and forget.
fun insertItem(item: SingleItem) =
myRepository.insertItem(item)
and in the fragment
lifecycleScope.launch {
val id = viewModel.insertItem(item)
Log.i("INSERT_ID", "Inserted ID is: $id")
}
but note that this is the wrong architecture, all business logic should be in the viewmodel, not the fragment.