Problem
I am writing an android application that has to handle a variable amount of elements in a RecyclerView. When I add an element, some data gets saved into a configuration file along with an UUID.
Either when the application starts or when the element gets created or removed by the user during runtime, the RecyclerView gets updated and shows all currently existing elements.
Each of these elements has a button to remove it and its additional data from the configuration. When I press this button, the elements data successfully gets removed from the configuration and it turns invisible leading the RecyclerView only to show the remaining elements.
However, when I try to insert a new element, it doesn't get shown but rather the element that has been removed previously during this session.
Tries
I have been looking for a solution for a while now.
First of all, I startet reading the documentation over and over again, thinking that I missed something. I also viewed some online examples on how to remove an item from a RecyclerView properly but I can't get it working.
Code
package com.rappee.protectiveparking.ui
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import com.rappee.protectiveparking.R
import com.rappee.protectiveparking.core.Resources
import java.util.*
class ScrollerAdapter : RecyclerView.Adapter< ConsoleViewHolder >() ,
DragItemTouchHelperAdapter {
private var stockpile: Queue< String > = LinkedList< String >()
private var holder: MutableList< ConsoleViewHolder > = ArrayList()
fun push( identification: String ) {
this.stockpile.add( identification )
this.notifyItemInserted( this.itemCount - 1 )
}
fun remove( position: Int ) {
this.holder.removeAt( position )
this.notifyItemRemoved( position )
}
override fun onCreateViewHolder( parent: ViewGroup , type: Int ): ConsoleViewHolder {
val view: ConsoleViewHolder = ConsoleViewHolder( LayoutInflater.from( parent.context ).inflate( R.layout.console , parent , false ) , this.stockpile.poll() )
this.holder.add( view )
return view
}
override fun onBindViewHolder( holder: ConsoleViewHolder , position: Int ) {
return
}
override fun getItemCount(): Int {
return this.holder.size + this.stockpile.size
}
override fun onItemMove( from: Int , to: Int ): Boolean {
Collections.swap( this.holder , to , from )
for ( i in 0 until this.holder.size ) {
Resources.CONFIGURATION.push( "${this.holder.get( i ).identification}.${Resources.KEY_POSITION}" , "$i" )
}
this.notifyItemMoved( from , to )
return true
}
}
Behaviour
When I try to insert a new element, I actually put the UUID into 'stockpile' and call 'notifyItemInserted', so that 'onCreateViewHolder' gets activated. When this happens, I fetch the foremost UUID from 'stockpile' in form of a String to give it to the constructor of 'ConsoleViewHolder' as a parameter.
Only when this method gets called, an actual View gets pushed into the RecyclerView ( not my decision! ).
However, when I insert a new UUID and update the RecyclerView after an item got deleted, this method doesn't get called so that no new View can be shown.
The only thing that changes is that the previously deleted View pops back up.
MY QUESTION: How can I fix this behaviour of my RecyclerView showing deleted items instead of newly pushed ones?
There is no need to store the view holders yourself.
It should look something like this:
private var stockpile = MutableList< String >()
fun push( identification: String ) {
stockpile.add( identification )
notifyItemInserted( itemCount - 1 )
}
fun remove( position: Int ) {
stockpile.removeAt( position )
notifyItemRemoved( position )
}
override fun onCreateViewHolder( parent: ViewGroup , type: Int ): ConsoleViewHolder {
val view: ConsoleViewHolder = ConsoleViewHolder( LayoutInflater.from( parent.context ).inflate( R.layout.console , parent , false ) , this.stockpile.poll() )
return view
}
override fun onBindViewHolder( holder: ConsoleViewHolder , position: Int ) {
//replace with something sensible you want to show in your view
view.someTextView.text = stockpile[position].name
return
}
override fun getItemCount(): Int {
return this.stockpile.size
}
I think you have a bad understanding of what the view holder does. Remove private var holder: MutableList< ConsoleViewHolder > = ArrayList()and all references to it. Do not edit viewholders outside of onBindViewHolder. The weird behaviour you experience is because you do not actually remove the item from the underlying stockpile and then the recyclerview is confused when you tell it that there is a new item, because your count is way off.
see the example at https://developer.android.com/guide/topics/ui/layout/recyclerview for a reference implementation.
Related
I am using this library to put a carousel view in an Android app: https://github.com/ImaginativeShohag/Why-Not-Image-Carousel
I'm also trying to use the showcase type, but a prerequisite to use this type is creating a custom layout for the carousel items.
Creating the layout I understand, but the OP uses this example in Kotlin to show how the custom layout is actually used:
binding.carousel3.carouselListener = object : CarouselListener {
override fun onCreateViewHolder(
layoutInflater: LayoutInflater,
parent: ViewGroup
): ViewBinding? {
return ItemCustomFixedSizeLayout1Binding.inflate(layoutInflater, parent, false)
}
override fun onBindViewHolder(
binding: ViewBinding,
item: CarouselItem,
position: Int
) {
val currentBinding = binding as ItemCustomFixedSizeLayout1Binding
currentBinding.imageView.apply {
scaleType = imageScaleType
// carousel_default_placeholder is the default placeholder comes with
// the library.
setImage(item, R.drawable.carousel_default_placeholder)
}
}
}
val listThree = mutableListOf<CarouselItem>()
for (item in DataSet.three) {
listThree.add(
CarouselItem(
imageUrl = item.first,
caption = item.second
)
)
}
binding.carousel3.setData(listThree)
binding.customCaption.isSelected = true
binding.carousel3.onScrollListener = object : CarouselOnScrollListener {
override fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int,
position: Int,
carouselItem: CarouselItem?
) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
carouselItem?.apply {
binding.customCaption.text = caption
}
}
}
override fun onScrolled(
recyclerView: RecyclerView,
dx: Int,
dy: Int,
position: Int,
carouselItem: CarouselItem?
) {
// ...
}
}
// Custom navigation
binding.btnGotoPrevious.setOnClickListener {
binding.carousel3.previous()
}
binding.btnGotoNext.setOnClickListener {
binding.carousel3.next()
}
I'm having some trouble figuring out what exactly this code is doing and how it would look in Java. Any pointers would be greatly appreciated!
A Quick Guess
It seems that the listener is providing the callback of Recycler View. If you need me to guess within a second, and I will say the custom view is a Recycler View using listener to allow users to register the Recycler View methods (which is the Adapter using in the RV)
Deep Investigation
1st question: What is the Custom View class for id=carousel3 in KotlinActivity in the sample project
Ans: org.imaginativeworld.whynotimagecarousel.ImageCarousel.
(P.S. identical between activity_kotlin.xml and activity_test.xml)
(Below is a screen cap, don't try to click the links since it will not work :))
Let's got to search ImageCarousel and we will find ImageCarousel.kt. Let's find CarouselListener in there
We can see that when CarouselListener is set, it will immediately assign to adapter?.listener (Just ignore the "?" sign if you are not familiar with Kotlin)
2nd question: What is adapter here?
Ans from the same file:
private var adapter: FiniteCarouselAdapter? = null
3rd question: What is FiniteCarouselAdapter?
Ans: Its a RecyclerView.Adapter
open class FiniteCarouselAdapter(
...
) : RecyclerView.Adapter<FiniteCarouselAdapter.MyViewHolder>() {
Last question: How is it related to FiniteCarouselAdapter#listener/CarouselListener/adapter?.listener?
When the RecyclerView#Adapter requires to call the ViewHolder method, it will call to CarouselListener methods instead.
In FiniteCarouselAdapter:
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
...
// Init listeners
listener?.onBindViewHolder(
holder.binding,
item,
realItemPosition
)
As CarouselListener is an interface, the method implementation will be defined in the KotlinActivity instead.
In KotlinActivity:
The above code in your question : )
I'm trying to create a method to set a listener to each of my views inside a list like:
private fun setListeners() {
val clickableViews: List<View> =
listOf(box_one_text, box_two_text, box_three_text,
box_four_text, box_five_text)
for(item in clickableViews){
item.setOnClickListener{makeColored(it)}
}
}
box_one_text, box_two_text and so on are the id of the views inside my xml file and I'm trying to set a color of it when they are clicked like:
fun makeColored(view: View) {
when (view.id) {
// Boxes using Color class colors for background
R.id.box_one_text -> view.setBackgroundColor(Color.DKGRAY)
R.id.box_two_text -> view.setBackgroundColor(Color.GRAY)
// Boxes using Android color resources for background
R.id.box_three_text -> view.setBackgroundResource(android.R.color.holo_green_light)
R.id.box_four_text -> view.setBackgroundResource(android.R.color.holo_green_dark)
R.id.box_five_text -> view.setBackgroundResource(android.R.color.holo_green_light)
else -> view.setBackgroundColor(Color.LTGRAY)
}
}
the problems is that all of the elements inside the list are all red lines or can't reference by the list
First thing first, the list type is wrong. You said that box_one_text and box_two_text are the view id. View id type is Int, so you should change the list to list of Int
val clickableViews: List<Int> =
listOf(box_one_text, box_two_text, box_three_text,
box_four_text, box_five_text)
Then, to apply a click listener to each of the id, you need to find the view using findViewById
for(item in clickableViews){
findViewById<View>(item).setOnClickListener{makeColored(it)}
}
Or if you use view binding, you can follow below code:
val clickableViews: List<View> =
listOf(binding.boxOneText, binding.boxTwoText, binding.boxThreeText,
binding.boxFourText, binding.boxFiveText)
for(item in clickableViews){
item.setOnClickListener{makeColored(it)}
}
Try this:
private fun setListeners() {
val clickableViews: List<Int> =
listOf(R.id.box_one_text, R.id.box_two_text, R.id.box_three_text,
R.id.box_four_text, R.id.box_five_text)
for(item in clickableViews){
findViewById<View>(item).setOnClickListener{makeColored(it)}
}
}
I'm trying to make something like below:
On that screenshot of facebook lite app:
At part marked "1": is a vertical recyclerview which contains posts.
At part marked "2": is a horizontal recyclerview which contains the stories.
At part marked "3": is the same recyclerview as at part marked "1" which contains posts.
I have already made the recyclerview for posts and It works well. Now I want to know how should I make the recyclerview for stories or friendship suggestion and make the two recyclerviews appear like on Facebook app ?
How could I have recyclerviews similar to the one in facebook app ?
Facebook show multiples recyclerViews One vertical where it shows the posts some others horizontal where it
shows stories or sometimes friendship suggestion.
Do you undestand me ?
Please tell me if I should explain more my issue.
Thanks.
you can define different View Holders for a Recycler List
the horizontal lists are just a view holder with another recycler inside them but with horizontal orientation in the main list. you can choose which view holder you want to use in OnCreatViewHolder
You should implement RecyclerView with multi-view type which gives you the various layout on a single RecyclerView adapter.
By doing this you need a model class which should have a field for define the different type.
Please have a look below sample code
Sample Model class
data class Sample(
var a: String = ""
...
var type: String = ""
)
Field type in model class above will define a unique view-type of adapter
Sample Adapter Class
class SampleAdapter(
private val context: Context,
private val items: ArrayList<Sample>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val HEADER = 1
private const val STORY = 2
private const val FEED = 3
}
override fun getItemViewType(position: Int): Int {
return when(items[position].type) {
Constant.HEADER -> HEADER
Constant.STORY -> STORY
else -> FEED
}
}
override fun getItemCount(): Int = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when(viewType) {
HEADER -> HeaderViewHolder(LayoutInflater.from(context).inflate(R.layout_header, parent, false))
STORY -> StoryViewHolder(LayoutInflater.from(context).inflate(R.layout_story, parent, false))
else -> FeedViewHolder(LayoutInflater.from(context).inflate(R.layout_feed, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder) {
is HeaderViewHolder -> holder.onBind()
is StoryViewHolder -> holder.onBind()
is FeedViewHolder -> holder.onBind()
}
}
inner class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun onBind() {
....your business logic
}
}
inner class StoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun onBind() {
....your business logic
}
}
inner class FeedViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun onBind() {
....your business logic
}
}
}
And when you are using the above adapter in Activity or Fragment. You should define the type of each item in ArrayList
For example: samples.add(Sample(...., type=Constant.Header))
Preparation:
RecyclerView with RecyclerView.Adapter binded to SQLite Cursor (via ContentProvider && Loader). RecyclerView and RecyclerView.Adapter linked with SelectionTracker as design suggests.
SelectionTracker builded with StableIdKeyProvider.
On first step - delete an item:
Select RecyclerViews's an item with a long press (cheers to SelectionTracker's SelectionObserver), draw Action Bar Context Menu, fire
the delete action, do the SQL deletion task.
After SQL deletion ends, do the Cursor Loader renewal with
restartLoader call.
onLoadFinished fired, new Cursor obtained, on
RecyclerView.Adapter method notifyDataSetChanged called.
RecyclerView.Adapter redraw RecyclerView content, and all is looks
good.
On second step - do the selection of some other item. Crash:
java.lang.IllegalArgumentException
at androidx.core.util.Preconditions.checkArgument(Preconditions.java:38)
at androidx.recyclerview.selection.DefaultSelectionTracker.anchorRange(DefaultSelectionTracker.java:269)
at androidx.recyclerview.selection.MotionInputHandler.selectItem(MotionInputHandler.java:60)
at androidx.recyclerview.selection.TouchInputHandler.onLongPress(TouchInputHandler.java:132)
at androidx.recyclerview.selection.GestureRouter.onLongPress(GestureRouter.java:96)
at android.view.GestureDetector.dispatchLongPress(GestureDetector.java:779)
at android.view.GestureDetector.access$200(GestureDetector.java:40)
at android.view.GestureDetector$GestureHandler.handleMessage(GestureDetector.java:293)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
What I see on first step while deletion item in progress.
While StableIdKeyProvider do internal job with onDetached ViewHolder item, it don't see previously assigned ViewHolder's position within an Adapter:
void onDetached(#NonNull View view) {
RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
int position = holder.getAdapterPosition();
long id = holder.getItemId();
if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
int position here is RecyclerView.NO_POSITION
Thats why the RecyclerView crashes after - StableIdKeyProvider's cache contains old snapshot of ID's without deletion affected.
The question is - WHY? and HOW to renew the cache of StableIdKeyProvider?
Another note:
While I read the RecyclerView code, I see this comment:
* Note that if you've called {#link RecyclerView.Adapter#notifyDataSetChanged()}, until the
* next layout pass, the return value of this method will be {#NO_POSITION}.
I am not understood what exactly mean this words. Perhaps I faced with described situation - notifyDataSetChanged called in not appropriate time? Or I need to call it twice?
PS.
Sorry for about literary description, there is a lot of complexity code
I am ended up to play with StableIdKeyProvider and switch to plain my own implementation of ItemKeyProvider:
new ItemKeyProvider<Long>(ItemKeyProvider.SCOPE_MAPPED) {
#Override
public Long getKey(int position) {
return adapter.getItemId(position);
}
#Override
public int getPosition(#NonNull Long key) {
RecyclerView.ViewHolder viewHolder = recyclerList.findViewHolderForItemId(key);
return viewHolder == null ? RecyclerView.NO_POSITION : viewHolder.getLayoutPosition();
}
}
Crash is gone, RecyclerView's navigation/selection/modification looks OK.
What about StableIdKeyProvider ?.. Hmm, may be it is not designed to work with mutable content of RecyclerView.
Update 2021-12-03
Last week I got a new round with fighting on RecycleView.
As mentioned on the question - exactly problem is the CACHE of StableIdKeyProvider. And switch to ItemKeyProvider is the workaround.
As code of StableIdKeyProvider explains, chache tied to the window's evens: attach and detach. So, and the comment which I am quoted above - is the exactly pointed to the problem: when new Cursor arrives - reattaching Cursor to the Adapter and notifying - requires to fire at right time. "Right time" - is enqueue this job in layout-message-thread. In this way RecyclerView and underlying "toolbox" can correct perform an update itself. For doing so, just wrap providing a new Cursor inside the post runnable method. The code:
#Override
public void onLoadFinished(#NonNull Loader<Cursor> loader, Cursor data) {
recycler.post(new Runnable() {
#Override
public void run() {
adapter.swapCursor(data);
}
});
...
My problem was solved by setting setHasStableIds(true) in Recycle view adapter and overriding getItemId, It seems that Tracker require both setHasStableIds(true) and overrindinggetItemId in adapter I got this error after setting stable Ids true without overriding getItemId
init {
setHasStableIds(true)
}
override fun getItemId(position: Int) = position.toLong()
override fun getItemViewType(position: Int) = position
Ran into the same problem and after a long search I have found the answer:
You just need to override the method in your Recycler Adapter view.
override fun getItemId(position: Int): Long = position.toLong()
I encountered the same issue with the StableIdKeyProvider. Writing a custom implementation of ItemKeyProvider seems to do the trick. Here's a basic Kotlin implementation you can use when building a selection tracker for a RecyclerView:
class RecyclerViewIdKeyProvider(private val recyclerView: RecyclerView)
: ItemKeyProvider<Long>(ItemKeyProvider.SCOPE_MAPPED) {
override fun getKey(position: Int): Long? {
return recyclerView.adapter?.getItemId(position)
?: throw IllegalStateException("RecyclerView adapter is not set!")
}
override fun getPosition(key: Long): Int {
val viewHolder = recyclerView.findViewHolderForItemId(key)
return viewHolder?.layoutPosition ?: RecyclerView.NO_POSITION
}
}
In my case, the problem is related to the initialization of ItemDetailsLookup.ItemDetails in ViewHolder. As it turned out, getAdapterPosition() may return the wrong position during ViewHolder binding. The solution is to call getAdapterPosition() at the time call getItemDetails() in ItemDetailsLookup.
I got it resolved by keeping the initialization and setting selectiontracker to the adapter at the very end.
capturedThumbnailListAdapter = new CapturedThumbnailListAdapter(this);
capturedThumbnailListAdapter.setCapturedThumbnailList(capturedThumbnailList);
viewBinding.capturedImagesRv.setLayoutManager(new LinearLayoutManager(this,
LinearLayoutManager.HORIZONTAL, true));
viewBinding.capturedImagesRv.setAdapter(capturedThumbnailListAdapter);
SelectionTracker<Long> tracker = new SelectionTracker.Builder<Long>("thumb_selection",
viewBinding.capturedImagesRv, new StableIdKeyProvider(viewBinding.capturedImagesRv),
new CapturedThumbnailListAdapter.ItemLookUp(viewBinding.capturedImagesRv),
StorageStrategy.createLongStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
capturedThumbnailListAdapter.setSelectionTracker(tracker);
This is the adapter:
class ContactsAdapter(val context: Context, private val users: MutableList<Contacts>, val itemClick: (Contacts) -> Unit) : RecyclerView.Adapter<ContactsAdapter.ViewHolder>(){
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.remove.setOnClickListener {
val builder = AlertDialog.Builder(context)
builder.setMessage(R.string.delete_contact)
builder.setPositiveButton(R.string.yes){_, _ ->
users.removeAt(position)
notifyItemRemoved(position)
}
builder.setNegativeButton(R.string.no){_,_ ->
}
val dialog: AlertDialog = builder.create()
dialog.show()
}
}
override fun getItemCount() = users.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.contacts, parent, false)
return ViewHolder(view, itemClick)
}
class ViewHolder(itemView: View?, val itemClick: (Contacts) -> Unit) : RecyclerView.ViewHolder(itemView!!){
val remove = itemView!!.removecontact!!
}
}
I got 2 items for testing, when I delete the second then first one it's fine, but when first then second one then the app crashes and the error is:
java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
at java.util.ArrayList.remove(ArrayList.java:503)
at com.xxx.xxx.classes.ContactsAdapter$onBindViewHolder$2$1.onClick(ContactsAdapter.kt:57)
at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:177)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6944)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
What could be the problem?
Thanks in advance
As you can see, it is an IndexOutOfBoundsException, because you are attempting to access the index 1 in an array of size 1. This is mostly because you are directly using the position argument from onBindViewHolder from inside the AlertDialog's setPositiveButton call.
Instead use the holder.getAdapterPosition method to get the latest position. This should prevent the crash.
Edit #1: What I mean is to replace the position usages with holder.getAdapterPosition(). Your onBindViewHolder should look like this after the edits:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.remove.setOnClickListener {
val builder = AlertDialog.Builder(context)
builder.setMessage(R.string.delete_contact)
builder.setPositiveButton(R.string.yes){_, _ ->
users.removeAt(holder.getAdapterPosition())
notifyItemRemoved(holder.getAdapterPosition())
}
builder.setNegativeButton(R.string.no){_,_ ->
}
val dialog: AlertDialog = builder.create()
dialog.show()
}
}
I got 2 items for testing, when I delete the second then first one it's fine, but when first then second one then the app crashes.
My guess is you do not refresh of the indezes of the list Items in your view
When you remove your fist item everything is fine you removed Index 0 from a list of 2
But your second crashes because you try to remove Index 1 (the second item) of a list that only has one element left.
your "notifyItemRemoved(position)" has to reassign the indezes of all the items that are left after removing one
Firstly, set the click listener outside the bindview holder as it is the bad practice. Just set it on onCreateViewHolder. Then, you will come to know that the listener gets called for the first time as soon as the adapter is set. For that you need to stop it from getting called by simply declaring a bool variable and assigning it to false in the adapter class like this.
private var islistenerCalledFirst: Boolean = false;
Then in the OnCreateViewHolder set the listener. Note, you can get position of the item by calling viewHolder.getAdapterPosition().
holder.remove.setOnClickListener {
if(islistenerCalledFirst){
... //your logic
}
islistenerCalledFirst = true
}
Hope this works for you.