Currently I am using Android Architecture Components for App development everything is working along with paging library Now I want to remove recyclerview Item using PagedListAdapter to populate this we required to add a data source and from data source list is updating using LiveData no I want to remove a item from list notifyItemRemoved() is working from PagedList I am getting this exception:
java.lang.UnsupportedOperationException
java.util.AbstractList.remove(AbstractList.java:638)
Since you cannot directly modify the data that you have in the PagedList as I believe its immutable, the key to implementing the removal of items in this situation is maintaining a backing dataset somewhere in your code that represents all the data that you have received so far. An ArrayList worked for me.
When you want to remove an item, remove it from your backing dataset and call invalidate on your data source. This will trigger the loadInitial() call, in which you can pass your backing dataset to the callback.onResult() function. Your PagedListAdapter will use the DiffUtil.ItemCallback you supplied to smartly determine that there has been an update to its data and will update itself accordingly.
This tutorial helped me understand the correct flow to use in order to properly delete an item while using the paging library:
https://medium.com/#FizzyInTheHall/implementing-swipe-to-delete-with-android-architecture-components-a95165b6c9bd
The repo associated with the tutorial can be found here:
https://gitlab.com/adrianhall/notes-app
Temporary hide item in list by call notifyItemUpdated() base on flags set in Pojo object
if(data?.hasVisible == false) {
itemBinding.root.visibility = View.GONE
itemBinding.root.layoutParams = RecyclerView.LayoutParams(0, 0)
}else{
itemBinding.root.visibility = View.VISIBLE
itemBinding.root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.WRAP_CONTENT)
}
I had same issue.
My PagedList displayed items that DataSource factory fetched from server. I came up with two solutions how to remove item from list.
Reload whole list.
Make API call to remove item from server and then call
pagedList.dataSource.invalidate().
Downside of this solution is that whole list is cleared and then all items received from server. Not exactly what I was looking for.
Store results in Room. Then PagedList will get items directly from Room and PagedListAdapter will manage removing/adding items itself.
In DAO object
("SELECT * FROM YourModel")
fun getAll(): DataSource.Factory<Int, YourModel>
#Delete
fun delete(item: YourModel)
To update database as user scrolls list I implemented BoundaryCallback. It is being called when there are no more items to show in DB, it can be called at the end of same page, so I ensured to not execute same request few times (In my case list's key is page number).
class YourModelBoundaryCallback(private val repository: Repository) : PagedList.BoundaryCallback<YourModel>() {
private val requestArray = SparseArray<Disposable>()
private var nextPage = 1
private var lastPage = 1
override fun onZeroItemsLoaded() {
if (requestArray.get(1) == null) {
requestArray.put(1, repository.getFirstPage()
.subscribe({
lastPage = it.total / PAGE_SIZE + if (it.total % PAGE_SIZE == 0) 0 else 1
nextPage++
}))
}
}
override fun onItemAtEndLoaded(itemAtEnd: YourModel) {
if (nextPage > lastPage) {
return
}
if (requestArray.get(nextPage) == null) {
requestArray.put(nextPage, repository.getNextPage(nextPage)
.subscribe({
lastPage = it.total / PAGE_SIZE + if (it.total % PAGE_SIZE == 0) 0 else 1
nextPage++
}))
}
}
override fun onItemAtFrontLoaded(itemAtFront: YourModel) {
// ignored, since we only ever append to what's in the DB
}
}
PagedList instance became this
private val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPrefetchDistance(3)
.setPageSize(PAGE_SIZE)
.build()
val pagedList = LivePagedListBuilder(yourModelDao.getAll(), pagedListConfig)
.setBoundaryCallback(YourModelBoundaryCallback(repository))
.build()
And finally to remove item from adapter I just call yourModelDao.remove(yourModel)
Adapter.notifyItemRemoved(position);
appDatabase.userDao().deleteUser(user);
It's work for me!
Related
I have a functioning RecyclerView which works fine with the given data I provide via ListAdapter as shown below. What I now want is to add additional data to my list items.
class IngredientAdapter(
private val ingredientClickListener: IngredientClickListener
) : ListAdapter<Ingredient, RecyclerView.ViewHolder>(EventDiffCallback()) {
private val adapterScope = CoroutineScope(Dispatchers.Default)
fun submitIngredientsList(list: List<Ingredient>?) {
adapterScope.launch {
withContext(Dispatchers.Main) {
submitList(list)
}
}
}
I have no idea how to do that properly or if RecyclerViews are even capable of doing this. The only way I am able to see is merging both data classes (Ingredient plus the new one) together and submit them as list together to the adapter but this seems messy and I am looking for a better way.
So my question is: How to feed data into my list items without merging it together with the data I already have? Is RecyclerView the wrong choice in my case?
Thanks in advance!
Ok I found a solution: I submitted the additional data list just how like the other one but did not attach it directly to the ListAdapter since this is not possible.
In the function onBindViewHolder after getting the item for the current position I use this information to retrieve the correct element from the new data list. Then I attach the data to the view by calling using the viewholders view binding
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val resources = holder.itemView.context.resources
val ingredientItem = getItem(position)
holder.bind(ingredientClickListener, ingredientItem)
val groceryInStock: GroceryInStock? = availableGroceries.firstOrNull{
ingredientItem.grocery.groceryId == it.grocery.groceryId
}
holder.binding.listItemAvailableAmount.text = groceryInStock.amount
}
}
}
Since the data I add fully depends on the already existing item being displayed I did not make any changes to the functions areItemsTheSame and areContentsTheSame in my overriden DiffUtil.ItemCallback class.
Help me please.
The app is just for receiving list of plants from https://trefle.io and showing it in RecyclerView.
I am using Paging library 3.0 here.
Task: I want to add a header where total amount of plants will be displayed.
The problem: I just cannot find a way to pass the value of total items to header.
Data model:
data class PlantsResponseObject(
#SerializedName("data")
val data: List<PlantModel>?,
#SerializedName("meta")
val meta: Meta?
) {
data class Meta(
#SerializedName("total")
val total: Int? // 415648
)
}
data class PlantModel(
#SerializedName("author")
val author: String?,
#SerializedName("genus_id")
val genusId: Int?,
#SerializedName("id")
val id: Int?)
DataSource class:
class PlantsDataSource(
private val plantsApi: PlantsAPI,
private var filters: String? = null,
private var isVegetable: Boolean? = false
) : RxPagingSource<Int, PlantView>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, PlantView>> {
val nextPageNumber = params.key ?: 1
return plantsApi.getPlants( //API call for plants
nextPageNumber, //different filters, does not matter
filters,
isVegetable)
.subscribeOn(Schedulers.io())
.map<LoadResult<Int, PlantView>> {
val total = it.meta?.total ?: 0 // Here I have an access to the total count
//of items, but where to pass it?
LoadResult.Page(
data = it.data!! //Here I can pass only plant items data
.map { PlantView.PlantItemView(it) },
prevKey = null,
nextKey = nextPageNumber.plus(1)
)
}
.onErrorReturn{
LoadResult.Error(it)
}
}
override fun invalidate() {
super.invalidate()
}
}
LoadResult.Page accepts nothing but list of plant themselves. And all classes above DataSource(Repo, ViewModel, Activity) has no access to response object.
Question: How to pass total count of items to the list header?
I will appreciate any help.
You can change the PagingData type to Pair<PlantView,Int> (or any other structure) to add whatever information you need.
Then you will be able to send total with pages doing something similar to:
LoadResult.Page(
data = it.data.map { Pair(PlantView.PlantItemView(it), total) },
prevKey = null,
nextKey = nextPageNumber.plus(1)
)
And in your ModelView do whatever, for example map it again to PlantItemView, but using the second field to update your header.
It's true that it's not very elegant because you are sending it in all items, but it's better than other suggested solutions.
Faced the same dilemma when trying to use Paging for the first time and it does not provide a way to obtain count despite it doing a count for the purpose of the paging ( i.e. the Paging library first checks with a COUNT(*) to see if there are more or less items than the stipulated PagingConfig value(s) before conducting the rest of the query, it could perfectly return the total number of results it found ).
The only way at the moment to achieve this is to run two queries in parallel: one for your items ( as you already have ) and another just to count how many results it finds using the same query params as the previous one, but for COUNT(*) only.
There is no need to return the later as a PagingDataSource<LivedData<Integer>> since it would add a lot of boilerplate unnecessarily. Simply return it as a normal LivedData<Integer> so that it will always be updating itself whenever the list results change, otherwise it can run into the issue of the list size changing and that value not updating after the first time it loads if you return a plain Integer.
After you have both of them set then add them to your RecyclerView adapter using a ConcatAdapter with the order of the previously mentioned adapters in the same order you'd want them to be displayed in the list.
ex: If you want the count to show at the beginning/top of the list then set up the ConcatAdapter with the count adapter first and the list items adapter after.
One way is to use MutableLiveData and then observe it. For example
val countPlants = MutableLiveData<Int>(0)
override fun loadSingle(..... {
countPlants.postValue(it.meta?.total ?: 0)
}
Then somewhere where your recyclerview is.
pagingDataSource.countPlants.observe(viewLifecycleOwner) { count ->
//update your view with the count value
}
The withHeader functions in Paging just return a ConcatAdapter given a LoadStateHeader, which has some code to listen and update based on adapter's LoadState.
You should be able to do something very similar by implementing your own ItemCountAdapter, except instead of listening to LoadState changes, it listens to adapter.itemCount. You'll need to build a flow / listener to decide when to send updates, but you can simply map loadState changes to itemCount.
See here for LoadStateAdapter code, which you can basically copy, and change loadState to itemCount: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:paging/runtime/src/main/java/androidx/paging/LoadStateAdapter.kt?q=loadstateadapter
e.g.,
abstract class ItemCountAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
var itemCount: Int = 0
set(itemCount { ... }
open fun displayItemCountAsItem(itemCount: Int): Boolean {
return true
}
...
Then to actually create the ConcatAdapter, you want something similar to: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt;l=236?q=withLoadStateHeader&sq=
fun PagingDataAdapter.withItemCountHeader(itemCountAdapter): ConcatAdapter {
addLoadStateListener {
itemCountAdapter.itemCount = itemCount
}
return ConcatAdapter(itemCountAdapter, this)
}
Another solution, although also not very elegant, would be to add the total amount to your data model PlantView.
PlantView(…val totalAmount: Int…)
Then in your viewmodel you could add a header with the information of one item. Here is a little modified code taken from the official paging documenation
pager.flow.map { pagingData: PagingData<PlantView> ->
// Map outer stream, so you can perform transformations on
// each paging generation.
pagingData
.map { plantView ->
// Convert items in stream to UiModel.PlantView.
UiModel.PlantView(plantView)
}
.insertSeparators<UiModel.PlantView, UiModel> { before, after ->
when {
//total amount is used from the next PlantView
before == null -> UiModel.SeparatorModel("HEADER", after?.totalAmount)
// Return null to avoid adding a separator between two items.
else -> null
}
}
}
A drawback is the fact that the total amount is in every PlantView and it's always the same and therefore redundant.
For now, I found this comment usefull: https://issuetracker.google.com/issues/175338415#comment5
There people discuss the ways to provide metadata state to Pager
A simple way I found to fix it is by using a lambda in the PagingSource constructor. Try the following:
class PlantsDataSource(
// ...
private val getTotalItems: (Int) -> Unit
) : RxPagingSource<Int, PlantView>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, PlantView>> {
...
.map<LoadResult<Int, PlantView>> {
val total = it.meta?.total ?: 0
getTotalItems(total)
...
}
...
}
}
I am new to the paging library 3.0, I need to paginate data only from the local database.
Initially, I fetched PagingSource<Int, MessageDisplayModel> from the database and use it as flow.
fun getChatMessages(chatWrapper: ChatWrapper): Flow<PagingData<MessageDisplayModel>> {
return Pager(
config = PagingConfig(pageSize = 15,
maxSize = 50,
enablePlaceholders = true)
) {
xmppChatManager.getChatMessages(chatWrapper)
}.flow
}
Then after PagingData is passing to the adapter through submitData() method.
lifecycleScope.launch {
#OptIn(ExperimentalCoroutinesApi::class)
mViewModel.getChatMessages(chatWrapper).collectLatest {
adapter.submitData(it) }
}
Now, my concern is how can we get the actual list of MessageDisplayModel from PagingData which I pass to the adapter?
You can use adapter.snapshot().items. More info can be found here.
as of paging 3 need constructor for insitating entire list of item to load once in your layout for binding if get list of pages by using snapshot() you cant update holder itemcount it's run before bindviewholder and DiffUtil also has high order lifecycle
try to use custom constructor
I am learning how to use ROOM as part of Android Architecture Components. For a regular recyclerviews, I am able to implement onSwipe to delete a post on the list easily and my ViewModel will directly calls my repository to call ROOM to delete the given post.
However, I ran into some issues when I tried to also implement the onMove callbacks. the callback listed as below:
public boolean onMove(#NonNull RecyclerView recyclerView,
#NonNull RecyclerView.ViewHolder viewHolder,
#NonNull RecyclerView.ViewHolder target)
from here i can get the before and after positions, my naive thinking is that I should be able to swap these two post in my Repository and my LiveData will update the list automatically.
But I don't know how to do the operation (swap) on DAO and ROOM parts. Can you please share your idea or correct me if I am heading to the wrong direction?
In your repository you will find something like :
var allRecords : LiveData<List<Record>> = fragmentDao.getAllRecordsOrderedByIDDesc()
This ensures that the feed into Recycler display (i.e. LiveData) will be current with any inserts, updates etc, to the database.
In the DAO you'll see something like:
#Query ("SELECT * from record_table ORDER BY id DESC" )
fun getAllRecordssOrderedByIDDesc(): LiveData<List<Record>>
Since you cannot modify id after drag and drop, you'll need a new database column to record position. Let's call it 'position'. Your LiveData would now be:
var allRecords : LiveData<List<Record>> = fragmentDao.getAllRecordsOrderedByPositionDesc()
based on:
#Query ("SELECT * from record_table ORDER BY position DESC" )
fun getAllRecordssOrderedByPositionDesc(): LiveData<List<Record>>
Once a new position is recorded in the database, it is persistent i.e. you can stop the app, restart it, and the record will still appear in the new position (LiveData is using positions read from the database, as described above).
Another approach is to use preferences to record drag and drop changes. After the database is loaded these are then applied. There are pro and cons. Updating a position in a database might be done with a database update, called after recyclerView.adapter.notifyItemMoved(from, to) (in override fun onMove), in override fun clearView.
You'll need code to determine new positions. It is more complex than 'item at position 3 goes to position 5).For example, dragging item at adapter position 3 to position 10 implies the following changes:
3 -> null
4 -> 3
5 -> 4
...
9 -> 8
10 -> 9
and finally 3 -> 10
The java.util, Collections.swap can be used:
from = viewHolder.adapterPosition
to = target.adapterPosition
Collections.swap(mutableList, from, to)
in override fun onMove(...) You can then use the mutableList to drive the database position update.
For drag and swipe in recyclerview follow this url
However ,If you want to persist the new order of list .
maintain the position in table like 1,2,3....
fetch the list using Room query - sort the items using position
user drag and drop ,update the new positions in database
to get positions => viewholder.getAdapterPosition() ,target.getAdapterPosition()
Livedata will be fired when there is change in database ,you will get new list from database.
About the sorting problem.
When drag and dropping the item, each of every data in entity need to be in the right position, I think I have the perfect solution.
Btw, my answer is based on #NY-Newboid, which by adding a new column section just to locate the position of each of the data in the entity.
And FYI, I am using Kotlin.
When item is dragged....
When the item is dragged, like in this picture, the fun onMove will be called twice. On each of the call, only 2 of the item will be affected.
So we can just use #update fun update(item1 : Entity,item2 : Entity) on each of the onMove call, to switch between the position of 2 of the item. And the data in the entity will rearrange one by one like climbing up a stair. Eventually will stop doing this when the user dropped the item.
This is my code on Note database project,
In Note Data Class
#Entity(tableName = "note_table")data class Note(
#PrimaryKey(autoGenerate = true)
var id: Int,
val title: String,
val description: String,
val position: Int
)
In DAO
#Update
suspend fun update(note1: Note, note2: Note)
In ActivityMain
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPosition = viewHolder.adapterPosition //started position
val toPosition = target.adapterPosition //ended position
val oriNote1 = noteAdapter.getNote(fromPosition) //note that are dragged
val oriNote2 = noteAdapter.getNote(toPosition) //note that are affected
val newNote1 = Note(oriNote1.id,oriNote1.title,oriNote1.description,toPosition)
val newNote2 = Note(oriNote2.id,oriNote2.title,oriNote2.description,fromPosition)
noteAdapter.isItemMoved() //setting up boiler code to disable notifyDataSetChanged()
noteViewModel.update(newNote1,newNote2)
noteAdapter.notifyItemMoved(fromPosition,toPosition)
return true
}
In RecyclerViewAdapter Class,
private var notes: List<Note> = ArrayList()
private var isItemMoved: Boolean = false
fun setNote(notes: List<Note>) {
this.notes = notes
println(isItemMoved)
if (!isItemMoved) {
notifyDataSetChanged()
}
isItemMoved = false
}
fun isItemMoved(){
println("set it to true")
isItemMoved = true
}
Hope this helps
I'm trying to use the new Paging Library and Room as database but I'm facing a problem, the PagedList returned by the database is not supposed to be the same list sent to the UI, I map some entities before showing it to the user and during this map operation I change the list size (add items), apparently the Paging Library doesn't support this kind of operation because when I try to run the app I get this exception:
Caused by: java.lang.IllegalStateException: Invalid Function 'function_name' changed return size. This is not supported.
Looking at the paging library source code you see this method:
static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) {
List<B> dest = function.apply(source);
if (dest.size() != source.size()) {
throw new IllegalStateException("Invalid Function " + function
+ " changed return size. This is not supported.");
}
return dest;
}
Is there a workaround or something for dealing with when you add dynamic items to the PagedList before using it?
This is what I'm doing
DAO
#Query("SELECT * FROM table_name")
fun getItems(): DataSource.Factory<Int, Item>
LocalSource
fun getItems(): DataSource.Factory<Int, Item> {
return database.dao().getItems()
.mapByPage { map(it) } // This map operation changes the list size
}
I face same problem and still looking for better solution.
In my case, I have to display 1 section before each users load from API and here is my workaround.
class UsersViewModel : ViewModel() {
var items: LiveData<PagedList<RecyclerItem>>
init {
...
items = LivePagedListBuilder<Long, RecyclerItem>(
sourceFactory.mapByPage { it -> mapUsersToRecyclerItem(it) }, config).build()
}
private fun mapUsersToRecyclerItem(users: MutableList<User>): List<RecyclerItem> {
val numberOfSection = 1
for (i in 0 until numberOfSection) {
users.add(0, User()) // workaround, add empty user here
}
val newList = arrayListOf<RecyclerItem>()
newList.add(SectionItem())
for (i in numberOfSection until users.size) {
val user = users[i]
newList.add(UserItem(user.login, user.avatarUrl))
}
return newList
}
}
My current user class
data class User(
#SerializedName("login")
val login: String,
#SerializedName("id")
val id: Long = 0,
#SerializedName("avatar_url")
val avatarUrl: String
) {
constructor() : this("", 0, "")
}
Of course, to display Section, I will have another way without add it to RecyclerView data list (like using position only) but in my case user can remove item from the list so using position may hard to handle
Actually, I rollbacked to use old load more way (using EndlessRecyclerViewScrollListener) but hope it help
I think I've found a solution..
Although it's a workaround it worked for me.
In my case, I was trying to create an alphabet sectioned list for a names like this:
**A - HeaderItem**
Aaron - Item
Anny - Item
**B - HeaderItem**
Bob - Item
Bil
**C - HeaderItem**
....
The items in ROOM are only the names of course, when I am trying to map the paged items and add the sections headers it change the list size and I am getting the same error.
What I did is, the HeaderItem object wraps an Item like this:
First, all Items are implementing the ListItem interface
interface ListItem{
const val HEADER = 0
const val ITEM = 1
fun getItemType() : Int
}
Then the header items looks like this
class HeaderItem(val headerTitle : String, val item : Item) : ListItem {
#override
fun getItemType() : Int {
return ListItem.HEADER
}
}
Then when I map the items, when adding a HeaderItem, it takes an Item in it, this way the mapped PagedList size doesn't change. Now I am not getting this exception.
However,
This creates some additional work as I had to set the HeaderItem decoration explicitly and also in the adapter, when bind the header item I had to take care the internal Item + all its logic such as click listeners etc'.
I will be happy if there will be support for changes in the list size out of the box.