0

I'm building my first Room project and need a fresh pair of eyes to see what I'm doing wrong.

Android studio keeps telling me the call to insertBopa or deleteBopa in the BopaRoomDao is an unresolved reference. My code seeme to match other examples I've looked at and tutorials but I just can't work out what I'm doing wrong.

This is my repository.kt

package com.example.mytestapp

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow

class BopaRepository(private val bopaRoomDao: BopaRoomDao) {

    val allBopaRoomEntry: LiveData<List<BopaRoomEntry>> = bopaRoomDao.allBopas()
    val searchResults = MutableLiveData<List<BopaRoomEntry>>()
    private val coroutineScope = CoroutineScope(Dispatchers.Main)

    fun insertBopaEntry(newbopa: BopaRoomEntry) {
        coroutineScope.launch(Dispatchers.IO) {
            BopaRoomDao.insertBopa(newbopa)
        }
    }

    fun deleteBopa(name: String) {
        coroutineScope.launch(Dispatchers.IO) {
            BopaRoomDao.deleteBopa(name)
        }
    }

    fun findBopa(name: String) {
        coroutineScope.launch(Dispatchers.Main) {
            searchResults.value = asyncFind(name).await()
        }
    }

    fun allBopas(): LiveData<List<BopaRoomEntry>> {
        return bopaRoomDao.allBopas()
    }

    private fun asyncFind(name: String): Deferred<Flow<List<BopaRoomEntry>>> =
        coroutineScope.async(Dispatchers.IO) {
            return@async bopaRoomDao.findBopa(name)
        }
}

This is my Dao

package com.example.mytestapp

import androidx.lifecycle.LiveData
import androidx.room.*
//import java.util.concurrent.Flow
import kotlinx.coroutines.flow.Flow

@Dao
interface BopaRoomDao {

    //add new entry to db
    @Insert
    fun insertBopa(bopaRoomEntry: BopaRoomEntry)

    //change entry on db
    @Update
    fun updateBopa(bopaRoomEntry: BopaRoomEntry)

    @Delete
    fun deleteBopa(bopaRoomEntry: BopaRoomEntry)

    //open list of previous entries from db
    @Query("SELECT * FROM bopa_table")
    fun findBopa(name: String): Flow<List<BopaRoomEntry>>

    @Query("SELECT * FROM bopa_table")
    fun allBopas(): LiveData<List<BopaRoomEntry>>

}

This is the BopaRoomEntry class

package com.example.mytestapp

import androidx.annotation.NonNull
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.lang.reflect.Constructor

@Entity (tableName = "BOPA_TABLE")
class BopaRoomEntry {

    @PrimaryKey(autoGenerate = true)
    @NonNull
    @ColumnInfo(name = "bopaId")
    var id: Int = 0

    @ColumnInfo(name = "bopa_topic")
    var bopaTopic: String = ""

    @ColumnInfo(name = "bopa_content")
    var bopaContent: String = ""

    constructor(){}

    constructor(bopatopic: String, bopacontent: String) {
        //this.id = id
        this.bopaTopic = bopatopic
        this.bopaContent = bopacontent

    }
}

I'm adding the database class to see if it helps clarify one of the answers...

package com.example.mytestapp

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [(BopaRoomEntry::class)], version = 1)
abstract class AppDatabase : RoomDatabase() {

    abstract fun bopaRoomDao(): BopaRoomDao

    companion object {

        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase? {
            synchronized(this) {
                var instance = INSTANCE

                if (INSTANCE == null) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        AppDatabase::class.java,
                        "bopa-database.db"
                    ).fallbackToDestructiveMigration()
                        .build()

                    INSTANCE = instance
                }

                return instance
            }
        }
    }
}

Any help appreciated :-P

  • Quick comment: the `findBopa` method has this query: `@Query("SELECT * FROM bopa_table")` but you're missing the `WHERE` clause. (I know this is potentially unrelated to the issue at hand, but just pointing it out) – Martin Marconcini Aug 05 '22 at 12:05
  • You are calling `BopaRoomDao.insertBopa` instead of `bopaRoomDao.insertBopa` – Jorn Aug 05 '22 at 12:05
  • Thanks guys. I'm sure I'd have noticed the missing WHERE sooner or later lol! I tried calling the lower case Dao and it gives me the same error...? – Phil Tinsley Aug 05 '22 at 16:05

