Android Room dao return null crash - android

I'm trying to implement offline first app by returning local data to UI before fetching remote data.
Here's my code
Repository
val trip: LiveData<DomainTrip> = Transformations.map(database.tripDao.getTrip(tripId)) {
it.asDomainModel()
}
suspend fun refreshTrip(token: String) {
withContext(Dispatchers.IO) {
val trip = webservice.getTrip(tripId, "Bearer $token").await()
database.tripDao.insertAll(trip.asDatabaseModel())
}
}
DAO
interface TripDao {
#Query("select * from databasetrip WHERE _id = :id")
fun getTrip(id: String): LiveData<DatabaseTrip>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(trip: DatabaseTrip)
}
ViewModel
private val tripRepository = TripRepository(getDatabase(application), tripId)
var trip = tripRepository.trip
If the user is opening a trip that is already stored in the database, above code works without any problem. it.asDomainModel() gets called as soon as user opens that trip. it.asDomainModel() gets called again as soon as that trip is retrieved from remote and saved into the database.
My problem is, if user is opening a trip that is not in the database, above code crashes when it.asDomainModel() is called the first time, with null pointer exception on it.
What confuses me more is that if above code were applied to this dao query
#Query("select * from databasetripinfo")
fun getTrips(): LiveData<List<DatabaseTripInfo>>
i won't get any null pointers exception on both call of it.asDomainModel() even when my database is empty.
Can somebody please help me? How do I avoid null pointer exception on it.asDomainModel() when database does not have that record?
thx

