I'm trying to observe changes in DB made from another fragment.
I have a fragment A (contains a recyclerView with items) with a ViewModel that has inside a LiveData property from the Room database.
Like this:
val allItems: LiveData<List <Item>> = repo.getAll()
If I open a new fragment (let's call it B) from fragment A and do repo.insert(item) there, I expect the LiveData's observer to fire on allItems when returning back to fragment A. But that doesn't happen.
How a can fix it nicely?
Of course I can get data in onViewCreated() every time I open the fragment A, but I believe there must be a better way.
class CharactersViewModel : BaseViewModel() {
private val db get() = AppDatabase.getDB(MyApplication.application)
private val repo = CharacterRepository(viewModelScope, db.characterDao())
val characters: LiveData<List<Character>> = repo.getAll()
}
class CharacterRepository(
private val scope: CoroutineScope,
private val dao: CharacterDao
) {
fun getAll() = dao.getAll()
fun getById(itemId: Int) = dao.getById(itemId)
fun insert(item: Character) = scope.launch {
dao.insert(item)
}
fun update(item: Character) = scope.launch {
dao.update(item)
}
fun delete(item: Character) = scope.launch {
dao.delete(item)
}
}
#Dao
interface CharacterDao {
fun getAll(): LiveData<List<Character>>
fun getById(itemId: Int) : LiveData<Character>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(item: Character)
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: Character)
#Delete
suspend fun delete(item: Character)
}
Note: It looks like this is because the ViewModel of Fragment A is currently inactive. And the issue is not due to viewLifecycleOwner, since the observeForever is also not notified.
Upd: Just found the issue, answer attached.
There was when getting the database instance.
fun getDB(appContext: Context) = Room.databaseBuilder(
appContext,
AppDatabase::class.java, DB_NAME
).build()
I solved the problem by making it a singleton, so now it returns the same instance of the DB.
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
#Synchronized
fun getDB(context: Context): AppDatabase {
// if the INSTANCE is not null, then return it, otherwise create the database
return INSTANCE ?: run {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
DB_NAME
).build()
INSTANCE = instance
instance
}
}
}
Related
ok so im quite new to using room, ive looked in a lot of places but find it hard to find my answer, alot of places ive looked around seem to have outdated information.
I have setup a room database, i have all the information for it set up as well as a viewmodel to send the infomration from. However i can not for the life of me figure out how to process the information in to android compose.
here is what i have done,any feedback would be good.
#Entity
data class Notes(
#PrimaryKey val uid: Int?,
#ColumnInfo(name = "header") val header: String?,
#ColumnInfo(name = "note") val note: String?
)
#Dao
interface NotesDao {
#Query("SELECT * FROM notes")
fun getAll(): LiveData<List<Notes>>
#Insert (onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(notes: Notes)
#Delete
suspend fun delete(notes: Notes)
}
#Database(entities = [Notes::class], version = 1)
abstract class NotesAppDatabase : RoomDatabase() {
abstract fun notesDao(): NotesDao
companion object {
#Volatile
private var INSTANCE: NotesAppDatabase? = null
fun getDatabase(context: Context): NotesAppDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
NotesAppDatabase::class.java,
"items_database"
)
// Wipes and rebuilds instead of migrating if no Migration object.
// Migration is not part of this codelab.
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
class NotesRepository (private val notesDao: NotesDao) {
val readAllData: LiveData<List<Notes>> = notesDao.getAll()
suspend fun addNote(note: Notes){
notesDao.insert(note)
}
}
class NotesRepoViewModel (application: Application): AndroidViewModel(application) {
private val repo: NotesRepository
private val allNotes: LiveData<List<Notes>>
init {
val dao = NotesAppDatabase.getDatabase(application).notesDao()
repo = NotesRepository(dao)
allNotes = repo.readAllData
}
fun notes(notes: Notes){
viewModelScope.launch {
repo.addNote(notes)
}
}
}
Whenever i call one of the getNotes() method in my fragment the obsever is not updating the data. It is however updating is if i go from mainfragment to other fragment and back to mainfragment again...
I don't know what is wrong. Here i am new to livedata please help
class MainViewModel #ViewModelInject constructor(
val repository: NoteRepository
): ViewModel() {
var notes: LiveData<List<Note>>
init {
notes= repository.getAllNotes()
}
fun getNotes(){
notes = repository.getAllNotes()
}
fun getFavoriteNotes(){
notes = repository.getAllNotesFavoriteOrder()
}
fun searchNotes(searchString:String){
notes = repository.getAllNotesQueryTitle(searchString)
mainViewModel.notes.observe(viewLifecycleOwner){
adapter.setData(it)
}
class NoteRepository #Inject constructor (val noteDao: NoteDao) {
suspend fun insertNote(note: Note){
noteDao.insertNote(note)
}
fun getAllNotes():LiveData<List<Note>>{
return noteDao.getAllNotes()
}
fun getAllNotesFavoriteOrder():LiveData<List<Note>>{
return noteDao.getAllNotesFavoriteOrder()
}
fun getAllNotesQueryTitle(searchString : String) : LiveData<List<Note>> {
return noteDao.getAllNotesQueryTitle(searchString)
}
suspend fun deleteAllNotes(){
noteDao.deleteAllNotes()
}
suspend fun deleteNote(note: Note){
noteDao.deleteNote(note)
}
suspend fun updateNote(note: Note){
noteDao.updateNote(note)
}
}
LiveData should be stored in a read-only val property. You keep assigning your read-write var property to point at other LiveData instances, so you are not updating the LiveData instance that you are initially observing.
You do need to make the property of type MutableLiveData so you can actually update it:
val notes = MutableLiveData<List<Note>>()
init {
getNotes()
}
The functions in NoteRepository should be returning Lists, not LiveData<List>s.
In your ViewModel, when you retrieve a list from the repo, assign it to the LiveData's value property, for example:
fun getNotes(){
notes.value = repository.getAllNotes()
}
A safer pattern is to make your MutableLiveData property private, and exposing a public LiveData version of it, so external classes cannot modify it:
private val _notes = MutableLiveData<List<Note>>()
val notes: LiveData<List<Note>> get() = _notes
//...
fun getNotes(){
_notes.value = repository.getAllNotes()
}
I suggest changing the function names getNotes() and getFavoriteNotes() to something like retrieveNotes(). Function names that start with get look like Java beans or the equivalent of Kotlin properties, so those names are misleading because the functions don't return anything.
After inserting data into RoomDB when I fetch it using mindValleyDao.getCategories().value It returns null
DatabaseClass
#Database(entities = arrayOf(CategoryBO::class), version = 1, exportSchema = false)
abstract class MindValleyDatabase : RoomDatabase(){
abstract fun mindValleyDao(): MindValleyDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
private var INSTANCE: MindValleyDatabase? = null
fun getDatabase(context: Context): MindValleyDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
MindValleyDatabase::class.java,
"mindvalley_database"
).allowMainThreadQueries()
.fallbackToDestructiveMigration().build()
INSTANCE = instance
return instance
}
}
}
}
CategoryBO.kt
#Entity(tableName = "CategoryEntity")
data class CategoryBO( #PrimaryKey(autoGenerate = true) val id:Int, val name:String)
Doa
#Dao
interface MindValleyDao {
#Query("SELECT * from CategoryEntity ORDER BY id ASC")
fun getCategories(): LiveData<List<CategoryBO>>
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(categoryBO: CategoryBO)
//suspend fun insert(categoryBO: CategoryBO)
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(categoryBOList: List<CategoryBO>)
}
I am testing it by inserting Category and fetching list of categories like
class MindValleyViewModelNew #Inject constructor() : BaseViewModel() {
var categoryList: MutableLiveData<List<CategoryBO>> = MutableLiveData()
private lateinit var mindValleyDao:MindValleyDao
fun loadDatabase(mContext:Context){
mindValleyDao = MindValleyDatabase.getDatabase(mContext).mindValleyDao()
GlobalScope.launch(Dispatchers.IO) {
mindValleyDao.insert(CategoryBO(0,"first item"))
val cats = mindValleyDao.getCategories().value
categoryList.postValue(cats)
}
}
}
mindValleyDao.getCategories() has return type is LiveData, that's why it query value async, you shouldn't call .value
LiveData type in Room should only use for observe,
If you want to get value, modify your code to fun getCategories(): List<CategoryBO> instead
I could not find the issue of not inserting the data into Room Database when app is launched first time, but when App is restarted data is inserted. Why not insertion is happening at first launch?
My code is:
#Dao
interface FooDAO {
#Insert
suspend fun save(foo: Foo)
}
and Database class is as simple:
#Database(entities = [Foo::class], version = 1, exportSchema = false)
abstract class FooDatabase: RoomDatabase() {
// get dao
abstract fun getFooDao(): FooDAO
companion object{
#Volatile private var INSTANCE: FooDatabase? = null
fun getDatabase(context: Context): FooDatabase {
if (INSTANCE == null) {
synchronized(this) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
FooDatabase::class.java,
"my_db.db"
).fallbackToDestructiveMigration()
.addCallback(CALLBACK)
.build()
}
}
return INSTANCE!!
}
private val CALLBACK = object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val fooDAO = INSTANCE!!.getFooDao()
CoroutineScope(Dispatchers.IO).launch {
fooDAO.save(Foo(1, "A"))
fooDAO.save(Foo(2, "B"))
fooDAO.save(Foo(3, "C"))
fooDAO.save(Foo(4, "D"))
fooDAO.save(Foo(5, "E"))
}
}
}
}
}
with the code above records are not inserted at first launch of app but when it is restarted recordes are inserted, how can we fix this issue and make it insert records at first launch as well?
I am using Coroutines to perform db operations on my Room database
I have create a helper for Coroutines as follows
object Coroutines {
fun main(work: suspend(() -> Unit)) = CoroutineScope(Dispatchers.Main).launch {
work()
}
fun io(work: suspend(() -> Unit)) = CoroutineScope(Dispatchers.IO).launch {
work()
}
}
Following is the code where i call my insert operation on the main thread
class LocalListViewModel(private val localVideoRepository: LocalVideoRepository) : ViewModel() {
val localVideos = MutableLiveData<ArrayList<LocalVideo?>?>()
fun insertAllLocalVideo() {
Coroutines.main {
localVideoRepository.insertLocalVideo(localVideos.value)
}
}
}
class LocalVideoRepository(private val db: AppDatabase) {
suspend fun insertLocalVideo(localVideos: ArrayList<LocalVideo?>?) =
db.getLocalVideoDao().insertLocalVideos(localVideos)
}
#Dao
interface LocalVideoDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLocalVideos(localVideoList: ArrayList<LocalVideo?>?)
}
#Database(entities = [LocalVideo::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun getLocalVideoDao(): LocalVideoDao
companion object {
#Volatile
private var instance: AppDatabase? = null
private val LOCK = Any()
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: buildDatabase(context).also { instance = it }
}
private fun buildDatabase(context: Context) = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
context.getString(R.string.psplayer_db)
).build()
}
}
What i don't understand is even after calling on main thread of Coroutines, the data gets inserted successfully,instead of crashing?
For an in-depth answer look at the CoroutinesRoom.execute helper:
suspend fun <R> execute(
db: RoomDatabase,
inTransaction: Boolean,
callable: Callable<R>
): R {
if (db.isOpen && db.inTransaction()) {
return callable.call()
}
// Use the transaction dispatcher if we are on a transaction coroutine, otherwise
// use the database dispatchers.
val context = coroutineContext[TransactionElement]?.transactionDispatcher
?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
return withContext(context) {
callable.call()
}
}
Unless it already is in a transaction it always moves the execution of the actual query onto a IO-optimized dispatcher. So regardless of the calling context a suspend room definition will never execute on the application thread.