1 Answers1

0

After a closer look:

  1. Could it be you're missing the BopaRoomDao.insertBopa(newbopa) vs the lower-case version: bopaRoomDao.insertBopa(newbopa)?

  2. Do you have a abstract class XXXX : RoomDatabase() { where you define your abstract bopaDao() = BopaRoomDao and is annotated with

@Database(
    entities = [
        BopaRoomEntry::class,
    ],
    version = 1,
    exportSchema = false
)

If so you should be using the "daos" provided by this:

val db = ... //obtain your DB

db.bopaDao().allBopas()

Update

After cloning your project, I see a few issues:

MainViewModel: You obtain your DB here, in an attempt to construct the Repository. This is fine (though with Hilt/DependencyInjection you would not need to worry) but your Repository is -correctly- expecting a non-nullable version of your DB. So

        val bopaDb = AppDatabase.getInstance(application)
        val bopaDao = bopaDb.bopaRoomDao()
        repository = BopaRepository(bopaDao)

Should really be changed to ensure getInstance cannot return null.

(maybe make INSTANCE a lateinit since you must have a DB to function it appears).

If having a DB is optional, then the repository must either deal with it or the viewmodel must not attempt to use/create a repository. As you can see this can get weird really fast. I'd say having a DB cannot fail or you have other issues.

If you still leave it as optional, then the sake of this demo, change it to:

        val bopaDb = AppDatabase.getInstance(application)
        val bopaDao = bopaDb?.bopaRoomDao() //add the required `?`
        repository = BopaRepository(bopaDao!!) //not good to force unwrap !! but will work.

Alternatively you can make your BopaRepository nullable BopaRepository? and use

repository = bopaDao?.let { BopaRepository(it) } ?: null

but then you have to add ? every time you want to use it... and this in turn will make this more messy.

I'd say your DB method should not return null, if it is null for some random other problem (say the filesystem is full and the DB cannot be created) then you should handle this gracefully elsewhere as this is an exception outside of your control. OR... your repository should fetch the DB and work with a different storage internally, you, the caller, should not care.

Anyway.. after taking care of that...

Let's look at BopaRepository

You have it defined like

class BopaRepository(private val bopaRoomDao: BopaRoomDao) {

The important bit is bopaRoomDao.

(note: I would pass the DB here, not a specific DAO, since the repo may need access to other Daos (though you could argue then it should receive the other Repositories instead) so... your choice).

Red Line 1: val allBopaRoomEntry: LiveData<List<BopaRoomEntry>> = bopaRoomDao.allBopaEntries()

The problem is that allBopaEntries doesn't exist. In the BopaRoomDao interface, the method is called: allBopas()

So change that to

val allBopaRoomEntry: LiveData<List<BopaRoomEntry>> = bopaRoomDao.allBopas()

Red Line #2

In fun insertBopaEntry(newbopa: BopaRoomEntry) {

BopaRoomDao.insertBopaEntry(newbopa) should be:

bopaRoomDao.insertBopa(newbopa)

Red Line #3:

        coroutineScope.launch(Dispatchers.IO) {
           BopaRoomDao.deleteBopaEntry(name)
        }
    }

The DAO in the repo doesn't have a delete method (forgot?)

but should look like bopaRoomDao.delete(theBopaYouWantToDelete) So:

    @Delete
    fun deleteBopa(bopaRoomEntry: BopaRoomEntry)

This means you cannot pass a name to the delete method (you could) but then because as far as I remember Room doesn't support a @Delete(...), you need to change it to a "custom" query:

@Query("DELETE FROM bopa_table WHERE bopa_topic=:name")
fun deleteByTopic(topic: String);

In truth, you should probably FETCH the row you want to delete and pass that to the original method.

For more info take a look at this SO answer.

Red Line #4 fun findBopa(name: String) {

You need to collect the flow:

    fun findBopa(name: String){
        coroutineScope.launch(Dispatchers.Main) {
            val result = asyncFind(name).await()
            result.collect {
                searchResults.postValue(it)
            }
        }
    }

This will have another issue though. You're not using the name you pass to find:

So it should look like:

    //open list of previous entries from db
    @Query("SELECT * FROM bopa_table WHERE bopa_topic=:name")
    fun findBopa(name: String): Flow<List<BopaRoomEntry>>

(assuming name is the bopa_topic).

Red Line #5 fun allBopas(): LiveData<List<BopaRoomEntry>> {

Should do return bopaRoomDao.allBopas() (incorrect name)

This one is strange as allBopaRoomEntry is a public variable, you should either make that one private or remove it, since you have this method that returns the reference to the same thing.

Red Line #6 Last but not least,

fun asyncFind(name: String): Deferred<Flow<List<BopaRoomEntry>>>

returns a Flow (deferred but flow) so I think you'd want to do this:

= coroutineScope.async(Dispatchers.IO) {
    return@async bopaRoomDao.findBopa(name)
}

Given that findBopa returns a Flow<List<BopaRoomEntry>> already.

With these changes, the project almost built correctly, but there's another issue in MainActivity:

        //button actions
        binding.saveBopaEntry.setOnClickListener{
            //code for sending editText to db
            BopaRoomDao.updateBopa(bopaTopic = R.id.bopaTopic, bopaContent = R.id.bopaContent)
        }
  1. This shouldn't be there. The click listener should tell the ViewModel: The User pressed save on this item.
viewModel.onSaveBopa(...)

And the ViewModel should launch a coroutine in its scope:

fun onSaveBopa(bopa: Bopa) {
   viewModelScope.launch { 
      repo.updateBopa(bopa)
   }
}

Keep in mind this is pseudo-code. If you pass the topic/content directly, then also pass the ID so the viewModel knows what BOPA must be updated in the database...

fun onSaveBopa(id: String, topic: String, content: String)

That's a more plausible method to call from your activity. But it really depends on what you're trying to do. in any case the activity should not need to deal with DB, Room, Daos, etc. Rely on your ViewModel, that's what it's doing there.

Anyway, commenting that in the Activity... made the project finally build

Android Studio Notification of Project Built Successfully in 8 seconds

I hope that helps you ;) Good Luck.

Martin Marconcini
  • 26,875
  • 19
  • 106
  • 144
  • Hi Martin, thanks for your suggestion. I tried to implement it but I get errors. I've added the database class to the OP to see if you can advice further? – Phil Tinsley Aug 05 '22 at 16:04
  • Hi Phil, at first sight it's a bit hard to tell what the problem could be; if this is an "open" project hosted in Github, perhaps I can take a quick look some time during this week. If not, I'm afraid you're going to have to take a step back and show us a bit more. Feel free to drop by [this Chat channel here at StackOverflow Chat](https://chat.stackoverflow.com/rooms/210228/android-help) where I _often (but not always)_ hang out and am available for real-time chatting ;) – Martin Marconcini Aug 08 '22 at 09:03
  • Thanks Martin. I've made the github public. Shall I post the link here or in a PM? – Phil Tinsley Aug 08 '22 at 20:56
  • You can make it public. – Martin Marconcini Aug 09 '22 at 07:32
  • Cheers. It can be found here https://github.com/thintin/BOPA I'm new to VC though so it may not be very tidy! – Phil Tinsley Aug 09 '22 at 09:01
  • ps I can't use the chat room you linked to as my reputation isn't high enough :-( – Phil Tinsley Aug 09 '22 at 09:06
  • No problem. I cloned the project and there are a few things. I'll update the answer. – Martin Marconcini Aug 09 '22 at 09:14
  • @PhilTinsley there you have it. – Martin Marconcini Aug 09 '22 at 10:08
  • 1
    thanks! I mentioned my inexperience with VC which was valid as the code you commented on was older than the code I'm working on. I'm adding VC and view model training to my growing list! I'll work through the suggestions you made and see how far I get. Perhaps this project was too big for my first? :-D – Phil Tinsley Aug 10 '22 at 10:29
  • If this is your absolute first android project, I'd say yeah, quite ambitious to work with Room already (not sure how much previous Kotlin or Java experience you have). But yes, if you're going to do Android, only very stubborn people or old projects don't use a ViewModel and such. Good luck on your Journey ;) – Martin Marconcini Aug 11 '22 at 08:15