LiveData not updating observer after Room database query - android

I have a room database where I have songs associated with artists and when I change artist from the Main Activity overflow menu, the fragment with the recyclerview showing a list of songs doesn't update unless I navigate away from the fragment and back again. I thought my observing of the list was sufficient because it worked for other changes being made but not this time.
How do I get it to update with the new artist's songs when data changes?
songViewModel
//default artist name for this phase of production
var artistName = "Ear Kitty"
private val _artistNameLive = MutableLiveData<String>(artistName)
val artistNameLive: LiveData<String>
get() = _artistNameLive
private var _allSongs : MutableLiveData<List<SongWithRatings>> = repository.getArtistSongsWithRatings(artistNameLive.value.toString()).asLiveData() as MutableLiveData<List<SongWithRatings>>
val allSongs: LiveData<List<SongWithRatings>>
get() = _allSongs
fun changeArtist(artist: String){
_artistNameLive.value = artist
artistName = artist
updateAllSongs()
}
fun updateAllSongs() = viewModelScope.launch {
run {
_allSongs = repository.getArtistSongsWithRatings(artistNameLive.value.toString())
.asLiveData() as MutableLiveData<List<SongWithRatings>>
}
}
MainFragment
The observer worked fin when changes were made to all songs but not when it was updated entirely with a different artist.
songViewModel.allSongs.observe(viewLifecycleOwner) { song ->
// Update the cached copy of the songs in the adapter.
Log.d("LiveDataDebug","Main Fragment Observer called")
Log.d("LiveDataDebug",song[0].song.songTitle)
song.let { adapter.submitList(it) }
}
I find a bunch of answers saying to make a function in the Fragment that I call from Main Activity but I don't know where that function would go since I can't make the adapter a member outside of the onViewCreated. At first I'd get an error saying the adapter may have changed then I tried this below and got a null pointer exception.
MainFragment
lateinit var adapter: ItemAdapter
fun notifyThisFragment(){
adapter.notifyDataSetChanged()
}
MainActivity
songViewModel.changeArtist(artistList[which])
val navHostController = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
//I get the null pointer exception on the line below
val mainFrag: MainFragment = navHostController.childFragmentManager.findFragmentById(R.id.mainFragment) as MainFragment
mainFrag.notifyThisFragment()
As I understand it, my main activity hosts the navHostFragment which is the parent fragment to my MainFragment. Am I getting something wrong there? Is there a different way I should be doing this? I'm trying to follow the suggested architecture rather than do a bunch of weird work abounds. Am I supposed to only get allSongs from the db one time and filter it in songViewModel? I don't understand why allSongs.observe isn't getting the change.

I ended up solving the problem by making an allArtistSongsWithRatings that I use for my list adapter. I filter the list from allSongsWithRatings to avoid extra db queries. This was unsuccessful at first because I was returning null when I'd try and filter allSongsWithRatings. It turns out I needed to observe allSongsWithRatings BEFORE filtering it because of the way the LiveData works. I observe allSongsWithRatings in MainActivity and initializeArtist.
Main Activity
//initialize viewModel
songViewModel.allSongsWithRatings.observe(this){
songViewModel.initializeWithArtist()
}
Main Fragment
songViewModel.allArtistSongsWithRatings.observe(viewLifecycleOwner) { song ->
// Update the cached copy of the songs in the adapter.
Log.d("LiveDataDebug","Main Fragment Observer called")
//Log.d("LiveDataDebug",song[0].song.songTitle)
song.let { adapter.submitList(it) }
}
SongViewModel
var artistName = "Ear Kitty"
private val _artistNameLive = MutableLiveData<String>(artistName)
val artistNameLive: LiveData<String>
get() = _artistNameLive
private val _allSongsWithRatings : MutableLiveData<List<SongWithRatings>> = repository.allSongsWithRatings.asLiveData() as MutableLiveData<List<SongWithRatings>>
val allSongsWithRatings: LiveData<List<SongWithRatings>>
get() = _allSongsWithRatings
private val _allArtistSongsWithRatings = (artistNameLive.value?.let {
repository.getArtistSongsWithRatings(
it
).asLiveData()
} )as MutableLiveData<List<SongWithRatings>>
val allArtistSongsWithRatings: LiveData<List<SongWithRatings>>
get() = _allArtistSongsWithRatings
fun changeArtist(artist: String){
_artistNameLive.value = artist
artistName = artist
initializeWithArtist()
}
fun initializeWithArtist(){
var newList = mutableListOf<SongWithRatings>()
newList = allSongsWithRatings.value as MutableList<SongWithRatings>
//make sure the list isn't empty
if(newList.isNullOrEmpty()){
//handle empty list error
}else{
//list sorted by performance rating
_allArtistSongsWithRatings.value = newList.filter { it.song.artistName == artistNameLive.value } as MutableList<SongWithRatings>
allArtistSongsWithRatings.value
}
}

