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:
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.
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.
My CrimeFragment gets crimeId UUID from fragment arguments successfully, based on Log debugging.
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!