I am struggling with one feature that I need to implement in recycler view, I need to update few child based on the action performed on any child.
E.g. I have recycler view with lets say 10 items
Child 1
Child 2
Child 3
-
-
Child 10
Now at a time there are only 5 child that are visible on the screen and rest of them comes only when the list is scrolled. What I wanted to achieve is when I click on child 1 and perform an action, that action returns me to update random Child 4, Child 7 & Child 8
Now the problem is how do I update the child which isn't visible in the list.
I have tried using following solution:-
val childCount = rvQuestion.childCount
for (i in 0 until childCount) {
val child = rvQuestion.getChildAt(i)
.findViewById<TextInputEditText>(R.id.etQuestion)
val questionHint = child.hint
// list is the list of child that needs to be populated with a hint
if(list.contains(questionHint)) {
child.setText("someValue")
}
}
The issue is recycler view never gives the childCount as 10 it gives only 5, which are currently visible and hence the wrong child are getting updated.
Any other way to handle this?
You need to use your RecyclerView adapter to grab the child count and to update any children. The adapter requires to override a getItemCount function which should return the total items in the list. Attempting to get the child count or updating a child directly from the RecyclerView is not the correct way.
class TestAdapter(private val data: MutableList<String>) : RecyclerView.Adapter<TestAdapter.ViewHolder>() {
...
override fun getItemCount(): Int {
return data.size
}
...
}
And to update a child in the your RecyclerView you need to tell the adapter to do that as well. Inside your adapter you can create a method that takes in an index any other necessary data to update a child.
class TestAdapter(private val data: MutableList<String>) : RecyclerView.Adapter<TestAdapter.ViewHolder>() {
...
fun updateChildAt(idx: Int, newValue: String) {
data[idx] = newValue
notifyItemChanged(idx)
}
...
}
Related
Im trying to implement a drag and drop in a ConcatAdapter. I did a ConcatAdapter bc I have multiples sections where the items are different, so I just need to drag and drop inside one section.
I did a ConcatAdapter with 2 adapters
I did the ItemTouchHelper for the recycler view -> itemTouchHelper.attachToRecyclerView(binding.recyclerView)
I guess that it's something related with the ItemTouchHelper bc its set on the recycler view and not on the adapter.
Is there any way I can set just the drag and drop for 1 adapter inside the ConcatAdapter?
Inside onMove from ItemTouchHelper.Callback, you can just decide to ignore some of the moves. Use viewHolder (the ViewHolder which is being dragged) and targetViewHolder and check their bindingAdapter. Or if they're instances of the same adapter, you can use a field that has been set on adapter initialization, to determine which adapter they are.
In the example below I have two adapters, both of type TaskAdapter, but second one holds completed tasks. Uncompleted tasks can't be moved between completed ones and completed tasks can't be moved at all. So if targetViewHolder is bound by the adapter flagged as completed, I just return false and ignore the move. Obviously an item has been dragged over that target item but since it's not actually moving, user understands this isn't allowed and will give up. I think it's acceptable and better than having multiple RecyclerViews.
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
targetViewHolder: RecyclerView.ViewHolder
): Boolean {
val targetAdapter = targetViewHolder.bindingAdapter as TaskAdapter
if (targetAdapter.completed) return false
val adapter = viewHolder.bindingAdapter as TaskAdapter
adapter.moveItem(
viewHolder.bindingAdapterPosition,
targetViewHolder.bindingAdapterPosition
)
return true
}
If you want some items can't be dragged in the first place, you can just return 0 from getDragDirs. In this example they are completed tasks.
override fun getDragDirs(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val adapter = viewHolder.bindingAdapter as TaskAdapter
if (adapter.completed) return 0
return super.getDragDirs(recyclerView, viewHolder)
}
I have a recycler view and there are items in it. Now I am using bubble sorting to rearrange them. But adapter.notifyItemMoved doesn't work properly in the loop (it is slow). Please look at the code below and help, I have added comments to make u understand. One more thing, I can't sort the list in beginning before adapting. So please don't put that as a solution. I know that this is a solution but that will not work in my case.
//rv_all is a recycler view
for (i in 0 until rv_all.childCount)
{
for (j in 0 until rv_all.childCount - 1)
{
/* i have added tag while binding in ViewHolder
itemView.stdate.tag = "date" */
val date1 =
sdf.parse(rv_all.getChildAt(j).findViewWithTag < TextView("date").text.toString())
val date2 =
sdf.parse(rv_all.getChildAt(j + 1).findViewWithTag < TextView("date").text.toString())
if (date1 != null && date2 != null)
{
if (date1.before(date2))
{
//alllist is a list used to adapt to recycler view
Collections.swap(alllist, j, j + 1)
alladapter.notifyItemMoved(j, j + 1)
// problem is here ,it is working fine when not in a loop but in loop it is doing nothing
// i have debugged and used breakpoints and what i saw that alllist is swapping elements but adapter child are not rearranging
}
}
}
}
you are iterating through childrens of RecyclerView (why twice? you arent using i anywhere, this is unefficient)
for (i in 0 until rv_all.childCount) {
for (j in 0 until rv_all.childCount - 1) {
but these are only Views - first child/View is at position 0, second 1 etc. When you scroll a bit down and your RecyclerView first visible item is e.g. 10th in alllist, then still first visible View is at position 0, like always
thus these lines makes no sense:
Collections.swap(alllist,j,j+1)
alladapter.notifyItemMoved(j,j+1)
they always swapping and notifying items at the beggining of array, starting 0, but your RecyclerView can be scrolled a bit down to e.g. 10th item - then above lines are swapping items in alllist, but notifyItemMoved does nothing as RecyclerView doesn't have to redraw first items, they are "scrolled out"
so in short: position of View drawn in RecyclerView != position in data array
you can add "real_position" tag in adapter to every child, then you can still iterate through visible childs/Views, obtain Views with findViewByTag, but swap and notifyItemMoved for positions in data array ("real_position" obtained from tag), not visible childs positions in parent RecyclerView
var realPosition : Integer = rv_all.getChildAt(j).tag as Integer // set in adapter
Collections.swap(alllist, realPosition, realPosition+1)
alladapter.notifyItemMoved(realPosition, realPosition+1)
You can try using notifyDataSetChanged():
class MovieAdapter(...) : RecyclerView.Adapter<MovieViewHolder>() {
var data: List<Movies> = list
set(value) {
field = value
notifyDataSetChanged()
}
...
For a more effient way of refreshing use DiffUtil.ItemCallback<...>(). See the Docs
I have custom ViewPager and PagerAdapter which is loading items inside instantiateItem. It is working fine if I initialize it for the first time with set list of items.
But as I call refresh on the list and I want to populate Adapter with new (totally different) list, after calling viewPager.adapter?.notifyDataSetChanged(), PagerAdapter stops working properly and those items are blank pages without content and I can swipe through them in UI.
PageScreen is not Fragment. It is just ViewGroup container which is inflating layout and setting values out of specific item. It is similar to ViewHolder + Binder in RecyclerView.
Also instatiateItem() is called only once as i add new list and call notifyDataSetChanged(). At start it is called 3 items, which is amount of PageScreen items in first list.
//init
val pages = mutableListOf<PageScreen>()
pages.add(PageScreen(activity, app, itemJs1, onClick = {onItemClicked(itemJs1.id)}))
pages.add(PageScreen(activity, app, itemJs2, onClick = {onItemClicked(itemJs2.id)}))
pages.add(PageScreen(activity, app, itemJs3, onClick = {onItemClicked(itemJs3.id)}))
swipePager.adapter = CustomPagerAdapter(pages).also { it.notifyDataSetChanged() }
...
//on refresh after API call
pages.clear()
contentList.forEach{item-> pages.add(PageScreen(activity, app, item, onClick = {onItemClicked(item.id)}))}
(swipePager.adapter as? CustomPagerAdapter)?.notifyDataSetChanged()
Also tried this (same result):
//on refresh after API call
val newPages = mutableListOf<PageScreen>()
contentList.forEach{item-> newPages.add(PageScreen(activity, app, item, onClick = {onItemClicked(item.id)}))}
swipePager.adapter = CustomPagerAdapter(newPages).also { it.notifyDataSetChanged() }
Adapter:
class CustomPagerAdapter(private var pageList: MutableList<PageScreen>) : PagerAdapter() {
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
override fun getCount(): Int {
return pageList.size
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
override fun instantiateItem(container: ViewGroup, position: Int): View {
val layout = pageList[position].getScreen(container)
container.addView(layout)
return layout
}
}
Also I tried to properly refresh items (I expect that this is done internally by PagerAdapter and ViewPager when I call notifyDataSetChanged()) by removing them from ViewPager contentView and calling instantiateItem() for each item. But same result as above. Now every single page was blank. Function below is added to CustomPagerAdapter.
fun refreshItems(vp: ViewPager, data: MutableList<PageScreen>){
pageList.apply {
forEachIndexed{pos, item->
item.screenView?.let { sv->
destroyItem(vp, pos, sv)
}
}
clear()
addAll(data)
forEachIndexed { pos, _ -> instantiateItem(vp, pos) }
}
notifyDataSetChanged()
}
UPDATE:
I managed to "fix" this by setting ViewPager height to fixed value instead WRAP_CONTENT but its not a solution. I want ViewPager with dynamic height, because some of its children can have different height + setting something to static is not good approach in Android. Some phones with square displays could have cropped page then.
What happened is as I replaced all items, those "blank" pages were items with 0px height and 0px width for some unknown reason.
If I replaced ViewPager height to dp value, it "worked". But as I replaced those Views, first item was always blank. but as I scrolled to third one and back to first, item was there for some reason.
Also I don't get that height problem. I have function inside ViewPager which is setting its height based on tallest child in list. It works if list is static, but it is not working now as I refresh that.
Recreating whole ViewPagerAdapter is a solution for this problem.
I called API inside onPageSelected() and remembered position returned to listener function.
Then I recreated whole adapter like this:
swipePager.apply {
adapter = null
adapter = CustomPagerAdapter(newPages)
adapter?.notifyDataSetChanged()
invalidate()
}
and after refresh I scrolled to remembered position like this:
setCurrentItem(position, true)
This solution will not left blank pages, but I had to set static height for my ViewPager, which can be a problem on mdpi screens which sometimes cannot redraw particular dp value from XML layout correctly. Phone with this problem is for example Sony Xperia E5 which has xhdpi screen and part of my ViewPager is cropped from the bottom.
This have to be tuned manually with separate dimens.xml for both mdpi and xhdpi.
I am using a RecyclerView to show list of products in my app, I need to group the product based on aisle. while the data are fetched for the first time in the list, the products are grouped correctly with respect to aisle. When we scroll the view, the aisle group divider is shown for the wrong item and the divider gets restored to correct position once the onBindViewHolder gets refreshed automatically.
MyAdapter.class
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
itemsGrouping(pickItem, pickItemView, holder.adapterPosition)
}
private fun itemsGrouping(pickItem: PickItem, pickItemView: View, adapterPosition: Int) {
//Based on some condition
if(SomeCondition)
itemDivider(pickItemView,true)
else
itemDivider(pickItemView,false)
}
private fun itemDivider(v: View, boolean: Boolean) {
if(boolean) {
v.visibility = View.VISIBLE
} else {
v.visibility = View.GONE
}
}
Well, you should know that the view holders are reused in the RecyclerView, so it's probable not the right idea to try to determine the visibility of the divider in onBindViewHolder. I would recommend using item decorator for dividers. Here's the question and answer for that
How to add dividers and spaces between items in RecyclerView?
The problem is RecyclerView recycles previous views in order to be efficient.
I guess "SomeCondition" contains artifacts which are from previous holders.
So at
itemsGrouping(pickItem, pickItemView, holder.adapterPosition)
you should get pickItem and pickItemView from newly bound holder. You should use like
pickItemView = holder.findViewById(R.id.pickItemView);
Or consider using DataBinding Library
Here is a good example (it's in Kotlin) : DataBoundListAdapter
Once you extend your adapter to DataBoundListAdapter and override bind() method, everything inside bind is executed for every row, so you won't get repeated results.
Note : notice "executePendingBindings()"
I am having trouble debugging a recycler view issue. Here I am trying to delete an item from recycler view using the following method:
when I click on the card (one that is to be deleted), it will store the title of a card in shared preference. (so that in future when I open the app I will know what cards are not to be displayed)
Then I am calling a method which checks if the title in shared preference matches the title of the card and make ArrayList consisting of cards which are not present in shared preference.
Now I am using this ArrayList to fill cards using notifyDataSetChanged
My code is as Follows.
Adapter.
class LocalAdapter(var localCardsInfo : ArrayList<LocalModel>?,var fragment: LocalListingFragment) : RecyclerView.Adapter<LocalHolder>() {
fun refreshDataOnOrientationChange(mLocalCards : ArrayList<LocalModel>?){
if (localCardsInfo!!.size>0)
localCardsInfo!!.clear()
localCardsInfo = mLocalCards
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return localCardsInfo!!.size
}
override fun onBindViewHolder(holder: LocalHolder, position: Int) {
if (localCardsInfo!=null)
holder.updateUI(holder.adapterPosition,localCardsInfo!![holder.adapterPosition])
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocalHolder {
val card_a : View = LayoutInflater.from(parent.context).inflate(R.layout.card_local_a,parent,false)
return LocalHolder(card_a,fragment)
}
}
ViewHolder
class LocalHolder(itemView : View,val fragment: LocalListingFragment) : RecyclerView.ViewHolder(itemView),OpenedLayoutManagerLocal{
fun updateUI(position : Int , localModel: LocalModel){
moveButton.setOnClickListener({
archived()
})
private fun archived(){
ALL_STATIC_CONSTANTS_AND_METHODS.addToArchive(fragment.activity!!,model!!.roomName,model!!.deviceName)
itemView.archiveLayout.visibility = View.GONE
itemView.elevateLayoutLocal.visibility = View.GONE
fragment.mAdapter!!.refreshDataOnOrientationChange(ArrayList(LocalDataService.ourInstance.getNonArchivedItems(fragment.activity!!)))
}
For example, if I have six item in recycler view and when I delete one I can see 5 items on the list while debugging, even onbind is called 5 times but only 4 items are displayed.
I tried my best to debug it yet failed to find a solution and I can't think about what to do next. I tried notifyItemRemoved and notifyItemRangeInserted also but still, I am facing the same issue. It would be a great help if someone can suggest a possible cause of such issue.
Thank you.
Edits:
I just experimented by adding dummy button in fragment where recycler view is placed. An to my surprise if I delete element here just element at that position gets deleted and there is no case of disappearing items. So it seems like problem happens when I delete an item from view holder. So I guess I got the cause of the error but can't think of a way to implement it because the original button is in the card so I am forced to use delete procedure inside view holder. Please do suggest if you have any suggestions. Code that I added in fragment is as follow:
v!!.findViewById<Button>(R.id.delete_button).setOnClickListener({
try {
val model = LocalDataService.ourInstance.getNonArchivedItems(activity!!)[0]
ALL_STATIC_CONSTANTS_AND_METHODS.addToArchive(activity!!,model.roomName,model.deviceName)
mAdapter!!.refreshDataOnOrientationChange(ArrayList(LocalDataService.ourInstance.getNonArchivedItems(activity!!)))
}catch (e : Throwable){}
})
Here on this new dummy button click, it deletes the first item in the list gets deleted while on clicking the button on RecyclerView card it deletes one item and then one more item gets disappeared.