How to handle click in a nested RecyclerView - android

Can someone explain the logic on how to handle this matter:
I have a fragment that after a WebSocket call inflates 2 Recyclerviews.
Child Recyclerview is nested to Parent Recyclerview and the parent adapter calls the child adapter.
I want to put an Interface for a click listener which handles the click in the Child Items in the Fragment.
Where should I put the interface and which class should implement it?

What you're trying to do has been done multiple times.
There are various approaches you can try, but in general, responsibilities would look something like this:
YourContext (Fragment/Activity)
Inflates a layout with a RecyclerView.
Instantiates YourAdapter
Subscribes, Requests, Waits, for your data and passes it onto YourAdapter.
Maintains an interface for click handling, like:
interface YourThingClickHandler {
fun onThingWasClicked(thing: Thing) // and any other thing you may need.
}
Can be YourContext: YourThingClickHandler or if you want, you can keep an anonymous/local instance of that. I usually do the former and then implement the fun onThingWasClicked(...) in the fragment/activity, it depends what you need to do when the item was clicked.
YourAdapter
Expects a list of Things and one YourThingClickHandler instance. So in your Fragment/Activity you'd do, something like (pseudo code):
// This is called once your ViewModel/Presenter/Repository/etc. makes the data available.
fun onThingsLoaded(things: List<Thing>) {
adapter.setClickHandler(this) // this can be passed when you construct your adapter too via constructor like adapter = YourAdapter(this)
adapter.submitList(things) // if the adapter extends a `ListAdapter` this is all you need.
}
Now that you've passed an outer click handler, you need to deal with the inner list. Now you have a few choices:
1. pass the same click handler all the way in and let the innerAdapter directly talk to this.
2. Have the outerAdapter act as an intermediate between the clicks happening in the innerAdapter and bubble them up via this click handler you just supplied.
Which one you chose, will depend largely on what you want to do with it, and how you want to handle it. There's no right or wrong in my opinion.
Regardless of what you do, you still need to get from the view holder to this click handler...
So in YourAdapter you should have another Interface:
interface InternalClickDelegate {
fun onItemTappedAt(position: Int)
}
This internal handler, will be used to talk from the viewHolder, back to your Adapter, and to bubble the tap up to the external click handler.
Now you can have a single instance of this, defined like so in your adapter class (remember this is Pseudo-Code):
private val internalClickHandler: InternalClickDelegate? = object : InternalClickDelegate {
override fun onItemTappedAt(position: Int) {
externalClickHandler?.run {
onThingWasClicked(getItem(position))
}
}
}
So if the external click handler (the YourThingClickHandler you supplied) is not null, then fetch the item from the adapter's data source, and pass it along.
How do you wire this internal handler with each view holder?
When you do onCreateViewHolder, have a ViewHolder that takes... you guessed, a InternalClickDelegate instance and so...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
// decide which viewHolder you're inflating... and...
return YourViewHolder(whateverViewYouInflate, internalClickHandler)
Now your ViewHolder(s) have a reference to this internal click handler...
so when you do onBindViewHolder(...) you probably call a common ViewHolder method of your choice, for example if your View holder can be of different types, you probably have an Abstract viewHolder with a fun bind(thing: Thing) method or similar that each concrete viewHolder subType will have to implement... in there, you'd do something like this:
override fun bind(thing: Thing) {
if (clickHandler != null) {
someViewYourViewHolderInflated.setOnClickListener(this) // this assumes your ViewHolder implements View.OnClickListener from the framework
}
}
Because your ViewHolder implements View.OnClickListener, you must implement the onClick method in it:
override fun onClick(v: View?) {
clickHandler?.onItemTappedAt(adapterPosition)
}
And this is how your ViewHolder, will receive the tap/click event from Android in the onClick method, if you supplied a click Handler (you did in the adapter onCreateViewHolder when you passed the internalClickHandler), it will simply bubble the tap, passing the position. adapterPosition is the Kotlin equivalent of calling getAdapterPosition() in a RecyclerView adapter.
TOO LONG, DIDN'T READ GRAPH
Fragment: ExternalClickListener -> passes an instance of it to the Adapter.
Adapter: Receives the ExternalClickListener, passes an InternalClickListener to each ViewHolder.
ViewHolder: Receives the internal Click Listener, sets itself as Clickable (either the entire itemView or just any widgets you want to make clickable, if you want the whole cell to be clickable, simply use itemView which is the "whole" view of the ViewHolder.
When the viewHolder's view is tapped, android calls the click listener's onClick method. In there, and because you are in a ViewHolder, you can do getAdapterPosition() and pass this to the internal click handler you received.
The Adapter then can transform that position back into data, and because you supplied an External clickListener, it can pass the actual item back to the external click listener.
Wait, but how about a NESTED RecyclerView.
There's nothing special about that, you simply need to provide the same mechanism, and keep passing things around. What you do or how many of these interfaces you have, depends entirely on what you're trying to achieve; like I said at the beginning, each solution is different and other factors must be taken into account when making architectural decisions.
In general, keep this thing in mind: Separation of Concerns: keep things small and to the point. For E.g.: it may seem crazy to have this double interface, but it's very clear what each does. The internal one, is simply concerned about a "tap" in a "view", and to provide the position in a list where said tap occurred.
This is "all" the adapter needs to fetch the data and make an informed guess at what item was truly tapped.
The fragment doesn't know (or care) about "positions", that's an Adapter's implementation detail; the fact that positions exist, is oblivious to the Fragment; but the Fragment is happy, because it receives the Thing in the callback, which is what most likely needs to know (if you needed to know the position for whatever reason, tailor and modify the externalCallback to have the signature of your choice.
Now replicate the "passing hands" from your OuterAdapter to your InnerAdapter, and you have done what you wanted to do.
Good luck!

1) You should put interface in child adapter and implement that in parent and then pass another one interface (long peocess)
2) Use local broadcast manager

