Home > front end >  Room Query always returns NULL when given UUID argument
Room Query always returns NULL when given UUID argument

Time:01-04

I am currently learning about Room through the Big Nerd Ranch Android Programming textbook. Currently I am able to successfully query for all of the Crimes in my Crimes table, but my query for only one Crime based on UUID always returns null. However, I do know four things that work:

  1. I can run "select * from crime where id = '0f14d0ab-9605-4a62-a9e4-5ed26688389b'" in my DB Browser for SQLite and the crime will return successfully in my database.

  2. I can run @Query("SELECT * FROM crime WHERE id='0f14d0ab-9605-4a62-a9e4-5ed26688389b'") in my CrimeDao and the crime will return successfully in my app.

  3. My CrimeFragment gets crimeId UUID from fragment arguments successfully, based on Log debugging.

  4. My CrimeDetailViewModel's loadCrime() gets the crimeId UUID from CrimeFragment successfully.

However, from there, @Query("SELECT * FROM crime WHERE id=:id") always returns null. This means that I must not be passing in :id correctly, but I am unsure where I went wrong.

Here is my setup:

Crime.kt

@Entity(tableName = "Crime")
data class Crime(
    @PrimaryKey val id: UUID = UUID.randomUUID(),
    var title: String = "",
    var date: Date = Date(),
    var isSolved: Boolean = false,
)

CrimeDao.kt

@Dao
interface CrimeDao {
    @Query("SELECT * FROM crime")
    fun getCrimes(): LiveData<List<Crime>>

    @Query("SELECT * FROM crime WHERE id=:id")
    fun getCrime(id: UUID): LiveData<Crime?>
}

CrimeDatabase.kt

@Database(entities = [Crime::class], version = 1)
@TypeConverters(CrimeTypeConverters::class)
abstract class CrimeDatabase : RoomDatabase() {

    abstract fun crimeDao(): CrimeDao
}

CrimeTypeConverters.kt

class CrimeTypeConverters {

    @TypeConverter
    fun fromDate(date: Date?): Long? {
        return date?.time
    }

    @TypeConverter
    fun toDate(millisSinceEpoch: Long?): Date? {
        return millisSinceEpoch?.let {
            Date(it)
        }
    }

    @TypeConverter
    fun toUUID(uuid: String?): UUID? {
        return UUID.fromString(uuid)
    }

    @TypeConverter
    fun fromUUID(uuid: String?): String? {
        return uuid?.toString()
    }
}

CrimeRepository.kt

private const val DATABASE_NAME = "crime-database"
class CrimeRepository private constructor(context: Context) {

    private val database : CrimeDatabase = Room.databaseBuilder(
        context.applicationContext,
        CrimeDatabase::class.java,
        DATABASE_NAME
    ).createFromAsset("database/crime-database.db")
        .fallbackToDestructiveMigration()
        .build()

    private val crimeDao = database.crimeDao()

    fun getCrimes(): LiveData<List<Crime>> = crimeDao.getCrimes()

    fun getCrime(id: UUID): LiveData<Crime?> = crimeDao.getCrime(id)

    ...
}

CrimeFragment.kt

private const val ARG_CRIME_ID = "crime_id"
class CrimeFragment : Fragment() {

    private lateinit var crime: Crime
    private lateinit var titleField: EditText
    private lateinit var dateButton: Button
    private lateinit var solvedCheckBox: CheckBox
    private val crimeDetailViewModel: CrimeDetailViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
        val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID
        Log.d(TAG, "args bundle crime ID: $crimeId") // this prints the crimeId successfully
        crimeDetailViewModel.loadCrime(crimeId)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_crime, container, false)

        titleField = view.findViewById(R.id.crime_title) as EditText
        dateButton = view.findViewById(R.id.crime_date) as Button
        solvedCheckBox = view.findViewById(R.id.crime_solved) as CheckBox

        dateButton.apply {
            text = CrimeDate.format(crime.date)
            isEnabled = false
        }

        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.d(TAG, "onViewCreated")
        crimeDetailViewModel.crimeLiveData.observe(
            viewLifecycleOwner,
            Observer { crime ->
                Log.d(TAG, "crime observed: $crime") // this claims that crime is null
                crime?.let {
                    Log.d(TAG, "found crime ${crime.title} ${crime.id}") // since crime is null, this let block does not run
                    this.crime = crime
                    updateUI()
                }
            }
        )
    }

    private fun updateUI() {
        titleField.setText(crime.title)
        dateButton.text = crime.date.toString()
        solvedCheckBox.apply {
            isChecked = crime.isSolved
            jumpDrawablesToCurrentState()
        }
        Log.d(TAG, "$crime update UI")
    }

    companion object {

        /**
         * Attach arguments to CrimeFragment before it is added to an activity.
         */
        fun newInstance(crimeId: UUID): CrimeFragment {
            val args = Bundle().apply {
                putSerializable(ARG_CRIME_ID, crimeId)
            }
            return CrimeFragment().apply {
                arguments = args
            }
        }
    }
}

CrimeDetailViewModel.kt

class CrimeDetailViewModel : ViewModel() {

    private val crimeRepository = CrimeRepository.get()
    private val crimeIdLiveData = MutableLiveData<UUID>()

    var crimeLiveData: LiveData<Crime?> =
        Transformations.switchMap(crimeIdLiveData) { crimeId ->
            crimeRepository.getCrime(crimeId)
        }

    fun loadCrime(crimeId: UUID) {
        crimeIdLiveData.value = crimeId
        Log.d(TAG, "${crimeId}") // this successfully prints out the crimeId
    }
}

CodePudding user response:

What you should see is that, you getCrime method in the repository is returning a live data. Inside your viewmodel, you are using

    var crimeLiveData: LiveData<Crime?> =
    Transformations.switchMap(crimeIdLiveData) { crimeId ->
        crimeRepository.getCrime(crimeId)
    }

When you do this, your repository method is getting called, but as it is a live data, you dont get a value there.

What you can do is, you can directly observe the viewmodel method, like

fun loadCrime(crimeId: UUID) = crimeRepository.getCrime(crimeId)

and then observe the function inside your fragment directly like

crimeDetailViewModel.loadCrime(crimeId).observe {
   // Whatever should go inside this.
}

Also, if you are using Kotlin coroutines, you can also use suspend functions inside your repository and dao methods, and inside your viewmodel, you can get the value and set the value to the live data. That would go something like this,

fun loadCrime(crimeId: UUID) {
    val response = crimeRepository.getCrime(crimeId)
    crimeLiveData.value = response
}

and then observe it in your fragment.

CodePudding user response:

One more piece of information: the book obviously uses outdated practices (since Android is always being updated by Google), and so part of my learning was to see if I could instead use the latest dependencies. I essentially just went ahead and used earlier versions of all the dependencies, and this appears to have worked. It is unfortunate that I could not get it to work otherwise, but I think I will figure it out after I go through the entire textbook and then scour the Android documentation and codelabs. Thank you!

  •  Tags:  
  • Related