Related

Jetpack Compose recompostion of property change in list of objects

I am quite new to Jetpack compose and have an issue that my list is not recomposing when a property of an object in the list changes. In my composable I get a list of available appointments from my view model and it is collected as a state.
// AppointmentsScreen.kt
#Composable
internal fun AppointmentScreen(
navController: NavHostController
) {
val appointmentsViewModel = hiltViewModel<AppointmentViewModel>()
val availableAppointments= appointmentsViewModel.appointmentList.collectAsState()
AppointmentContent(appointments = availableAppointments, navController = navController)
}
In my view model I get the data from a dummy repository which returns a flow.
// AppointmentViewModel.kt
private val _appointmentList = MutableStateFlow(emptyList<Appointment>())
val appointmentList : StateFlow<List<Appointment>> = _appointmentList.asStateFlow()
init {
getAppointmentsFromRepository()
}
// Get the data from the dummy repository
private fun getAppointmentsFromRepository() {
viewModelScope.launch(Dispatchers.IO) {
dummyRepository.getAllAppointments()
.distinctUntilChanged()
.collect { listOfAppointments ->
if (listOfAppointments.isNullOrEmpty()) {
Log.d(TAG, "Init: Empty Appointment List")
} else {
_appointmentList.value = listOfAppointments
}
}
}
}
// dummy function for demonstration, this is called from a UI button
fun setAllStatesToPaused() {
dummyRepository.setSatesInAllObjects(AppointmentState.Finished)
// Get the new data
getAppointmentsFromRepository()
}
Here is the data class for appointments
// Appointment data class
data class Appointment(
val uuid: String,
var state: AppointmentState = AppointmentState.NotStarted,
val title: String,
val timeStart: LocalTime,
val estimatedDuration: Duration? = null,
val timeEnd: LocalTime? = null
)
My question: If a property of one of the appointment objects (in the view models variable appointmentList) changes then there is no recomposition. I guess it is because the objects are still the same and only the properties have changed. What do I have to do that the if one of the properties changes also a recomposition of the screen is fired?
For example if you have realtime app that display stocks/shares with share prices then you will probably also have a list with stock objects and the share price updates every few seconds. The share price is a property of the stock object so this quite a similiar situation.

Item is not updating if an attribute is changed (Jetpack Compose)

I am drawing a list of devices
#Composable
fun DeviceListScreen(){
val model: DeviceListViewModel = hiltViewModel()
val myDevices: List<MyDevice> by model.myDevices.observeAsState(emptyList())
for(device in myDevices)
Device(device)
}
In model I have a livedata
private val items: List<MyDevice> = ArrayList()
private val _myDevices = MutableLiveData<List<MyDevice>> (emptyList())
val myDevices: LiveData<List<MyDevice>> = _myDevices
I change content of an item then update live data
items[0].signal = 54
_myDevices.value = items
However data is not updating in ui.
I guess this is because the pointer to list was not changed and number of items in the list also is not changes and thus compose does not update this data.
Update 12-08-2022
I've just encountered the same problem you had. I have successfully solved it, so the following solution that I have adapted for your problem should also work fine:
fun updateSignal(idOfSelectedDevice: Int) {
_myDevices.value = myDevices.value?.map { device ->
if (device.id == idOfSelectedDevice) device.copy(
signal = <YOUR_DESIRED_VALUE>
) else device
}
}
You could try wrapping items in its own data class:
data class ItemsList(val devices: List<MyDevice> = ArrayList())
Then change items to private val items: ItemsList = ItemsList()
Since you are using your own data class for items, you can then access the copy function which copies an existing object into a new object and should therefore trigger the update of the liveData object:
_myDevices.value = _myDevices.value?.copy(items = items.devices.apply {
this[0].signal = 54
})

Android Paging 3 library: how to change list with new param?