you will add ClickListener in parent adapter and also add it in constructor of adapter
public interface HandleClickListener
{
void onItemClicked(int position, SurveysListModel surveysListModel);
}
Make an instance of your clicklistener and then on holderclick listner get the position of item and its value from your model list
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
handleClickListener.onItemClicked(position, yourModelList.get(position));
});
and get in to your activity like this making an instance of you adapter
adapter = new InstrumentsSearchAdapter(yourModelsList, activity.this, new SearchAdapter.HandleClickListener() {
#Override
public void onItemClicked(int position, Model listModel) {
instumentChild = listModel.getInstrument_title();
Intent intent = new Intent(Activity.this, Listing.class);
intent.putExtra("selectedQuestions", listModel.getQuestions());
startActivityForResult(intent, 5);
}
});
And if you want to go to parent recyclerview class implement onActivityResutlt method and get data back from child through intent and get that intent in onActivityResutlt method

Related

How to ccess items inside RecyclerView from parent fragment?

I'm trying to attach an onClickListener() method to an item which is inside a Recycler view. I know I can easily achive that by doing it from the RecyclerAdapter, but the goal of doing that is to show a custom dialog with some information that parent fragment contains, there are some ways to pass data, but I think that's better to attach the listener from fragment instead, and this way I can directly access the data.
I've tried to access from the fragment the way I use to do it from the adapter, with some modifications:
myRecyclerAdapter.myViewHolder.reportContainer.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(getContext(),"Touch",Toast.LENGTH_SHORT).show();
}
});
But aparently the myViewHolder object it's not created yet by the time I try to use it, so I get the Java NullPointerException (F..$&##^$&^%, don't misunderstand me, I love it).
So, I need some help to do what I'm trying to, or some other good ideas to try, warning: I;m really trying to avoid passing data, except with maybe a ViewModel (don't know if I can), becouse it's a lot of fields to pass
This is fundamentally incorrect. The problem here is, there are multiple ViewHolders in the RecylerView. Which one do you want to attach it to? There would be n number of items and not all items will be rendered at the same time.
Instead of updating the ViewHolder, use a callback.
class MyAdapter extends RecyclerView.Adapter {
MyAdapterCallback callback = null;
....
#Override
public void onBindViewHolder(holder: ViewHolder, position: Int) {
holder.reportContainer.setOnClickListener { // You can set this in OnCreateViewHolder as well.
if (callback != null) {
callback.onClick();
}
}
}
}
interface MyAdapterCallback {
void onClick()
}
From your fragment,
myAdapter.callback = new MyAdapterCallback() {
#Override
public void onClick() {
// Access your fragment variables here.
}
}