It's all OK. If there is no item with the given id, the live data will return null. You can check for nullity before mapping in the transformation (it?.asDomainModel)
However for lists you will get empty list instead of null (it's a convention).

Related

Android Room Pre-populated Data not visible first time

Freshly installing the app, the view model doesn't bind the data.
Closing the app and opening it again shows the data on the screen.
Is there any problem with the pre-population of data or is the use of coroutine is not correct?
If I use Flow in place of LiveData, it collects the data on the go and works completely fine, but its a bit slow as it is emitting data in the stream.
Also, for testing, The data didn't load either LiveData/Flow.
Tried adding the EspressoIdlingResource and IdlingResourcesForDataBinding as given here
Room creation
#Provides
#Singleton
fun provideAppDatabase(
#ApplicationContext context: Context,
callback: AppDatabaseCallback
): AppDatabase {
return Room
.databaseBuilder(context, AppDatabase::class.java, "database_name")
.addCallback(callback)
.build()
AppDatabaseCallback.kt
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
CoroutineScope(Dispatchers.IO).launch {
val data = computePrepopulateData(assets_file_name)
data.forEach { user ->
dao.get().insert(user)
}
}
}
Dao
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
#Query("SELECT * FROM $table_name")
suspend fun getAllUser(): List<User>
ViewModel
CoroutineScope(Dispatchers.IO).launch {
repository.getData().let {
listUser.postValue(it)
}
}
Attaching the data using BindingAdapter
app:list="#{viewModel.listUser}"
Your DAO returns suspend fun getAllUser(): List<User>, meaning it's a one time thing. So when the app starts the first time, the DB initialization is not complete, and you get an empty list because the DB is empty. Running the app the second time, the initialization is complete so you get the data.
How to fix it:
Switch getAllUser() to return a Flow:
// annotations omitted
fun getAllUser(): Flow<List<User>>
Switch insertUser to use a List
// annotations omitted
suspend fun insertUser(users: List<User>)
The reason for this change is reducing the number of times the Flow will emit. Every time the DB changes, the Flow will emit a new list. By inserting a List<User> instead of inserting a single User many times the (on the first run) Flow will emit twice (an empty list + the full list) compared to number of user times with a single insert.
Another way to solve this issue is to use a transaction + insert a single user.
I recommend you use viewModelScope inside the ViewModel to launch coroutines so it's properly canceled when the ViewModel is destroyed.

Unlike Livedata, using Flow in Room query doesn't trigger a refresh when updating a table entry but works if I delete or insert an entry

When using Livedata as a return type for a select* query on a table in Room, then I observe on it, I get triggers if I update/insert/delete an entry in that table. However, when I tried using Kotlin Flow, I only get 2 triggers.
The first trigger gives a null value as the initial value of the stateflow is a null. The second trigger is the list of entries in the Room table.
If I perform an insert/delete action on the DB, I receive a trigger from the StateFlow.
However, If I update an entry, the Stateflow doesn't trigger.
N.B: The update operation works correctly on the DB. I checked using DB inspector.
Data class & DAO
#Entity
data class CartItem (
#PrimaryKey
val itemId: Int,
var itemQuantity: Int=1
)
#Dao
interface CartDao {
#Query("SELECT * FROM CartItem")
fun getAllItems(): Flow<List<CartItem>>
#Update
suspend fun changeQuantityInCart(cartItem:CartItem)
#Insert
suspend fun insert(item: CartItem)
#Delete
suspend fun delete(cartItem:CartItem)
}
ViewModel
val cartItems: StateFlow<List<CartItem>?> =
repo.fetchCartItems().stateIn(viewModelScope, SharingStarted.Lazily, null)
Fragment
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.cartItems.collect {
Log.e("Update","Update")
}
My pitfall was that I was updating the object like this:
currentItem.itemQuantity = currentItem.itemQuantity + 1
changeQuantity(currentItem)
(currentItem is an object of class CartItem which is received initially from the getAllItems Flow in the DAO.)
(changeQuantity fun calls the changeQuantityInCart fun in the DAO.
This caused the reference of the CartItem object in the StateFlow to hold the updated value of the object with the new itemQuantity value before calling the update on the DB.
After that, when calling the Update fun in the DAO, the DB entry is updated and the Flow value changes, but when putting it in the Stateflow no changes are detected. Thus, the stateflow doesn't trigger as it is how stateflows differ from livedata.
In the case of livedata, it will trigger regardless if the new value is the same or not.
Thus, to solve this bug do not change the value of the object in the stateFlow before calling a DB update operation like this:
val updatedCartItem = cartItem.copy(itemQuantity = cartItem.itemQuantity + 1)
changeQuantity(updatedCartItem)

Android database (Room) not updating after application update

I'm kind of new to Android development, but I'm taking over some project somebody more experimented than me did.
I'm making an application that retrieves data from the internet at startup and creates (or updates) a room database to store the information.
So far, it works. However, I noticed that when I push an update of the application, for some users, the database doesn't update anymore. They can reinstall the app, it doesn't change anything. The only thing that works is to clear the cache and restart the application. Then everything goes back to normal, when data are retrieved from the internet, they are properly inserted in the database. But the problem comes back with the next update.
I added the 'fallbackToDestructiveMigration()' option but it doesn't help, because it's not a migration of the database per se, as the structure doesn't change here.
Ideally, I'd like to preserve the data already present in the database.
I'm using Room 2.2.5 and API 28.
I'm not sure why updating the app results in the database not updating anymore. Maybe the new version of the app creates another database and populates this one, but the app is still using the old one (not updated anymore).
The Storage Module:
val storageModule = module {
single {
Room.databaseBuilder(androidContext(), LessonDatabase::class.java, "MyDB")
.fallbackToDestructiveMigration().build()
}
single {
get<LessonDatabase>().lessonDao()
}
}
The LessonDatase:
#Database(entities = [Lesson::class], version = BuildConfig.DATABASE_VERSION, exportSchema = false)
abstract class LessonDatabase : RoomDatabase() {
abstract fun lessonDao(): LessonDao
}
The LessonDao:
#Dao
interface LessonDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertLesson(lesson: Lesson)
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertLessons(lessons: List<Lesson>)
#Update
fun updateLesson(lesson: Lesson)
#Delete
fun deleteLesson(lesson: Lesson)
#Query("DELETE FROM Lesson")
fun clearLessons()
#Query("SELECT * FROM Lesson WHERE id == :id")
fun getLessonById(id: String): Lesson
#Query("SELECT * FROM Lesson ORDER BY creation_date DESC")
fun getLessons(): List<Lesson>
#Query("SELECT * FROM Lesson WHERE favorite = 1")
fun getFavoriteLessons(): List<Lesson>
#Query("SELECT * FROM Lesson WHERE difficulty LIKE :level")
fun getLessonsByDifficulty(level: Int): List<Lesson>
}
And the code for the application startup:
class SplashscreenViewModel(
private val repository: LessonRepository,
private val lessonDao: LessonDao,
val context: Context
) : BaseViewModel() {
val nextScreenLiveData = MutableLiveData<Boolean>()
override fun start() {
ioScope.launch {
val lessons = repository.getLessons().filter {
it.site.contains("website")
}.filter {
DataUtils.isANumber(it.id)
}
lessonDao.insertLessons(lessons)
nextScreenLiveData.postValue(true)
}
}
override fun stop() {
}
}
A question I have is, if I update the application, I guess Room.databaseBuilder will be called again. But what happens if the name of the database is the same as the previous one? Will it retrieve the old one, or create a new one? Overwrite the old one?
Another question I have, in the Insert query, it says onConflictStrategy.IGNORE. But as I pass a list as parameters, what happens if some of the entries are already in the database and some not? Will it ignore all of them? Just the already existing ones?
Thank you.
Edit: I installed Android-Debug-Database (https://github.com/amitshekhariitbhu/Android-Debug-Database) and it seems the database is fine actually. The only problem is that when I update the app, the new entries I insert are returned at the end of the SELECT * FROM table query. So I tried to sort them by Id, and it seems to work.

Android - Kotlin Coroutines for handling IllegalStateException: Cannot access database on the main thread

In my Android App, I use Room as local database to store the Account information of a user. When I make a simple Room request to retrieve the Account object stored in the database, I get the following error message :
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
Here, is the Fragment code from which I make the local database request:
// AccountInformationFragment.kt
accountDataFragmentViewModel.retrieveAccountData(accountId).observe(viewLifecycleOwner, Observer {
// do some stuff
})
In the ViewModel class I have implemented retrieveAccountData() like this:
// AccountInformationFragmentViewModel.kt
// used to get the account from the local datasource
fun retrieveAccountData(id:Long): LiveData<Account>{
val result = MutableLiveData<Account>()
viewModelScope.launch {
val account = authRepository.retrieveAccountData(id)
result.postValue(account)
}
return result
}
In the Repository class, I have implemented retrieveAccountData() like this:
// AccountRepository.kt
suspend fun retrieveAccountData(accId:Long): Account =
accountDao.retrieveAccountData(accId)
I understand that I have to use some sort of asnyc operation because the local database operation may take a long time when its performed on the main thread.
But in the ViewModel class I launched the coroutine inside the viewModelScope. Is that not enough? Based on the exception, it seems not. So, is there someone who could tell me how to do this correctly.
EDIT:
Here is the Dao class :
#Query("SELECT * FROM account_table WHERE id = :id")
fun retrieveAccountData(id: Long) : Account
Thanks in advance
As per the Room documentation, if you want Room to automatically move to a background thread to run your #Query, you can make your method a suspend method:
#Query("SELECT * FROM account_table WHERE id = :id")
suspend fun retrieveAccountData(id: Long) : Account
RoomDB supports LiveData. You could return the query result as a livedata which is by default does the operation in the background thread and observe it in your UI layer. I have modified your query below which will return LiveData instead of Account.
#Query("SELECT * FROM account_table WHERE id = :id")
fun retrieveAccountData(id: Long) : LiveData<Account>

Android: await() seems not to work using Room database

I am working on an app to practise some calculations which saves each given answers and data about practise sessions into a Room database for tracking progress. There is a table that contains the answers and there is a table which contains the sessions, and each row in answer table needs to contain the id of the session in which the answer was given
I am using a Room database thus using coroutines when writing to database.
When the user clicks a button the answer is evaluated and saved, and also the session data is updated.
(Such as number of questions answered and the average score.)
To achieve this, I need to have the Id of the freshly created session data. So what I am trying is to call a method in the init{} block of the ViewModel which uses a Defferred and call await() to wait for the session data to be inserted and then get the last entry from the database and update the instance of SessionData that the view model holds and only when it is all done I enable the button thus we will not try to save any data before we know the current session id.
To test this out, I am using the Log.d() method to print out the current session id.
The problem is that I don't always get the right values. Sometimes I get the same id as previous session was, sometimes I get the correct one (so Logcat in Android Studio looks like: 33,33,35,36,38,38,40,41,42,...etc). However if I get all data from the database and check it out, all the ids are in the database, in correct order, no values are skipped.
For me it seems that await() doesn't actually make the app to wait, it seems to me that the reading of the database sometimes happenes before the writing is complete.
But I have no idea why.
In the ViewModel class:
SimpleConversionFragmentViewModel(
val conversionProperties: ConversionProperties,
val databaseDao: ConversionTaskDatabaseDao,
application: Application) : AndroidViewModel(application){
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private lateinit var sessionData : SessionData
...
init{
startNewSession()
}
...
/**
* This method starts and gets the current session
*/
private fun startNewSession() {
uiScope.launch {
/** Initialize session data*/
sessionData = SessionData()
sessionData.taskCategory = conversionProperties.taskCategory
sessionData.taskType = conversionProperties.taskType
/**
* First insert the new session wait for it to be inserted and get the session inserted
* because we need it's ID
**/
val createNewSession = async { saveSessionDataSuspend() }
val getCurrentSessionData = async { getCurrentSessionSuspend() }
var new = createNewSession.await()
sessionData = getCurrentSessionData.await() ?: SessionData()
_isButtonEnabled.value = true //Only let user click when session id is received!!
Log.d("Coroutine", "${sessionData.sessionId}")
}
}
/**
* The suspend function to get the current session
*/
private suspend fun getCurrentSessionSuspend() : SessionData? {
return withContext(Dispatchers.IO){
var data = databaseDao.getLastSession()
data
}
}
/**
* The suspend function for saving session data
*/
private suspend fun saveSessionDataSuspend() : Boolean{
return withContext(Dispatchers.IO) {
databaseDao.insertSession(sessionData)
true
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
And here is some details from the ConversionTaskDatabaseDao class:
#Dao
interface ConversionTaskDatabaseDao {
#Insert(entity = SessionData::class)
fun insertSession(session: SessionData)
#Query("SELECT * FROM session_data_table ORDER BY session_id DESC LIMIT 1")
fun getLastSession() : SessionData?
...
}
Has anyone got any idea how to solve this?
My first attempt was actually to save the session data only once in the onCleared() method of the ViewModel, but because I have to call the viewModelJob.cancel() method to prevent memory leaks, the job is cancelled before saving is done. But I think it would be a more efficient way if I could save the data here only once.
Or is there a better way to achive what I am trying to do?
Thanks in advance for any help,
Best regards: Agoston
My thought is since you need to wait one suspend method for another and they are already inside coroutine (initiated with launch-builder), you don't need await and you can simplify this:
val createNewSession = async { saveSessionDataSuspend() }
val getCurrentSessionData = async { getCurrentSessionSuspend() }
var new = createNewSession.await()
sessionData = getCurrentSessionData.await() ?: SessionData()
to that:
var new = saveSessionDataSuspend()
sessionData = getCurrentSessionSuspend()
On the contrary when you use await you have no guarantee what method would be first

Categories

Resources