I have a RecyclerView of images that the user is able to edit directly in the RecyclerView itself.
The editing part is fine and works well. It's done on a bitmap that overlays the images and then saves any changes to the image file. As soon as the user scrolls the recyclerview, the bitmap is destroyed and becomes invisible.
The trouble is, any changes the user makes aren't visible when they scroll. They scroll the OLD image, not the EDITED image. They have to get out of the recycler and back into it to see the changes.
So how do I force the RecyclerView to reload the image that was just saved. I'm using Glide in my adapter, and as you can see I have the caching based on the save time of the image.
class InfinityPageAdapter(val memo: Memo) : ListAdapter<Page, InfinityPageAdapter.ViewHolder>(PageDiffCallback()) {
class ViewHolder(pageView: View) : RecyclerView.ViewHolder(pageView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InfinityPageAdapter.ViewHolder {
val pageView = LayoutInflater.from(parent.context).inflate(R.layout.infinity_page_list_item, parent, false)
return ViewHolder(pageView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val page = getItem(position)
val imageUrl = getMemoPath(memo.uuid) + page.uuid + ".webp"
if (imageUrl.isNotEmpty()) {
Glide.with(holder.itemView.context)
.load(imageUrl)
.apply(RequestOptions()
.signature(ObjectKey(System.currentTimeMillis()))
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.override(memo.pageWidth,memo.pageHeight)
)
.transition(DrawableTransitionOptions.withCrossFade())
.into(holder.itemView.findViewById(R.id.memo_page_image))
}
}
Is there a way to notify the recyclerview that the image has changed, and force a reload of the image?
I'm using DiffUtil on the adapter. My understanding is, I don't need to use notifyDataSetChanged() when you're using DiffUtil. But I don't understand how you notify of an image change.
I don't know whether the Diff Callback is the issue. For what it's worth, here it is. It doesn't look at the image file itself, just the name of the image file. It might be that, because the name isn't changing, the image doesn't update. But I don't want to be changing the name of the file with every edit.
class PageDiffCallback : DiffUtil.ItemCallback<Page>() {
override fun areItemsTheSame(oldItem: Page, newItem: Page): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Page, newItem: Page): Boolean {
return oldItem == newItem
}
}
thanks!
John
This thing is, you are not updating an entire element in your recycler view, this is, a row. The notifyDataSetChanged() will force the data update like you said, but in this case it will also update your layout.
With DiffUtil, besides contents you are also comparing if the items are the same, which they are, since you are comparing their ID, and that isn't changing when you update your image.
Before trying with notifyDataSetChanged() tho, could you try to do this in your areContentsTheSame()method:
override fun areContentsTheSame(oldItem: Page, newItem: Page): Boolean {
return oldItem.id == newItem.id && oldItem.image == newItem.image // If you want just add more validations here
}
Related
Hey Guys
I'm working on androidTV application using leanback library.
I should show list of categories that each category has it's own list of contents. For this approach leanback offered RowsSupportFragment that you can show this type of UI inside that.
Here I am using Room + LiveData + Retrofit + Glide to perform and implement the screen, but the issue is here, the api will not pass content cover images directly, so developer should download each content cover image, store it and then show covert over the content.
Every thing is working but at the first time, If there is no cover image for content, I will download the cover and store it, but content will not be triggered to get and show image. Using notifyItemRangeChanged and methods like this will blink and reset the list row so this is not a good solution.
This is my diffUtils that I'm using, one for category list, one for each contents list.
private val diffCallback = object : DiffCallback<CardListRow>() {
override fun areItemsTheSame(oldItem: CardListRow, newItem: CardListRow): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: CardListRow, newItem: CardListRow): Boolean {
return oldItem.cardRow.contents?.size == newItem.cardRow.contents?.size
}
}
private val contentDiffCallback = object : DiffCallback<ContentModel>() {
override fun areItemsTheSame(oldItem: ContentModel, newItem: ContentModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ContentModel, newItem: ContentModel): Boolean {
return oldItem.hashCode() == newItem.hashCode()
}
}
As I said, for storage I'm using room, retrieving data as LiveData and observing them in my fragment and so on. I have not posted all the codes for summarization.
If you have any idea or similar source code, I would appreciate it. Thanks
Edit: Fri Dec 2 --- add some more details
This is my live-data observer that holds and observe main list on categories and datas
private fun initViewModel() {
categoriesViewModel.getCategoriesWithContent().observe(viewLifecycleOwner) { result ->
val categoryModelList = MergedContentMapper().toCategoryModelList(result)
initData(categoryModelList)
}
}
And this is the row creation scenario using ArrayObjectAdapter
private fun initData(categoryModelList: List<CategoryModel>) {
showLoading(false)
createRows(categoryModelList)
}
private fun createRows(categoryModelList: List<CategoryModel>) {
val rowItemsList: MutableList<CardListRow> = arrayListOf()
// create adapter for the whole fragment. It displays Rows.
categoryModelList.forEach { categoryModel ->
// create adapter for each row that can display CardView using CardPresenter
val cardListRow = createCardRow(categoryModel)
// add card list rows into list
rowItemsList.add(cardListRow)
}
// set item with diff util
rowsAdapter.setItems(rowItemsList, diffCallback)
}
private fun createCardRow(categoryModel: CategoryModel): CardListRow {
val contentList = categoryModel.contents ?: emptyList()
val cardListRowsAdapter = ArrayObjectAdapter(CardPresenterSelector(context, this))
cardListRowsAdapter.setItems(contentList, contentDiffCallback)
val headerItem = HeaderItem(categoryModel.title)
return CardListRow(headerItem, cardListRowsAdapter, categoryModel)
}
Your code looks correct, but it's missing the part where you tell the Presenter what changed on your items so it can change only that piece of data and doesn't need to re-bind the entire content avoiding the blink.
After your DiffCallback detects that the items are the same but the content has changed it will call its getChangePayload() function to gather details about the changes to pass them to the Presenter. To achieve that you need to do the following changes:
First, you need to override the DiffCallback.getChangePayload() function to something like this:
override fun getChangePayload(oldItem: ListRow, newItem: ListRow): Any {
return when {
oldItem.headerItem.name != newItem.headerItem.name -> "change_title"
else -> "change_items"
}
}
With that your ListRowPresenter will receive the information of what changed in the ListRowPresenter.onBindViewHolder() overload that receives a payload list (returned by your DiffCallback) like so:
override fun onBindViewHolder(
viewHolder: Presenter.ViewHolder?,
item: Any?,
payloads: MutableList<Any>?
) {
when {
payloads == null || payloads.isEmpty() -> {
// Your DiffCallback did not returned any information about what changed.
super.onBindViewHolder(viewHolder, item, payloads)
}
"change_title" in payloads -> getRowViewHolder(viewHolder)?.let {
headerPresenter.onBindViewHolder(it.headerViewHolder, item)
}
"change_items" in payloads -> {
val newItems = ((item as? ListRow)?.adapter as? ArrayObjectAdapter)?.unmodifiableList<Item>()
when (val listRowAdapter = (getRowViewHolder(viewHolder).row as? ListRow)?.adapter) {
is ArrayObjectAdapter -> listRowAdapter.setItems(newItems, null)
else -> super.onBindViewHolder(viewHolder, item, payloads)
}
}
else -> {
// If you don't know what changed just delegate to the super.
super.onBindViewHolder(viewHolder, item, payloads)
}
}
}
Customize the implementation of DiffCallback.getChangePayload() to your needs. You can return a list of changes in this function and treat all of them in your ListRowPresenter.onBindViewHolder().
I recently wrote a blog post with samples that might help.
I'm working on android apps using MVVM, and Data Binding. I'm using ListAdapter for my RecyclerView Adapter. The case is, when I submit new data to the adapter using submitList, it reset RecyclerView scroll position. It blink at first and just reset it's position to the top.
My Binding Adapter
#BindingAdapter("listTemplate", "hirarki")
fun bindListTemplate(recyclerView: RecyclerView, data: List<Template>?, hirarki: Int) {
var adapter = recyclerView.adapter as TemplateChiefAdapter
adapter.submitList(data)
}
TemplateFragment where I resubmit my data
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<Boolean>("shouldUpdate")
?.observe(viewLifecycleOwner, {
if (it) {
viewModel.fetchdata()
navController.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("shouldUpdate")
}
})
This piece of code will update LiveData in my ViewModel, so the DataBinding will detect its change and re-submitList the data to the adapter
My List Adapter
class TemplateChiefAdapter(val onClickListener: OnClickListener) : ListAdapter<Template, TemplateChiefAdapter.TemplateChiefViewHolder>(DiffCallback) {
class TemplateChiefViewHolder(private var binding: ItemTemplateChiefBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(template: Template) {
binding.template = template
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<Template>() {
override fun areItemsTheSame(oldItem: Template, newItem: Template): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Template, newItem: Template): Boolean {
return oldItem.id_template == newItem.id_template
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TemplateChiefViewHolder {
return TemplateChiefViewHolder(ItemTemplateChiefBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: TemplateChiefViewHolder, position: Int) {
val template = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(template)
}
holder.bind(template)
}
class OnClickListener(val listener: (template: Template) -> Unit) {
fun onClick(template: Template) = listener(template)
}
}
How can I keep the recycler scroll position after submitList called?
I didn't examine in ultra detail all your code, but the DiffUtil Callback caught my attention.
areItemsTheSame is an optimization from the DiffUtil class to determine if the items changed position. If the didn't, then the contents can be checked, and re-bound to their new data if it changed. If the positions changed, then the item may need to be animated elsewhere or well.. as you can imagine it becomes more complicated from there.
The idea of that method is to compare if the items are the same or not, not to compare the entire item. I would use an id (or anything that can help you identify uniqueness in your items). You are using the === operator and I don't know the rest of your architecture, but comparing by reference may not be accurate if, for instance, your data layer transforms and copies these objects around (something you can't/shouldn't tell/care for in your adapter).
For instance, instead of
return oldItem === newItem
You could do
return oldItem.someId === newItem.someId
This would ensure that even if your items are the same but were copied/recreated/etc., you'd still identify them as such despite them being a different reference.
Then, in areContentsTheSame you are expected to check all the contents that you consider instrumental in deciding if onBind must be called on your specific viewHolder because the contents are different. So I would have expected something more like:
oldItem.something == newItem.something
&& oldItem.xxx == newItem.xxx
&& oldItem.yyy == newItem.yyy
(but maybe with DataBinding you don't need this, I wouldn't know).
All that being said, I have 0.1 experience with DataBinding (and personally for me that was enough), so if this is related in anyway how the data binding library behaves, I can't help you any more. :/
From a RecyclerView's point of view, the rest of the code looks adequate.
I have a list that gets updated from a Room Database. I receive the updated data from Room as a new list and I then pass it to ListAdapter's submitList to get animations for the changes.
list.observe(viewLifecycleOwner, { updatedList ->
listAdapter.submitList(updatedList)
})
Now, I want to add a drag and drop functionality for the same RecyclerView. I tried to implement it using ItemTouchHelper. However, the notifyItemMoved() is not working as ListAdapter updates its content through the submitList().
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
val list = itemListAdapter.currentList.toMutableList()
Collections.swap(list, from, to)
// does not work for ListAdapter
// itemListAdapter.notifyItemMoved(from, to)
itemListAdapter.submitList(list)
return false
}
The drag and drop now works fine but only when dragged slowly, when the dragging gets fast enough, I get different and inconsistent results.
What could be the reason for this? What is the best way that I can achieve a drag and drop functionality for my RecyclerView which uses ListAdapter?
So I made a quick test (this whole thing doesn't fit in a comment so I'm writing an answer)
My Activity contains the adapter, RV, and observes a viewModel. When the ViewModel pushes the initial list from the repo via LiveData, I save a local copy of the list in mutable form (just for the purpose of this test) so I can quickly mutate it on the fly.
This is my "onMove" implementation:
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
list[from] = list[to].also { list[to] = list[from] }
adapter.submitList(list)
return true
I also added this log to verify something:
Log.d("###", "onMove from: $from (${list[from].id}) to: $to (${list[to].id})")
And I noticed it.. works. But because I'm returning true (you seem to be returning false).
Now... unfortunately, if you drag fast up and down, this causes the list to eventually become shuffled:
E.g.: Let's suppose there are 10 items from 0-9.
You want to grab item 0 and put it between item 1 and 2.
You start Dragging item 0 at position 0, and move it a bit down so now it's between 1 and 2, the new item position in the onMove method is 1 (so far, you're still dragging). Now if you slowly drag further (to position 2), the onMove method is from 1 to 2, NOT from 0 to 2. This is because I returned "true" so every onMove is a "finished operation". This is fine, since the operations are slow and the ListAdapter has time to update and calculate stuff.
But when you drag fast, the operations go out of sync before the adapter has time to catch up.
If you return false instead (like you do) then you get various other effects:
The RecyclerView Animations don't play (while you drag) since the viewHolders haven't been "moved" yet. (you returned false)
The onMove method is then spammed every time you move your finger over a viewHolder, since the framework wants to perform this move again... but the list is already modified...
So you'd get something like (similar example above, 10 items, moving the item 0)> let's say each item has an ID that corresponds to its position+1 (in the initial state, so item at position 0 has id 1, item at position 1 has id 2, etc.)
This is what the log shows while I slowly drag item 0 "down":
(format is `from: position(id of item from) to: position(id of item to)
onMove from: 0 (1) to: 1 (2) // Initial drag of first item down to 2nd item.
onMove from: 0 (2) to: 1 (1) // now the list is inverted, notice the IDs.
onMove from: 0 (1) to: 1 (2) // Back to square one.
onMove from: 0 (2) to: 1 (1) // and undo-again...
I just cut it there, but you can see how it's bouncing all over the place back and forth. I believe this is because you return false but modify the list behind the scenes, so it's getting confused. on one side of the equation the "data" says one thing, (and so does the diff util), but on the other, the adapter is oblivious to this change, at least "yet" until the computations are done, which, as you guessed, when you drag super fast, is not enough time.
Unfortunately, I don't have a good answer (today) as to what would the best approach be. Perhaps, not relying on the ListAdapter's behavior and implementing a normal adapter, where you have better list/source control of the data and when to call submitList and when to simply notifyItemChanged or moved between two positions may be a better alternative for this use-case.
Apologies for the useless answer.
I ended up implementing a new adapter and use it instead of ListAdapter, as mentioned on Martin Marconcini's answer. I added two separate functions. One for receiving updates from Room database (replacement for submitList from ListAdapter) and another for every position change from drag
MyListAdapter.kt
class MyListAdapter(list: ArrayList<Item>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
// save instance instead of creating a new one every submit
// list to save some allocation time. Thanks to Martin Marconcini
private val diffCallback = DiffCallback(list, ArrayList())
fun submitList(updatedList: List<Item>) {
diffCallback.newList = updatedList
val diffResult = DiffUtil.calculateDiff(diffCallback)
list.clear()
list.addAll(updatedList)
diffResult.dispatchUpdatesTo(this)
}
fun itemMoved(from: Int, to: Int) {
Collections.swap(list, from, to)
notifyItemMoved(from, to)
}
}
DiffCallback.kt
class DiffCallback(
val oldList: List<Item>,
var newList: List<Item>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldList[oldItemPosition]
val newItem = newList[newItemPosition]
return compareContents(oldItem, newItem)
}
}
Call itemMoved every position change:
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
itemListAdapter.itemMoved(from, to)
// Update database as well if needed
return true
}
When receiving updates from Room database:
You may also want to check if currently dragging using onSelectedChanged if you are also updating your database every position change to prevent unnecessary calls to submitList
list.observe(viewLifecycleOwner, { updatedList ->
listAdapter.submitList(updatedList)
})
I've tried danartillaga's answer and got a ConcurrentModificationException for the list variable. I use LiveData in the code and it looks like the data was changed during invalidation of the list.
I've tried to keep the ListAdapter implementation and concluded to the following solution:
class MyListAdapter : ListAdapter<Item, RecyclerView.ViewHolder>(MyDiffUtil) {
var modifiableList = mutableListOf<Item>()
private set
fun moveItem(from: Int, to: Int) {
Collections.swap(modifiableList, to, from)
notifyItemMoved(from, to)
}
override fun submitList(list: List<CourseData>?) {
modifiableList = list.orEmpty().toMutableList()
super.submitList(modifiableList)
}
}
and the onMove code from ItemTouchHelper.SimpleCallback:
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val adapter = recyclerView.adapter as CoursesDownloadedAdapter
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
val list = adapter.modifiableList
// Change your DB here
adapter.moveItem(from, to)
return true
}
The magic here is saving the modifiableList inside the adapter. ListAdapter stores a link to the list from submitList call, so we can change it externally. During the Drag&Drop the list is changed with Collections.swap and RecyclerView is updated with notifyItemMoved with no DiffCallback calls. But the data inside ListAdapter was changed and the next submitList call will use the updated list to calculate the difference.
I have a RecyclerView that has a list of jobs. The jobs are downloaded and updated in the database. The changes is then reflected in the list and all is good so far.
The data is downloaded and stored regulary in a room database and populated with data binding and live objects.
To avoid that the list is refreshed when the data did not change (overwritten with the same content in the db) I have added a custom DiffCallback for the ListAdapter.
class JobDiffCallback : DiffUtil.ItemCallback<JobWrapper>() {
override fun areItemsTheSame(oldItem: JobWrapper, newItem: JobWrapper): Boolean {
return oldItem.job.jobId == newItem.job.jobId
}
override fun areContentsTheSame(oldItem: JobWrapper, newItem: JobWrapper): Boolean {
return oldItem == newItem
}
}
These makes sure that the objects in the list does not fade in and out when updated with the same content. And also makes sure that data changes are reflected in the list as expected.
The problem is now, that even though the items no longer fades in and out, they will still animate as if they were reordered and back again - so moving a bit.
My question is then: What am I missing to let the view know that the data actually just was updated with the same content and that it should not make any visual changes?
In areContentsTheSame you're supposed to check all relevant variables one by one.
oldItem == newItem is only checking the references (unless you are comparing data classes or have overridden the isEqual() method) of the objects which is not what you want. when you check the references, they are never the same so all items count as items with different content. you need to check the content in details yourself.
So do something like this:
override fun areContentsTheSame(oldItem: JobWrapper, newItem: JobWrapper): Boolean {
return oldItem.job.id == newItam.job.id && oldItem.job.name == newItem.job.name && ....
}
or change your classes to data classes.
Or override the isEqual() method in your job class and use the the method to compare them.
I have a recycler view in my layout, at first it will be filled by data which is stored in local database, and then after a few second it will be updated using server.
the problem is when it updates, items of recycler view change suddenly, how can I set an animation for recycler view that change the items smoothly?
I notify my recycler view just like this:
fun add(list: List<BestStockModel>) {
items.clear()
items.addAll(list)
notifyItemRangeChanged(0, list.size)
}
There's a better way for you do so, you can use ListAdapter link.
Using ListAdapter you can simply submit a new list and the adapter will calculate the diff between the old one and the new one and add need animations for new/changed/deleted items.
It can detect the diff using simple callbacks that you provide to it.
Here's an example that you can use as a reference:
class HomeMoviesAdapter : ListAdapter<Movie, MoviesViewHolder>(
//note the following callbacks, ListAdapter uses them
// in order to find diff between the old and new items.
object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem.title == newItem.title //this can be a unique ID for the item
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem == newItem
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MoviesViewHolder {
val v: View = LayoutInflater.from(parent.context)
.inflate(R.layout.movies_item_view, parent, false)
return MoviesViewHolder(v)
}
override fun onBindViewHolder(holder: MoviesViewHolder, position: Int) {
//your binding logic goes here as usual.
}
}
And then from where you have the list (ex: fragment) you can do the following:
adapter.submit(newList)
And that's it for the list adapter to do the needed animations for you.
There's one gotcha though: if submitted the same list reference, the adapter will consider it the same as the old list, meaning it won't trigger the diff calculations. Note the following example:
//the following is a bad practice DO NOT do this!
val list: MutableList<Int> = mutableListOf(1, 2, 3)
adapter.submitList(list)
list.clear()
list.add(7)
adapter.submitList(list) //nothing will happen, since it's the same ref
Compare that to the following:
//the following is good practice, try to do the following!
adapter.submitList(listOf(1, 2, 3))
adapter.submitList(listOf(7)) //will delete all the old items, insert 7 and will also trigger the need animations correctly.
Although they both seem similar, they quite different: the second one submits a totally new list "reference-wise" to the adapter, which will cause the ListAdapter to trigger the calculations correctly.