A single listener for multiple list items

I am using a RecyclerView for a list. Each layout item of the RecyclerView has a Button. This button is supposed to save some data(that which I bound to the view).
I thought that since the same button is in every view item, why not have a single static listener instance that I can bind to all the buttons. Because that would save some memory.
Now, a few points about the listener(View.OnClickListener()):
It is stored as a static member of my RecyclerView.Adapter class and gets instantiated once when the data is being bound for the first item.
The instance is created as an anonymous instance and inside a method( onBindViewHolder(ViewHolder, int position))
Inside the onClick(View v) method of the listener, I access the data to be bound. This data is retrieved from the list of data objects stored as Adapter source using position provided in this method( onBindViewHolder(ViewHolder, int position)).
Now, the problem:
When I click on button and check what item is going to be saved by the listener, I find that the first data entry in the RecyclerView is the one that gets saved corresponding to the clicks of any of the items of the RecyclerView.
I have no idea why this is happening. Can anybody find the cause of it?
Secondly, is this a good strategy to have a single listener for multiple list items?
Note: I am unable to post the code for it. Sorry about that.
The best way to handle RecyclerView clicks is like this:
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = layoutInflater.inflate(R.layout.list_item, parent, false);
final ViewHolder viewHolder = new ViewHolder(view);
viewHolder.actionButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
int itemPosition = viewHolder.getAdapterPosition();
handleActionButtonClick(itemPosition);
}
});
return viewHolder;
}
It is simple and robust. Having single click listener for all the items is an extreme example of over optimisation. These few extra bytes will never matter and it will come with cost of extra boilerplate.
If you are getting the response based upon the first item of the Recyclerview, then to get the actual postion use getAdapterPosition() at ViewHolderclass or onBindView() and getTag() might as well help to get the exact data
Since RecyclerView(as its name suggest) recycles its child views, so there is no memory related issues.

How to find out item is created in recycler view for the first time?

I have a recycler view and I want to find out if an item is created in recycler view for the first time or not?
Is there an event handler for it?
Note: I know how to implement it with flag, but I'm looking for another approach.
Why do you want this? To my mind it seems like there is not a good reason to know this information, a ViewHolder should be independent from that knowledge.
Nothing is provided for this by RecyclerView.Adapter , the only callbacks are to manage the ViewHolder instances, which can be recycled and so any particular instance wouldn't know if it is the first instance or not.
You could store a flag in your data model and access it when you set up the ViewHolder in onBindViewHolder, as you seem to know.
You can ovveride onViewAttachedToWindow(holder: ViewHolder) in your recycler adapter. sample code like this:
private var isFirstChildAttached = false // single fire
override fun onViewAttachedToWindow(holder: ViewHolder) {
super.onViewAttachedToWindow(holder)
holder.pageView.registerTopLevelTouchListener()
if (!isFirstChildAttached) {
isFirstChildAttached = true
checkAndFirePageDimen(holder.itemView)
}
}
private fun checkAndFirePageDimen(itemView: View){
itemView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
itemView.viewTreeObserver.removeOnGlobalLayoutListener(this)
pagerCallback.onFirstItemAttachedToPager(itemView)
}
})
}
"checkAndFirePageDimen" helps you for reliable itemView size.

