I am using Android API Level 28 with android components.
I have an app that displays our data visually in a chart. I'd like to background load the data and begin charting it as it comes in. I also would like you to be able to view the data as a list.
Ideally, the data for the chart and the list would be the same object instances. After all, the network call to retrieve the data is expensive. Plus, depending on the filters selected, there could be a lot of data to be charted.
I cannot use a server-based chart building API; I must draw graphics from the data on the device.
I am able to download the data in the background and build the chart. I looked into PagedList et al for the list view and that works terrifically.
What I cannot figure out is how to share the data. In other words, if you view the chart first then the list, the list should not trigger a download again since the data is already there. Likewise if the user scrolls through the list then looks at the chart.
This is especially helpful for tablets where one side displays the list and the other is the chart (eg, tapping on an item in the list highlights the data point in the chart).
Any pointers would be greatly appreciated.
What I tend to do is to build a class that handles the downloading of the data, then hand it out to whoever wants it
like
class BananaProvider {
private var _bananaData: List<String>? = null
val bananas: List<String>
get() {
_bananaData?.let{ return _bananaData!!}
_bananaData = getBananaData()
return _bananaData!!
}
fun getBananaData(){
(...)
}
}
and perhaps have a function for refilling your babana's with fresh data if you want.
Then, just drop an instance of the BananaProvider in every fragment/function/whatever that needs it.
Alternatively, if you want to share the data between activities, you could serialize it and send it in an intent, or drop it in an SQLite database if you want to save it locally for later use.
What I wound up doing was:
Created a DataSource with the functions to fetch the data from the server. I'm using a PositionalDataSource.
Created a DataSourceFactory to deliver my DataSource
Created a LiveData using LivePagedListBuilder with my factory and a PagedList.Config (to bring in 100 records at a time, which seems optimal for my needs)
It's the LiveData that I share between the RecyclerView and the chart.
The object which controls the download for the chart keeps track of the current download position and size. If the chart winds up creating the shared LiveData, this will trigger the initial download and populate the underlying PagedList that LiveData is observing.
Each time data comes down, I update the download position and size and draw points for the data just received. If the data isn't complete, I found that I have to wait a few milliseconds before triggering the next download using the PagedList's loadAround function.
If the RecyclerView is the thing which is opened first by the user, its PagedListAdapter automatically brings the data down through the same DataSource and it fills the shared LiveData as the user scrolls.
When I switch back to the Chart, since the LiveData is already populated (all or partially), it can draw the points. If the data isn't complete yet, the PagedList will attempt to load the rest, drawing the points of the data coming down.
It's a pretty neat method. The key was keeping the LiveData around and reusing it and figuring out how to "manually" populate it when not using RecyclerView.
Related
Working on a project where we need to have a list of items in a view model. The type is a custom data class.
Until now, I have used a MutableLiveData to store the state of the list. The list will be updated when a user scans an item on an RFID reader that we have connected to the device. When scanned, an item will be fetched from a server and the list will be updated accordingly.
I want to try and move on to using a StateFlow instead, but I'm running into an issue here.
When a new item is scanned, I'm updating the mutable list with the add command and later on updating the corresponding item in the list. This does not trigger the LiveData or StateFlow observers/collectors - but with LiveData I could assign the list to itself after I'm done with updating it. (_listOfItems.value = _listOfItems.value) as this would notify observers.
This does not work with StateFlow however, since it only trigger when the reference changes (which it doesn't since it's the same list, just with new/changed items in it).
I'm aware that there are probably some issues with using a mutable list and coroutines to update the same list, since it might collide if trying to update the list at the same time, or something like that.
So, generally the question is: what's the best approach when having a list in a view model and having the UI update whenever the content of the list changes?
It would be impossible to use a MutableList in a StateFlow and make it update because StateFlow always does an equals comparison with the previous value, and since the previous value is an instance of the same list, they will always be considered equal.
You could make this work with SharedFlow, which does not compare values with the previous.
It is however very error prone to use mutable classes with a SharedFlow/StateFlow (or even LiveData) in general because the data could be getting read from and written to from multiple different places. Very hard to manage.
One solution is to create a copy of the list every time you send it to the flow:
myMutableStateFlow.value = myMutableList.toList()
If you want code that's even easier to manage, possibly at the expense of performance, don't use mutable Lists at all. Use read-only Lists. You could put it in a var property, or just modify the value of your StateFlow directly.
myMutableStateFlow.value += someNewItems
// where myMutableStateFlow's type is a List, not MutableList
I'm trying to implement paging on Android using a DataSource implementation, plus PagedAdapter.
Initially, the requirements were to have a full list in memory (not using RoomDB) and I wanted to take a moving "view" over that data as the user scrolls - i.e. feeding it to the adapter in pages. I've accomplished that with the use of PositionalDataSource.
However, now I have a new requirement. Some of the items in the original full list are actually "loading" items (i.e. spinners) and I need to fetch the data that these cells represent in chunks. These chunks have undetermined sizes. When a chunk loads in, the "loading" item should move down the list, and the loaded chunk be inserted where the "loading" item used to be. This should continue until all chunks that the "loading" item represents have been loaded in, at which point the "loading" item at the end of the list should be removed.
This means, my underlying data source actually grows dynamically as the user scrolls through the list. Which I think means PositionalDataSource is not the right type of data source to use as the source docs state:
* Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
* arbitrary page positions.
Emphasis on fixed-size and countable - obviously my data set is not fixed size (and is therefore also uncountable).
I've looked at other implementations of DataSource and think I've found the right one; ItemKeyedDataSource. Each of my items do indeed have a unique key, and the source docs of this class state that:
* Incremental data loader for paging keyed content, where loaded content uses previously loaded
* items as input to future loads.
Which to me indicates that I can use it for my required purposes. I.e. when it needs to load in a range for an item with the given key which also happens to be a "loading" item, it can use the loading items data to determine what to load.
However, I'm struggling a bit with actual implementation of this, as the official docs don't give any real example usage, and the links to example code assumes the use of RoomDB or retrofit, neither of which is the approach I need.
Could anyone help with giving me an overview of how this DataSource is supposed to function conceptually and/or in code examples using an in memory data set that needs to grow dynamically?
I realize this is pretty vague, I've only started working with this class this morning and I'm struggling.
Paging already loads paginated data for you - the point of implementing a DataSource is to provide Paging a way to incrementally load more data as user scrolls near the end. The one caveat is that in Paging 2.x, load state is not built into the library so you need to track it yourself and show the spinner using some method such as ConcatAdapter.
If you want to try the v3 apis (still in beta), LoadState is a built-in concept and you can simply use the .withLoadStateFooter() transform to turn a PagingDataAdapter to a ConcatAdapter which automatically shows the loading spinner when Paging fetches a new page.
To clarify the bit on the docs about counted snapshots - Paging operates with a single source of truth (DataSource / PagingSource), which is supposed to represent a static list (once fully loaded). This doesn't mean you have to have the whole list in memory, but the items each instance of DataSource fetches should generally match the mental model of a static list. For example if you are paging in data from DB, then a single instance of DataSource / PagingSource is only valid while there are no changes in the DB. Once you insert / modify / delete a row, that instance is no longer valid, which is where DataSource.Factory comes into play, giving you a new PagedList / DataSource pair.
Now if you need to also incrementally update the backing dataset (DB in this example) via layered source approach, Paging v2 offers a BoundaryCallback you can register to fire off network fetch when Paging runs out of data to load, or alternatively in v3 the new API for this is RemoteMediator (still experimental).
I have a list with pagination which I implemented using Paging library. Items on this list can be modified (changed/deleted).
According to official documentation, I'm first changing in-memory list cache from which my DataSource gets pages and after that calling datasource.invalidate() in order to create new pair PagedList/DataSource:
If you have more granular update signals, such as a network API signaling an update to a single item in the list, it's recommended to load data from network into memory. Then present that data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the snapshot can be created.
It works and looks WELL if user modifies items on first page.
However, if user is on page two or further during datasource.invalidate() he will be thrown at the end of the first page.
Debugging shows this happens because new PagedList has only first page when it's submitted to PagedListAdapter.submitList. Adapter compares old and new lists and removes all items not from first page. It happens always but not visible for user if he is on the first page.
So to me, it looks like new pair PagedList/DataSource have no idea about number of pages which fetched previous pair and datasource.invalidate() doesn't fit for the situation in docs. Behavior that I see acceptable for cases then user updates all list (like swipe-to-refresh) but not
an update to a single item in the list
Has anybody faced such issue or somehow archived things I want? Maybe I'm missing some trick which helps me to get new PagedList already with all pages.
For clarification: library version 2.1.0. Custom PageKeyedDataSource based on in-memory cache and remote servise (No Room)
I want to share my research in case anybody is interested:
Issue ("lack of feature") is known, at least I've found the couple related discussions on official tracker one two
If you are using PositionalDataSource or ItemKeyedDataSource you should dig into the direction of requestedStartPosition/requestedInitialKey from initial params as this answer says. I didn't have much time to build the whole solution but those params are indeed different for initial load after invalidation
About my case : PageKeyedDataSource. Here you can read that there is no similar to requestedInitialKey params in this type of data source. Still, I found a solution which fits me, very simple, although, feels like a dirty trick:
When loadInitial() is called after invalidate() in-memory cache returns all already loaded pages instead of just first one.
At first I was worry that something will break if, for example, requestedLoadSize is 5 but the result is 50 items list but turns out it's just a hint and it can be ignored. Just don't forget to pass nextPageKey which corresponds to the last cached page and not the first one.
Hope it will help
With observable method you will only get first page list items....if you want to edit other items you can get that list by adapter.currentlist method.
Example:
private fun list():MutableList<String>{
val list = mutableListOf<String>()
for (value in videosAdapter.currentList.orEmpty()) {
val abc = value.snippet.resourceId.videoId
list.add(abc)
}
return list
}
According to a response made by Yigit Boyar from Google, Live Data is not the best use case for a chat app, because it may lose displaying some items if they come at the same time. He recommends using the new Google's Paging Library. I've been using ItemKeyedDataSource for my inbox(all the people that have message the user), and the inside chat(the messages themselves). The problems are the following:
1- From the chat, when the user scrolls downwards, the user retrieves old messages, which means that the insertion of those messages should be in position 0 of the adapter, not sequentially like the paging library does. How can I alternate the position of the inserted items to be sequentially for new messages, and in position 0 for old messages?
2- From the inbox(people that have message the user), again I'm using ItemKeyedDataSource here, the problem is that I want to maintain the multiple document listener from the repository (I'm using Firebase Firestore), so I can detect every time a new person talks to the user. The problem is that callback.onResult is called only once, and fails when Firebase sends another user. How can I maintain an update-able list?
I understand that this answer is probably too late, but maybe it can help someone in future.
Position of item in RecyclerView is determined by the position of corresponding data object (of type T) inside PagedList<T>. PagedList is designed to look alike good old List<T>, but can be thought of as an "endless" list of elements.
PagedList gets its elements by pages on demand through something called DataSource.Factory. A Factory is used because DataSource by itself can only grow in one direction. If you need to prepend elements in PagedList, or change or remove existing elements you must invalidate the DataSource and a new instance will be created through DataSource.Factory.
So, to insert your data elements where you want them you should implement your own DataSource and DataSource.Factory by subclassing these base classes.
Note: Room, data persistence library from AndroidX, provides facilities to automatically generate instances of these classes for your data. You can write SQL query like this:
SELECT * FROM messages WHERE threadId=:threadId ORDER BY timestamp DESC
then get DataSource.Factory from this, use the factory to create LivaData<PagedList<Message>> and finally use the paged list to display messages in a RecyclerView in a chat application. Then, when you insert, update or remove data inside DB these changes will automatically propagate to the UI. This can be very useful.
I recommend you to read a few related examples a do codelabs:
https://codelabs.developers.google.com/codelabs/android-paging/#0
https://github.com/googlesamples/android-architecture-components/tree/master/PagingSample
https://github.com/googlesamples/android-architecture-components/tree/master/PagingWithNetworkSample
I have a project that loads a list from the server. This data will eventually be stored into a database, but for now is stored in memory in a MutableLiveData. A RecyclerView's adapter is watching the data and displaying it. So far everything is working as expected, using a FAB the user can post a new entry which will go at the top of the list, on success I get a 200 and here's the main part where I'm getting lost...
When I want to add a single item to a list stored in a LiveData, the observer is unaware of the delta. I currently make a call to RecyclerView.Adapter.notifyDataSetChanged(), though the ideal in my case would be to call notifyItemInserted(0) or in other cases I can see various other notifications. What the best way to do this? The lifecycle architecture library appears to be very well thought of, I assume I'm missing something simple. I can't imagine having to manually perform a diff between the lists?