Android: Check if object is present in database using Room and RxJava - android

I'm using Room as ORM and here is my Dao interface:
#Dao
interface UserDao {
#Query(value = "SELECT * FROM User LIMIT 1")
fun get(): Single<User?>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun add(profile: User)
}
In other hand I have a method in Repository which should check if the user is login or not, here is the implementation code:
override fun isUserLogin(): Single<Boolean> = userDao
.get().async()
.onErrorReturn { null }
.map { it != null }
Room will throw the following exception if no row matches the query:
Query returned empty result set: SELECT * FROM User LIMIT 1
I want to return null in this case, but when I execute the code it throw an exception with the following messsage:
Value supplied was null
I can't use Optional because Dao is returning Single<User?> so onErrorReturn should return the same type.
How can I check if the user exists or not without changing Dao?

I think the proper way to do this, besides Android and Room, is to use COUNT in your query.
#Query("SELECT COUNT(*) from User")
fun usersCount() : Single<Int>
It will return the number of rows in the table, if it's 0 you know there is no user in the db, so.
override fun isUserLogin(): Single<Int> = userDao
.usersCount()
.async()
.map { it > 0 }
If you really want to do it the ugly way though:
#Query(value = "SELECT * FROM User LIMIT 1")
fun get(): Single<User>
Don't use a nullable type, it's pointless. Then map to Boolean and handle the exception:
override fun isUserLogin(): Single<Boolean> = userDao
.get()
.async()
.map { true }
.onErrorReturn { false }

Use Maybe, which accurately models the response expecting either 0 results, 1 result or an exception.

If you change your DAO to return a Flowable list of users you can then map this into a boolean value by checking the size of the list.
#Query(value = "SELECT * FROM User LIMIT 1")
fun get(): Flowable<List<User>>
e.g.
fun isUserLogin(): Single<Boolean> = userDao
.get()
.flatMapSingle { Single.just(it.size == 1) }
.onErrorReturn { false }
.first(false)

Related

How to get the id of inserted row in android room databse in Kotlin?

I am trying to get the user ID from the newest user. How can I make the insert method spit the ID when the ID is autogenerated?
in Model
#PrimaryKey(autoGenerate = true)
val userId: Int
in Dao
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun addUserWithLong(user: User): LiveData<Long>
in Repository
fun addUserWitLong(user: User): LiveData<Long> {
return userDao.addUserWithLong(user)
}
in ViewModel
fun addUserWithLong(user: User): LiveData<Long> {
return repository.addUserWitLong(user)
}
in Fragment
val id: Long? = userViewModel.addUserWithLong(user).value
I have read in the docs that #Insert returns Long as the row ID but I do not know how to program it. Now the error is "Not sure how handle insert method return type." Is there some way to make with LiveData and not with Rxjava. That is without the need to download more dependecies.
As per the documentation here
If the #Insert method receives a single parameter, it can return a
long value, which is the new rowId for the inserted item. If the
parameter is an array or a collection, then the method should return
an array or a collection of long values instead, with each value as
the rowId for one of the inserted items. To learn more about returning
rowId values, see the reference documentation for the #Insert
annotation, as well as the SQLite documentation for rowid tables
So you can use it like
#Insert(onConflict = OnConflictStrategy.REPLACE)
long addUserWithLong(user: User)
or if you are inserting a list
#Insert(onConflict = OnConflictStrategy.REPLACE)
long[] addUserWithLong(user: List<User>)
Edit-1
After checking answers from this post.
No, you can't. I wrote an answer to the issue. The reason is, that
LiveData is used to notify for changes. Insert, Update, Delete won't
trigger a change.
I just created a test project and successfully received Id of last inserted item in activity. Here is my implementation.
Dao
#Insert
suspend fun addUser(user: Users): Long
Repo
suspend fun insertUser(context: Context, users: Users): Long {
val db = AppDatabase.getInstance(context)
val dao = db.userDao()
return dao.addUser(users)
}
ViewModel
fun addUser(context: Context, users: Users) = liveData {
//you can also emit your customized object here.
emit("Inserting...")
try {
val userRepo = UsersRepo()
val response = userRepo.insertUser(context, users)
emit(response)
} catch (e: Exception) {
e.printStackTrace()
emit(e.message)
}
}
Activity
viewModel.addUser(applicationContext, user).observe(this, Observer { userId ->
Log.d("MainActivity", "Inserted User Id is $userId")
})
Check test application here.

Kotlin Flow - Nullable value