BottomSheetDialog/BottomSheetDialogFragment — which to use and how?

I'm working on a Material design app. One feature I want to implement is some kind of a poll. When a user clicks an element of a list, the persistent bottom sheet dialog, which looks like this should show up:
Then, when user clicks any button, this dialog should go away and the modal bottom sheet dialog should show up, providing a user with more information about the list item which was clicked at the beginning. It looks like this:
I can't find any clear explanations about BottomSheetDialog and BottomSheetDialogFragment, and how to use them correctly, even after reading some information about AppCompat dialogs. So, my questions are:
In what way are they different and which one should I use for each
case?
How to get data in the activity about which button was pressed in the dialog?
Any links to the code of implementations or tutorials about using them?
Finally, I've found the solution and it works. Tell me if I'm doing something wrong. It basically works like DialogFragment from this guide, but I've done it a bit different.
1) Their difference is the same as it of DialogFragment and Dialog, and they both are modal. If you need persistent dialog, use BottomSheetBehaviour instead (I found out that both dialogs had to be modal in my app).
2) I have to answer the third question with some code first, and then it will be easy to answer the second one.
3) Create a new public class, which extends BottomSheetDialogFragment, I called it FragmentRandomEventPoll. There are two two things which have to be implemented here.
Override method onCreateView. It is nearly the same as onCreate method in Activities, except for that it returns the View it should inflate:
// We will need it later
private static String headerItem;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_random_event_poll, container, false);
header = (TextView) v.findViewById(R.id.uRnd_fragment_bottom_sheet_poll_header);
skip = (Button) v.findViewById(R.id.uRnd_fragment_bottom_sheet_button_skip);
header.setText(...);
// I implemented View.OnClickListener interface in my class
skip.setOnClickListener(this);
return v;
}
Static method which you can pass necessary data to and get new instance of this class (Probably I could have just used a regular constructor, I'll have to experiment with it a bit more). URandomEventListItem is the data model class.
public static FragmentRandomEventPoll newInstance(URandomEventListItem item) {
FragmentRandomEventPoll fragment = new FragmentRandomEventPoll();
headerItem = item.getHeader();
return fragment;
}
2) To get input events in activity or any other place, define an interface with necessary methods and create setter method for it's instance:
private PollButtonClickListener listener;
public void setListener(PollButtonClickListener listener) {
this.listener = listener;
}
public interface PollButtonClickListener {
void onAnyButtonClick(Object data)
}
And in the place you want to get your data ("dialog_event_poll" tag was specified in the layout):
FragmentRandomEventPoll poll = FragmentRandomEventPoll.newInstance(events.get(id));
poll.setListener(new FragmentRandomEventPoll.PollButtonClickListener() {
#Override
public void onAnyButtonClick(Object data) {
// Do what you want with your data
}
});
poll.show(getSupportFragmentManager(), "dialog_event_poll");
}
If there is anything unclear, my project files could be found on Github.
About handling events from DialogFragment/BottomSheetDialogFragment.
For applications with many activities, this method is great:
context as MyDialogFragmentListener
But I have a problem with an application with single activity and multiple fragments. Since there can be a lot of fragments, it seems like a very bad option to transfer all events to the necessary fragments through the main activity. Therefore, I decided to do this:
private inline fun <reified T> findListeners(): ArrayList<T> {
val listeners = ArrayList<T>()
context?.let {
if (it is T) listeners.add(it)
if (it is AppCompatActivity) {
it.supportFragmentManager.fragments.forEach { fragment ->
if (fragment is T) listeners.add(fragment)
fragment.childFragmentManager.fragments.forEach { childFragment ->
if (childFragment is T) listeners.add(childFragment)
}
}
}
}
return listeners
}
Code in DialogFragment:
private val listeners by lazy { findListeners<MyDialogFragmentListener>() }
Of course, fragments can contain as many other fragments as you like and probably need to be checked through recursion, but in my case this is superfluous.