I have a search fragment that shows list of searched items.
if user type something, I pass that string to url as new query parameter and get new list using paging 3 library.
first solution is:
//viewModel
lateinit var postListUrl: String
val postList: Flow<PagingData<Post>> = Pager(PagingConfig(pageSize = 20)) {
PostPagingSource(postRepository, postListUrl)
}.flow.cachedIn(viewModelScope)
//fragment
fun showPostList(url: String) {
postListAdapter.submitData(lifecycle, PagingData.empty())
viewModel.postListUrl = url
viewLifecycleOwner.lifecycleScope.launch {
viewModel.postList.collectLatest {
postListAdapter.submitData(it)
}
}
}
by this solution by changing url (showPostList(newUrl), list remain without any changes. maybe using cached list in viewModel.
another solution is:
using showPostList(initUrl) in onViewCreated of fragment and then using blew method by changing parameter:
//fragment
fun changePostList(url: String) {
viewModel.postListUrl = url
postListAdapter.refresh()
}
this work but if old list and new list have common item, new list show on last common visible item.
for example if 5th position item of old list is same as 7th of new list, then on after list change to show new list, it start from 7th position not first item.
I found another solution here:
//viewModel
val postListUrlFlow = MutableStateFlow("")
val postList = postListUrlFlow.flatMapLatest { query ->
Pager(PagingConfig(pageSize = 20)) {
PostPagingSource(postRepository, query)
}.flow.cachedIn(viewModelScope)
}
//fragment
fun showPostList(url: String) {
postListAdapter.submitData(lifecycle, PagingData.empty())
viewModel.postListUrlFlow.value = url
viewLifecycleOwner.lifecycleScope.launch {
viewModel.postList.collectLatest {
postListAdapter.submitData(it)
}
}
}
but by using this list refresh on back to fragment and sometimes Recyclerview state changing.
class MyViewModel:ViewModel(){
private val _currentQuery = MutableLiveData<String>()
val currentQuery:LiveData<String> = _currentQuery
val users = _currentQuery.switchMap { query->
Pager(PagingConfig(pageSize = 20)) {
PostPagingSource(postRepository, query)
}.livedata.cachedIn(viewModelScope)
}
fun setCurrentQuery(query: String){
_currentQuery.postValue(query)
}
}
By the use of
SwitchMap
you can get new results every time query is chnaged and it will replace the old data .

Android - LiveData doesn't get updated

In my fragment I observe dbQuestionsList field:
viewModel.dbQuestionsList.observe(viewLifecycleOwner, Observer { list ->
Log.i("a", "dbQuestionsList inside fragment = $list ")
})
In my fragment I have few buttons and depending on which one is pressed I call method on viewModel passing the string which was set as tag to the button.
viewModel.onFilterChanged(button.tag as String)
My ViewMode:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("")
}
private fun onFilterChanged(filter: String) {
dbQuestionsList = mRepository.getDbQuestionsForCategory(filter)
}
Repository method:
fun getDbQuestionsForCategory(categoryName: String): LiveData<List<DatabaseQuestion>> {
return database.dbQuestionsDao().getDbQuestionsByCategory(categoryName)
}
Dao method:
#Query("SELECT * FROM db_questions_database WHERE category = :categoryId")
fun getDbQuestionsByCategory(categoryId: String): LiveData<List<DatabaseQuestion>>
When I press button, viewModel method is called with argument which should be used to update LiveData by searching through room database, but NOTHING gets updated for no reason. Database is not empty so there is no reason to return null and not trigger observer in main Fragment.
But when I do this in my viewModel:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("travel")
}
where I hardcode parameter, the room will return list and observer in fragment will be triggered, so it works like that but doesn't work when arguments is passed when button is pressed, Please explain because this thing doesn't make sense. I tried with mutable live data, with using .setValue and .postValue but NOTHING works.
The reason you aren't getting updates is because onFilterChanged() is reassigning dbQuestionsList, not updating it. So the variable you observe initially is never actually modified.
I would probably implement this using a Transformation:
val filter = MutableLiveData<String>().apply { value = "" }
val dbQuestionsList = Transformations.switchMap(filter) {
mRepository.getDbQuestionsForCategory(filter)
}
Then in your fragment just set the filter when your button is clicked:
viewModel.filter.value = button.tag as String
Try this:
dbQuestionsList.value = mRepository.getDbQuestionsForCategory(filter)
or
dbQuestionsList.postValue(mRepository.getDbQuestionsForCategory(filter))

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.

Categories

Resources