Is it possible to check if Flow sends back a value and to act on it if it doesn't?
override suspend fun insertUserResponse(userResponse: UserResponse) {
val userResponseFromBDD: Flow<UserResponse>? = userResponseDAO.searchUserByToken(userResponse.profilePOJO.uniqueID)
userResponseFromBDD?.collect {
userResponseDAO.updateUser(userResponse)
} ?: {
userResponseDAO.insertUser(userResponse)
}
}
Several remarks:
There are two types of queries - one-shot and streams. For your use-case you need one-shot query (to ge response or null once with searchUserByToken and once - to insert or update value). For one-shot queries in Room you can use suspend function with no Flow:
#Query("SELECT * FROM userResponse where id = :id")
suspend fun searchUserByToken(id: Int):UserResponse?
And your code with checking value would be:
override suspend fun insertUserResponse(userResponse: UserResponse) {
val userResponseFromBDD = userResponseDAO.searchUserByToken(userResponse.profilePOJO.uniqueID)
userResponseFromBDD?.let { userResponseDAO.updateUser(userResponse)}
?: userResponseDAO.insertUser(userResponse)
}
With Flow you get stream, that issues values whenever data is updated in DB. That's why in your use-case you can get values in loop: you get value value from searchUserByToken -> you update value -> you get new value since Room uses Flow and invokes searchUserByToken again -> you update value -> ...
If you use Room's #Insert(onConflict = OnConflictStrategy.REPLACE) you can not to check if userResponse is in DB and use just insertUser method (since if user is in db insert would cause it's update)

Sorting list with repository and ViewModel

