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
})
Related
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
}
}
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.
I'm trying to load Products items from a Parse Server database using data source type of PositionalDataSource()to be able to fetch pages of items while scrolling down.
This what i've done so far :
My PositionalDataSource() class
class ParsePositionalDataSource: PositionalDataSource<Product>() {
private fun getQuery() : ParseQuery<Product> {
return ParseQuery.getQuery(Product::class.java).orderByDescending("createdAt")
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Product>) {
val query = getQuery()
query.limit = params.loadSize
query.skip = params.startPosition
val products = query.find()
callback.onResult(products)
}
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Product>) {
val query = getQuery()
query.limit = params.requestedLoadSize
query.skip = params.requestedStartPosition
val count = query.count()
val products = query.find()
callback.onResult(products, params.requestedStartPosition, count)
}
}
And then a data source factory :
class ParseDataSourceFactory : DataSource.Factory<Int, Product>() {
override fun create(): DataSource<Int, Product> {
return ParsePositionalDataSource()
}
}
And finally my ViewModal class
class CategoryTabViewModel() : ViewModel() {
private var pagedListConfig: PagedList.Config = PagedList.Config.Builder().setEnablePlaceholders(true)
.setPrefetchDistance(5)
.setInitialLoadSizeHint(5)
.setPageSize(5).build()
private val sourceFactory = ParseDataSourceFactory()
private val _productList = LivePagedListBuilder(sourceFactory, pagedListConfig).build()
val x = _productList
//val productList: LiveData<PagedList<Product>> = _productList
fun getProductList(): LiveData<PagedList<Product>> {
return _productList
}
}
I've checked if loadinitial() returns data by setting a breakpoint after : val products = query.find()and yes, productscontains a list of Product
But in my ViewModal, _productList it's always null or empty list.
Do I miss something ?
I've found the solution by my self while digging in debugger console output.
I saw a line that says :
E/RecyclerView: No layout manager attached; skipping layout
And it works just fine after i added the layoutmanager in my RecyclerView
<androidx.recyclerview.widget.RecyclerView
...
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
... />
And the documentation says :
A LayoutManager is responsible for measuring and positioning item
views within a RecyclerView as well as determining the policy for when
to recycle item views that are no longer visible to the user. By
changing the LayoutManager a RecyclerView can be used to implement a
standard vertically scrolling list, a uniform grid, staggered grids,
horizontally scrolling collections and more. Several stock layout
managers are provided for general use.
And
When writing a RecyclerView.LayoutManager you almost always want to
use layout positions whereas when writing an RecyclerView.Adapter, you
probably want to use adapter positions.
It seems that RecyclerView.Adapter needs RecyclerView.LayoutManager to be set.
Android Studio 3.6
Snippet:
private fun createHistoryList(participantsEffectResourceList: List<ParticipantsEffectResource>) {
val historyList = mutableListOf<MyHistoryItem>()
participantsEffectResourceList.forEach({
val date = it.operation.appliedAt
val details = it.operation.details
val myHistoryItem = MyHistoryItem(date, details)
historyList.add(myHistoryItem)
})
}
As you can see I extract property from one item type (ParticipantsEffectResource) and transform it to another list of items (MyHistoryItem).
Nice. It's work fine.
But can any another simpler method? E.g. by participantsEffectResourceList.map {} or smt else?
List already provides .map() you can use:
fun createHistoryList(participantsEffectResourceList: List < ParticipantsEffectResource > ) {
val historyList = participantsEffectResourceList.map {
MyHistoryItem(it.operation.appliedAt, it.operation.details)
}
}
hi I'm getting information from web with jsoup and coroutine and I want to show data in recyclerview
All the information is well received but the RecyclerView does not show anything and the view is not updated
fun myCoroutine(): ArrayList<DataModel> {
val listx = arrayListOf<DataModel>()
GlobalScope.launch { // launch new coroutine in background and continue
Log.d("asdasdasd", "start")
var doc: Document = Jsoup.connect("http://5743.zanjan.medu.ir").timeout(0).maxBodySize(0).ignoreHttpErrors(true).sslSocketFactory(setTrustAllCerts()).get()
val table: Elements = doc.select("table[class=\"table table-striped table-hover\"]")
for (myTable in table) {
val rows: Elements = myTable.select("tr")
for (i in 1 until rows.size) {
val row: Element = rows.get(i)
val cols: Elements = row.select("td")
val href: Elements = row.select("a")
val strhref: String = href.attr("href")
listx.add(DataModel(cols.get(2).text(),strhref))
Log.d("asdasf",cols.get(2).text())
}
}
}
return listx
}
private fun getData() {
itemsData = ArrayList()
itemsData = myCoroutine()
adapter.notifyDataSetChanged()
adapter = RVAdapter(itemsData)
}
and this is oncreate
var itemsData = ArrayList<DataModel>()
adapter = RVAdapter(itemsData)
val llm = LinearLayoutManager(this)
itemsrv.setHasFixedSize(true)
itemsrv.layoutManager = llm
getData()
itemsrv.adapter = adapter
This code has numerous bugs (getData, for instance, never sets the adapter onto the RecyclerView), but the biggest issue is that you're not actually waiting for listx to be populated - you're returning it immediately before it's populated. You need to either move the population of the adapter to the coroutine and run that part on the UI thread dispatcher, or use a callback, or dispatch it to the UI thread. Launching a coroutine and returning immediately doesn't make the data get populated when something tries to use it.