Sorting list with repository and ViewModel - android

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...

Related

AlertDialog setItems with a list from room database: getValue() returns null

I'm simply trying to select from a list of Artists in my room database in an AlertDialog. Calling getValue() on the LiveData object from the viewModel consistently gives me null. Do I really need to make a ListAdapter for something this simple?! Why is it so hard to get some strings from the database?
ArtistDao
#Dao
interface ArtistDao {
#Query("SELECT * FROM artist_table")
fun getAllArtists(): Flow<List<Artist>>
#Query("SELECT name FROM artist_table")
fun getArtistList(): Flow<List<String>>
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(artist: Artist)
}
Menu Selection Option in Main Activity
R.id.action_choose_artist -> {
//create artist list
val testList = songViewModel.artists.value
val artistList = testList?.toTypedArray()
// User chose choose artist action
val alertDialog: AlertDialog? = this.let {
val builder = AlertDialog.Builder(it)
builder.apply {
setTitle(R.string.choose_artist)
setItems(artistList, DialogInterface.OnClickListener { dialog, which ->
// The 'which' argument contains the index position
// of the selected item
artistName = artistList!![which]
})
}
// Create the AlertDialog
builder.create()
}
alertDialog?.show()
true
}
SongViewModel
val artists: LiveData<List<String>> = repository.artists.asLiveData()
SongRepository
val artists: Flow<List<String>> = artistDao.getArtistList()
You always get null because nobody is observing your Flow->LiveData-chain. LiveData itself will only trigger and perform its work if somebody is observing it.
In your case I think you want a one-time request to receive the data from your DB and should use a suspend function and coroutines to achieve it. Flow in context of Room DB is only useful if you are interested in changes to the DB for this Query and want to react to it, e.g. by displaying the updated data in your RecyclerView.
Dao:
#Dao
interface ArtistDao {
#Query("SELECT name FROM artist_table")
fun getArtistList(): List<String>
}
Repo:
suspend fun getArtistList(): List<String> = artistDao.getArtistList()
ViewModel:
fun getArtistList(onResult: (List<String>) -> Unit) {
viewModelScope.launch{
val artist = withContext(Dispatchers.IO){repository.getArtistList()}
// maybe validate data, e.g. not empty
onResult(artist)
}
}
Activity:
R.id.action_choose_artist -> {
//create artist list
songViewModel.getArtistList { testList ->
val artistList = testList.toTypedArray()
// User chose choose artist action
...
}
}

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
}

Access and Delete a row in one query in SQLite

I am using Room Database to make a database to store information in a table. I want to access one entry from the table and delete the same entry without the need to call two functions.
#Query("SELECT * FROM history_packet_table ORDER BY timestamp ASC LIMIT 1")
fun get(): HistoryPacket?
#Query("DELETE FROM history_packet_table ORDER BY timestamp ASC LIMIT 1")
fun delete()
I want these two operations to happen only by calling get. Is there a way?
I believe that you can add the following to the Dao :-
#Transaction
fun getAndDelete() {
get()
delete()
}
Obviously you can call the function what you wish. However, the get seems to be useless as it is.
So you may want something like :-
#Query("SELECT * FROM history_packet_table WHERE timestamp = (SELECT min(timestamp) FROM history_packet_table)")
fun get() :HistoryPacketTable
#Query("DELETE FROM history_packet_table WHERE timestamp = (SELECT min(timestamp) FROM history_packet_table)")
fun delete() :Int
#Transaction
fun getAndDelete() :HistoryPacketTable {
// Anything inside this method runs in a single transaction.
var rv: HistoryPacketTable = get()
val rowsDeleted: Int = delete()
if (rowsDeleted < 1) {
rv = HistoryPacketTable();
//....... set values of rv to indicate not deleted if needed
}
return rv
}
Note as LIMIT on delete is turned off by default, the queries can be as above, this assumes that timestamp is unique otherwise multiple rows may be deleted, in which case the Dao could be something like
:-
#Delete
fun delete(historyPacketTable: HistoryPacketTable) :Int
#Transaction
fun getAndDelete() :HistoryPacketTable {
// Anything inside this method runs in a single transaction.
var rv: HistoryPacketTable = get()
val rowsDeleted: Int = delete(rv)
if (rowsDeleted < 1) {
rv = HistoryPacketTable();
//....... set values to indicate not deleted
}
return rv
}

Shuffle LiveData<List<Item>> from Room Database on App Open

I have a RecyclerView which displays LiveData<List<Item>> returned from a Room Database. Everything works fine, however, the Item order needs to be randomized every time the app is open for a more dynamic feel.
The Item's are displayed in AllItemFragment. When an item is clicked, it will be added to the users favourites. This will then add the Item to the FavouriteFragment.
Ordering the SQL query by RANDOM() would be called every time the data is changed (i.e. when an item is clicked) and therefore wont work.
List.shuffle cannot be called on LiveData object for obvious reasons.
Data is retrieved in the following format:
DAO -> Repository -> SharedViewholder -> Fragment -> Adapter
DAO
#Query("SELECT * from items_table")
fun getAllItems(): LiveData<MutableList<Item>>
Repository
val mItemList: LiveData<MutableList<Item>> = itemDoa.getAllItems()
SharedViewHolder
init {
repository = ItemRepository(itemDao)
itemList = repository.mItemList
}
fun getItems(): LiveData<MutableList<Item>> {
return itemList
}
Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mSharedViewModel = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
mSharedViewModel.getItems().observe(viewLifecycleOwner, Observer { item ->
// Update the UI
item.let { mAdapter.setItems(it!!) }
})
}
Adapter
internal fun setItems(items: MutableList<Item>) {
val diffCallback = ItemDiffCallback(this.mItems, items)
val diffResult = DiffUtil.calculateDiff(diffCallback)
this.mItems.clear()
this.mItems.addAll(items)
diffResult.dispatchUpdatesTo(this)
}
EDIT
Using switchMap() still shuffles the entire list when a user presses the favourite button
fun getItems(): LiveData<MutableList<Item>> {
return Transformations.switchMap(mItemList) { list ->
val newLiveData = MutableLiveData<MutableList<Item>>()
val newList = list.toMutableList()
Collections.shuffle(newList)
newLiveData.setValue(newList)
return#switchMap newLiveData }
}
Just use .shuffled() with seeded Random instance. The idea is to randomize the list, but the randomize in the same way, until the process dies and the user relaunches the app to generate a new seed.
Repository
private val seed = System.currentTimeMillis()
val mItemList: LiveData<MutableList<Item>> = Transformations.map(itemDoa.getAllItems()) {
it.shuffled(Random(seed))
}
The seed must be consistent throughout the application's process. I think keeping the seed in the repository is pretty safe, assuming that your repository is implemented in a singleton pattern. If it is not the case, just find yourself a singleton object and cache the seed.
You should consider using switchMap transformation operator on LiveData.
return liveData.switchMap(list -> {
var newLiveData = LiveData<MutableList<Item>>()
var newList = list.toMutableList()
Collections.shuffle(newList)
newLiveData.setValue(newList)
return newLiveData
})
For creating new LiveData you can use LiveData constructor and setValue(T value) method.
As value you can set Collections.shuffle(list)
You could use it in your repository or in the view model.

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

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)

Categories

Resources