First time adding an object (Deck) is added invisibly. It will only appear once the sorting method has been chosen from context menu. However, this has to be repeated each time for the screen to be updated.
The issue should lie within the repository as getAllDecks references allDecks. It is as if allDecks is not updating or not realising its .value is changing as allDecks.postValue() intakes the List<Deck> from database. This isn't a LiveData<List<Deck>>, it only does a one time thing. How to make repository reading constant updates from database?
I am trying to sort a list stored in the repository. My ViewModel has a List<obj> referencing items in repository. Sorting occurs when a user presses a context menu item. This action isn't working. The debugging tool showed repository method being called and things were reassigned. This should have worked as ViewModel was referencing the repository and MainActivity would automatically update if the list changed.
MainActivity context menu opens and reacts to onClick sorting changes. Thus it is called. I also know since delete and insert queries are working that MainActivity is listening to the ViewModel's changing list. What am I doing wrong? Also, how to debug database queries (when debugger transitions to viewing SQLite query it gets in a loop)?
Main Activity (abbreviated) :
globalViewModel.sortBy(Sort.ALPHA_ASC) //Set default sorting
//Listen for livedata changes in ViewModel. if there is, update recycler view
globalViewModel.allDecks.observe(this, Observer { deck ->
deck?.let { adapter.setDecks(deck) }
})
override fun onContextItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.sort_by_alpha_asc -> { globalViewModel.sortBy(Sort.ALPHA_ASC) ; currentSort = Sort.ALPHA_ASC ; contextMenuText.setText(R.string.sort_by_alpha_asc) ; return true; }
R.id.sort_by_alpha_desc -> { globalViewModel.sortBy(Sort.ALPHA_DES) ; currentSort = Sort.ALPHA_DES ; contextMenuText.setText(R.string.sort_by_alpha_des) ; return true; }
R.id.sort_by_completed_hidden -> { globalViewModel.sortBy(Sort.NON_COM) ; currentSort = Sort.NON_COM ; contextMenuText.setText(R.string.sort_by_non_complete) ; return true; }
R.id.sort_by_due_date -> { globalViewModel.sortBy(Sort.DUE_DATE) ; currentSort = Sort.DUE_DATE ; contextMenuText.setText(R.string.sort_by_due_date) ; return true; }
else -> return super.onContextItemSelected(item)
}
}
View Model :
private val repository: DeckRepository
val allDecks: LiveData<List<Deck>>
init {
val decksDao = FlashCardDB.getDatabase(application, viewModelScope).DeckDAO()
repository = DeckRepository(deckDao = decksDao)
allDecks = repository.getAllDecks()
}
fun sortBy(sortMethod: Sort) = viewModelScope.launch(Dispatchers.IO) {
when (sortMethod) {
Sort.ALPHA_ASC -> repository.sortBy(Sort.ALPHA_ASC)
Sort.ALPHA_DES -> repository.sortBy(Sort.ALPHA_DES)
Sort.NON_COM -> repository.sortBy(Sort.NON_COM)
Sort.DUE_DATE -> repository.sortBy(Sort.DUE_DATE)
}
}
DeckRepository :
private var allDecks = MutableLiveData<List<Deck>>() //instantiate object
fun getAllDecks(): LiveData<List<Deck>> = allDecks //Repository handles livedata transmission. ViewModel references the actual Data.
suspend fun sortBy(sortingMethod: Sort) {
when (sortingMethod) {
Sort.ALPHA_ASC -> allDecks.postValue(deckDao.getDecksSortedByAlphaAsc())
Sort.ALPHA_DES -> allDecks.postValue(deckDao.getDecksSortedByAlphaDesc())
Sort.NON_COM -> allDecks.postValue(deckDao.getDecksSortedByNonCompleted())
Sort.DUE_DATE -> allDecks.postValue(deckDao.getDecksSortedByDueDate())
}
}
suspend fun insert(deck: Deck) {
deckDao.insert(deck)
}
Database :
//Sorting
#Query("SELECT * from deck_table ORDER BY title ASC")
fun getDecksSortedByAlphaAsc(): List<Deck>
#Query("SELECT * from deck_table ORDER BY title DESC")
fun getDecksSortedByAlphaDesc(): List<Deck>
#Query("SELECT * from deck_table WHERE completed=1 ORDER BY title ASC")
fun getDecksSortedByNonCompleted(): List<Deck>
#Query("SELECT * from deck_table ORDER BY date ASC")
fun getDecksSortedByDueDate(): List<Deck>
//Modifying
#Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(deck: Deck)
Your activity isn't showing the changes in real time because your repository reassigns its LiveData. While your approach of having a single LiveData to be observed by the activity is correct, you should actually change only its value, not the reference if that makes sense.
Here's an example:
Repository
private val allDecks = MutableLiveData<List<Deck>>()
fun getAllDecks(): LiveData<List<Deck>> = allDecks
fun sortBy(sortingMethod: Sort) {
when (sortingMethod) {
/* If you're handling your DB operations with coroutines, this function
* should be suspendable and you should set the value to allDecks
* with postValue
*/
Sort.ALPHA_ASC -> allDecks.value = deckDao.getDecksSortedByAlphaAsc()
Sort.ALPHA_DES -> allDecks.value = deckDao.getDecksSortedByAlphaDesc()
Sort.NON_COM -> allDecks.value = deckDao.getDecksSortedByNonCompleted()
Sort.DUE_DATE -> allDecks.value = deckDao.getDecksSortedByDueDate()
}
}
Consequently, your DAO queries will no longer return LiveData, but the lists themselves:
DAO
#Query("SELECT * from deck_table ORDER BY title ASC")
fun getDecksSortedByAlphaAsc(): List<Deck>
#Query("SELECT * from deck_table ORDER BY title DESC")
fun getDecksSortedByAlphaDesc(): List<Deck>
#Query("SELECT * from deck_table WHERE completed=1 ORDER BY title ASC")
fun getDecksSortedByNonCompleted(): List<Deck>
#Query("SELECT * from deck_table ORDER BY date ASC")
fun getDecksSortedByDueDate(): List<Deck>
The problem is in DAO's query. It returns List, witch is not Flow or LiveData, thus it can't be observed and react to changes in list... It should be like this:
#Query("SELECT * from deck_table ORDER BY title ASC")
fun getDecksSortedByAlphaAsc(): Flow<List<Deck>>
#Query("SELECT * from deck_table ORDER BY title DESC")
fun getDecksSortedByAlphaDesc(): Flow<List<Deck>>
#Query("SELECT * from deck_table WHERE completed=1 ORDER BY title ASC")
fun getDecksSortedByNonCompleted(): Flow<List<Deck>>
#Query("SELECT * from deck_table ORDER BY date ASC")
fun getDecksSortedByDueDate(): Flow<List<Deck>>
Now you can iplement allDecks variable witch will be LiveData<List> using flow and Transformations. Something like this:
val sortingMethod: LiveData<Sort> = "your sorting type here"
val allDecks: LiveData<List<Deck>> = Transformations.switchMap(sortingMethod) {
when (sortingMethod) {
Sort.ALPHA_ASC -> allDecks.value = deckDao.getDecksSortedByAlphaAsc().asLiveData()
Sort.ALPHA_DES -> allDecks.value = deckDao.getDecksSortedByAlphaDesc().asLiveData()
Sort.NON_COM -> allDecks.value = deckDao.getDecksSortedByNonCompleted().asLiveData()
Sort.DUE_DATE -> allDecks.value = deckDao.getDecksSortedByDueDate().asLiveData()
}
Whats happens is we observe liveData sorting method and change our allDecks variable every time when it changes.
After this you just need to observe allDecks and submit it to recycler view adapter every time it changes.
Something like this:
viewModel.allDecks.observe(this.viewLifecycleOwner) {
adapter.submitList(it)
}
P.S. RecyclerViews adapter must be implemented using DiffCallback so it can change every time list is changed...

How to get a List of cocktails based on the name

From my API, I get all the cocktails by name, lets say, if I put margarita in the searchView of my app. It will return all the cocktails that match that query. I save all the queries the user does to let them search for cocktails even when they are offline.
I have created this query that should return a list of cocktails if the name of the cocktail matches the name passed in the searchView
#Query("SELECT *
FROM cocktailTable
WHERE cocktail_name = :cocktailName")
suspend fun getCocktails(cocktailName:String):List<CocktailEntity>
But this does not return any cocktails even when the fetch has succefully added those cocktails in my database, any clue ?
I have tried with the IN operator like this
#Query("SELECT *
FROM cocktailTable
WHERE cocktail_name IN (:cocktailName)")
suspend fun getCocktails(cocktailName:String):List<CocktailEntity>
But it also does not return all the cocktails with that name
Added code for the call
override suspend fun getCocktailByName(cocktailName: String): Resource<List<Cocktail>>? {
when (val cocktailList = networkDataSource.getCocktailByName(cocktailName)) {
is Resource.Success -> {
for (cocktail in cocktailList.data) {
saveCocktail(cocktail.asCocktailEntity())
}
}
is Resource.Failure -> {
return getCocktails(cocktailName)
}
}
return getCocktails(cocktailName)
}
override suspend fun getCocktails(cocktailName: String): Resource<List<Cocktail>>? {
return cocktailDao.getCocktails(cocktailName)
}
Try LIKE '%' || :drinkName || '%' your api might give you whitespaces. You can also try trimming before saving the entity.

LiveData + ViewModel + Room: Exposing a LiveData returned by query which changes over time (Through a fts search)

I have an FTS query in my DAO which I'd like to use to provide search in my App. The activity passes the query to view model each time the search text is changed.
The problem is that, Room returns a LiveData every single time the query is executed while I'd like to get same LiveData object updated when I run the query.
I was thinking about copying data from the LiveData which room returns into my dataSet (see the code below). Would it be a good approach? (And if yes, how would I actually do that?)
Here's my work so far:
In my Activity:
override fun onCreate(savedInstanceState: Bundle?) {
//....
wordViewModel = ViewModelProviders.of(this).get(WordMinimalViewModel::class.java)
wordViewModel.dataSet.observe(this, Observer {
it?.let {mRecyclerAdapter.setWords(it)}
})
}
/* This is called everytime the text in search box is changed */
override fun onQueryTextChange(query: String?): Boolean {
//Change query on the view model
wordViewModel.searchWord(query)
return true
}
ViewModel:
private val repository :WordRepository =
WordRepository(WordDatabase.getInstance(application).wordDao())
//This is observed by MainActivity
val dataSet :LiveData<List<WordMinimal>> = repository.allWordsMinimal
//Called when search query is changed in activity
//This should reflect changes to 'dataSet'
fun searchWord(query :String?) {
if (query == null || query.isEmpty()) {
//Add all known words to dataSet, to make it like it was at the time of initiating this object
//I'm willing to copy repository.allWordsMinimal into dataSet here
} else {
val results = repository.searchWord(query)
//Copy 'results' into dataSet
}
}
}
Repository:
//Queries all words from database
val allWordsMinimal: LiveData<List<WordMinimal>> =
wordDao.getAllWords()
//Queries for word on Room using Fts
fun searchWord(query: String) :LiveData<List<WordMinimal>> =
wordDao.search("*$query*")
//Returns the model for complete word (including the definition for word)
fun getCompleteWordById(id: Int): LiveData<Word> =
wordDao.getWordById(id)
}
DAO:
interface WordDao {
/* Loads all words from the database */
#Query("SELECT rowid, word FROM entriesFts")
fun getAllWords() : LiveData<List<WordMinimal>>
/* FTS search query */
#Query("SELECT rowid, word FROM entriesFts WHERE word MATCH :query")
fun search(query :String) :LiveData<List<WordMinimal>>
/* For definition lookup */
#Query("SELECT * FROM entries WHERE id=:id")
fun getWordById(id :Int) :LiveData<Word>
}
val dataSet :LiveData<List<WordMinimal>>
val searchQuery = MutableLiveData<String>()
init {
dataSet = Transformations.switchMap(searchQuery) { query ->
if (query == null || query.length == 0) {
//return WordRepository.getAllWords()
} else {
//return WordRepository.search(query)
}
}
}
fun setSearchQuery(searchedText: String) {
searchQuery.value = searchedText
}

Categories

Resources