RecyclerView Adapter notifyDataSetChanged stops fancy animation

I am building a component based on RecyclerView, allowing user to reorder items by drag and drop.
Once I am on the DragListener side, I need the position it has in the adapter in order to perform correct move, but I only have access to the view.
So here is what I am doing in the adapter view binding :
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
Track track = mArray.get(position);
viewHolder.itemView.setTag(R.string.TAG_ITEM_POSITION, position);
}
Does it seem correct to you ?
Because if I move an item like this :
public void move(int from, int to){
Track track = mArray.remove(from);
mArray.add(to, track);
notifyItemMoved(from, to);
}
then position tag is not correct anymore, and if I notifyDataSetChanged(), I lose the fancy animation.
Any suggestion ?
There is a way to preserve fancy animations with just notifyDataSetChanged()
You need to make your own GridLayoutManager with overriden supportsPredictiveItemAnimations() method returning true;
You need to mAdapter.setHasStableIds(true)
The part I find tricky is you need to override you adapter's getItemId() method. It should return value that is truly unique and not a direct function of position. Something like mItems.get(position).hashCode()
Worked perfectly fine in my case - beautiful animations for adding, removing and moving items only using notifyDataSetChanged()
No, it is wrong. First of all, you cannot reference to the position passed to the onBindViewHolder after that method returns. RecyclerView will not rebind a view when its position changes (due to items moving etc).
Instead, you can use ViewHolder#getPosition() which will return you the updated position.
If you fix that, your move code should work & provide nice animations.
Calling notifyDataSetChanged will prevent predictive animations so avoid it as long as you can. See documentation for details.
Edit (from comment): to get position from the outside, get child view holder from recyclerview and then get position from the vh. See RecyclerView api for details
1) You'll use notifyItemInserted(position); or notifyItemRemoved(position); instead of notifyDataSetChanged() for animation.
2) You can just manually fix your problem - using
public void move(int from, int to){
Track track = mArray.remove(from);
mArray.add(to, track);
notifyItemMoved(from, to);
ViewHolder fromHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(from);
ViewHolder toHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(to);
Tag fromTag = fromHolder.itemView.getTag();
fromHolder.itemView.setTag(toHolder.itemView.getTag());
toHolder.itemView.setTag(fromTag);
}
You should move your method to OnCreateViewHolder, then notifyItemRemoved(index) works properly.
I'm able to maintain the touch animations by adding this to my list item's outer element
<View
android:foreground="?android:attr/selectableItemBackground"
...>
I fixed it with using 'notifyItemChanged(int position);' instead of 'notifyDataSetChanged();'
My adapter shows fancy animations perfectly and without any lags
Edit: I got position from onBindViewHolder's position.
as stated by others above, you can have animation while using notifyDataSetChanged on your adapter, although you need to specifically use stable ids. if your items IDs are strings, you can generate a long id for each string id you have and keep them in a map. for example:
class StringToLongIdMap {
private var stringToLongMap = HashMap<String, Long>()
private var longId: Long = 0
fun getLongId(stringId: String): Long {
if (!stringToLongMap.containsKey(stringId)) {
stringToLongMap[stringId] = longId++
}
return stringToLongMap[stringId] ?: -1
}
}
and then in your adapter:
private var stringToLongIdMap = StringToLongIdMap()
override fun getItemId(position: Int): Long {
val item = items[position]
return stringToLongIdMap.getLongId(item.id)
}
another useful thing to consider, if you are using kotlin data class as items in your adapter, and you don't have an id, you can use the hashCode of the data class itself as stable id (if you are sure that the item properties combination are unique in your data set):
override fun getItemId(position: Int): Long = items[position].hashCode().toLong()

Categories

Resources