Least worst pattern for handling clicks inside an Android recycler adapter

3,100 views
Skip to first unread message

F Estevez

unread,
Feb 9, 2015, 2:15:17 PM2/9/15
to rxj...@googlegroups.com
Hallo! I have my beautiful injecting + binding architecture for most view types already integrated on Android, but there's one that's resisting me: handling clicks inside a recycler adapter. Given that I want inversion of control the idea is that for every binding there's a subscription sent to a centralized click handler and it'll decide what to do with it. The problem comes with view recycling, as the observable cannot be bound to the view's lifecycle, hence effectively duplicating it whenever the viewholder is reused. I've tried a couple of ideas with Subjects, but none made me exceptionally happy, and I've seen no helpers related to RecyclerView on the package.

Extra quick question: why isn't rxandroid-framework visible in 0.24.+?

Joshua Pierce

unread,
Mar 6, 2015, 9:46:00 AM3/6/15
to rxj...@googlegroups.com
So i am having the same problem, I'll post my research and follow it with what I have come up with so far that seems to work.

RecyclerView.Adapter is tricky because we have to deal with the itemViews, the ViewHolder, and the Adapter's Owner, which in my case is a View (could be Fragment/Activity/etc).  I've seen a lot of examples of using OnClickListener on the adapter and bridging that with a Listener Interface on the adapter, but like you I am looking for a more Rx way of doing things.

My idea, like yours, is to create a click subscription on each ViewHolder and publish it to interested parties.  I started with the idea of have a subject in the adapter, then having a clickObserver mapped to the position of the viewholder that would subscribe the subject.  A lot of problems though, for one thing, the only way to get the position of the viewholder in the adapter is in bindViewHolder, and you have to capture the position as a final.  Seems dirty to me.  I tried it anyway, but how to manage the subscriptions when the viewHolder is recycled?

I started looking at the Adapter and there's some lifecycle callbacks I thought might be useful:

onAttachedToRecyclerView(V)
onDetachedFromRecyclerView(V)
onViewAttachedToWindow(VH)
onViewDetachedFromWindow(VH)
onViewRecycled(VH)

I thought I could maybe hook in there somewhere and do some unsubscribing, but that creates asymmetry because I'm still subscribing in bind.  I decided to set the Observable and the Subscription inside the viewHolder, that way I could subscribe and unsubscribe at will with null checks....

It was rapidly becoming a mess.  Then I found out that RecyclerView has this method:
getChildPosition(View): int

and I realized that I didn't need to capture the position in the adapter, I could just observe the itemView and the observer can deal with looking up its position through the recyclerview.  That vastly simplified my code and then led me to realize that everything can be handled in onCreateViewHolder().  I've tested the automatic unsubscribing using the ViewObservable.bindView() and everything seems to be working.  I was worried about the onViewRecycled(VH) because I thought it might deallocate the view but the views don't ever get deallocated as far as I can tell.

Actually, I don't know what the difference is between onViewDetachedFromWindow() and onViewRecycled(), I guess it has something to do with the way RecyclerView uses "scraps", views that can be detached temporarily without being recycled?  I assume that is for animations or re-ordering itemViews or something, I haven't really used those features yet.

Anyway, that's my line of thinking, research, and what I've come with so far.

Joshua Pierce

unread,
Mar 6, 2015, 10:07:14 AM3/6/15
to rxj...@googlegroups.com
Here's what I ended up with, like I said, it seems to be unsubscribing correctly.  If anyone has come up with anything in the meantime that is better, or if you see I am doing something wrong, please let me know!  I'm pretty new to Rx so I can use all the help I can get.

In the Adapter, I create a PublishSubject that will subscribe to clickObservables and expose it as an Observable<View>:

private PublishSubject<View> clickedView = PublishSubject.create();
public Observable<View> onClickView() {
   
return clickedView;
}

in onCreateViewHolder, I subscribe the subject to a clickObservable that is bound to the parent ViewGroup (RecyclerView).  This will auto-unsubscribe when the parent is detached, as I understand it (tested, it unsubscribes correctly):

@Override
public SettingsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
   
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_cell_settings, parent, false);
   
SettingsViewHolder vh = new SettingsViewHolder(v);

   
ViewObservable.bindView(parent, ViewObservable.clicks(v))
               
.map(new Func1<OnClickEvent, View>() {
                   
@Override
                   
public View call(OnClickEvent onClickEvent) {
                       
return onClickEvent.view();
                   
}
               
})
               
.subscribe(clickedView);

   
return vh;
}

So I am binding the clickObservable to the parent, then mapping the onClickEvent to the actual itemView that I want, then subscribing the subject (clickedView)

Finally, to complete the puzzle, I am observing the subject in the Adapter's owner, where I use the recyclerView to lookup the itemView's position:

ViewObservable.bindView(this, _adapter.onClickView())
               
.subscribe(new Action1<View>() {
                   
@Override
                   
public void call(View view) {
                       
int pos = _recyclerView.getChildPosition(view);
                       
MyItem item = _adapter.itemAt(pos);
                       
Log.e(getClass().getName(), "clicked menu item: " + item.getTitle());
                   
}
               
});

Hope that helps!  Like I said, I would appreciate any feedback on this, especially if anyone spots any errors!

Thanks

John Hogan

unread,
May 14, 2015, 10:09:35 AM5/14/15
to rxj...@googlegroups.com
@Joshua when you say adapter's owner do you mean the wherever the adapter was initialized (i.e. maybe an activity/fragment).  I am in the process of implementing something similar.

Bob van der Linden

unread,
May 15, 2015, 10:46:56 AM5/15/15
to rxj...@googlegroups.com
I'm doing something similar at the moment, but am using a single general event-observable (instead of a click, longclick and others), so that all events can propagate through the Adapter.
However, I'm a bit worried about the subscription. In "onCreateViewHolder" the observable is not stored and not unsubscribed. Doesn't this create leakage?
What I thought of doing was subscribe in onViewAttachedToWindow and unsubscribe in onViewDetachedFromWindow. I'm still a bit unsure whether this is the right solution as well.

Joshua Pierce

unread,
Aug 20, 2015, 11:46:00 AM8/20/15
to RxJava
Yes, exactly, the adapter's owner in this context is the thing that is controlling it (an activity).  It has a reference to the adapter "_adapter"

Joshua Pierce

unread,
Aug 20, 2015, 11:58:23 AM8/20/15
to RxJava
@Bob Using ViewObservable.bindView() on the parentView will take care of unsubscribing (it uses onViewDetachedFromWindow to do that)

Joshua Pierce

unread,
Aug 20, 2015, 12:00:26 PM8/20/15
to RxJava
@Bob you can always use doOnUnsubscribe() to test if something is being unsubscribed correctly


On Friday, May 15, 2015 at 4:46:56 PM UTC+2, Bob van der Linden wrote:

Bob van der Linden

unread,
Aug 20, 2015, 1:53:51 PM8/20/15
to RxJava
Ah, thanks. ViewObservable.bindView on the parent is a much more practical option.
Checking whether doOnUnsubscribe is called is another good one. I might be able to keep a global list of not-unsubscribed subscriptions, which can be logged when the application unloads.

Thanks for the suggestions!
Message has been deleted

Yair Kukielka

unread,
Aug 30, 2015, 9:31:49 PM8/30/15
to RxJava
Hi, you can check out this library RxBinding with these methods 

RxView.attaches(View v) 
RxView.dettaches(View v)

They internally use onViewAttachedToWindow and onViewDetachedFromWindow

So, to subscribe to an observable until the view is detached you could do something like observable.takeUntil(RxView.detaches(myView)).subscribe(...)

Cheers!

Joshua Pierce

unread,
Sep 1, 2015, 4:33:47 AM9/1/15
to RxJava
@Yair Nice! There have been a lot of changes to RxAndroid in the last month or so, Jake Wharton has split off the UI stuff into RxBinding so my solution may no longer be valid or there may now be a better one.

Joshua Pierce

unread,
Sep 21, 2015, 9:33:46 AM9/21/15
to RxJava
Ok I have had a chance to look at the new RxAndroid / RxBinding et al and thanks to Yair's suggestion I modified my original solution:

private PublishSubject<View> clickSubject = PublishSubject.create();
public Observable<View> getItemClickSignal() {
   
return clickSubject;
}

@Override
public InstallationViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

   
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_installation, parent, false);

   
RxView.clickEvents(view)
           
.takeUntil(RxView.detaches(parent))
           
.map(new Func1<ViewClickEvent, View>() {
               
@Override
               
public View call(ViewClickEvent viewClickEvent) {
                   
return viewClickEvent.view();
               
}
           
})
           
.subscribe(clickSubject);

   
return new InstallationViewHolder(view);
}

No change on the consumer side (the object that subscribes to the getItemClickSignal() observable (in my case Activity/Fragment).  They will receive the view and use that to query the recyclerview for the correct index. (method has changed, getChildPosition() is deprecated, now use getChildAdapterPosition() )


Agent Knopf

unread,
Nov 29, 2015, 5:43:55 PM11/29/15
to RxJava
@Joshua Pierce

Thanks a lot for posting your solution! However it seems a few things have changed since September, such as:

  1. RxView.clickEvents has been removed (22.10.2015)
  2. subscribe will only take Actions, Subscribers or Observers, PublishSubject however is a Observable
I tried a slightly different approach to solve the problem under the changed circumstances however it doesn't strike me as a very elegant solution so suggestions are more than welcome!

In the (abstract) RecyclerView.Adapter<Viewholder> class I changed the code as follows:

@Override
public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
 
final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_day, parent, false);
 
EntryHolder holder = new EntryHolder(view);
 onViewCreated
(view, parent);
 
return holder;
}
public abstract void onViewCreated(View view, ViewGroup parent);

The owning Fragment/Activity then creates a new Adapter and implements onViewCreated - below example is based on a Fragment:

public class SomeFragment extends Fragment {
 
private Subscription recyclerViewSubscription;


 
@Override
 
public void onViewCreated(View view, Bundle savedInstanceState) {
 
super.onViewCreated(view, savedInstanceState);
 
 adapter
= new EntryAdapter() {
 
@Override
 
public void onViewCreated(final View view, ViewGroup parent) {

 recyclerViewSubscription
= RxView.clicks(view)
 
.takeUntil(RxView.detaches(parent))
 
.map(new Func1<Void, View>() {
 
@Override
 
public View call(Void aVoid) {
 
return view;

 
}
 
})
 
.subscribe(new Action1<View>() {
 
@Override
 
public void call(View view) {

 
int position = recyclerView.getChildAdapterPosition(view);
 
Log.i(TAG, "User clicked on item at position " + position);
 
}
 
});
 
}
 
};
 
//Set the adapter on the recyclerview...
 
}

 
@Override
 
public void onDestroyView() {
 
super.onDestroyView();
 
if (recyclerViewSubscription != null) {
 recyclerViewSubscription
.unsubscribe();
 
}
 recyclerView
.setAdapter(null);
 
}
}

I just started using RxJava today - so like I said: Might not be very elegant and I am looking for a better solution.

Agent Knopf

unread,
Nov 29, 2015, 5:59:46 PM11/29/15
to RxJava

P.s. i guess one way to get rid of that onViewCreatedCallback would be, if the recycler view posts to an event bus once the view has been created and the fragment creates the subscribe to onclick in the method that gets notified by the event bus. But iam guessing there is a more straight forward way involving only rxjava.

Joshua Pierce

unread,
Dec 22, 2015, 10:12:39 AM12/22/15
to RxJava
PublishSubject is a subclass of Observable and implements Observer, so that part hasn't changed (https://github.com/ReactiveX/RxJava/blob/1.x/src/main/java/rx/subjects/Subject.java).

As for clickEvents(), I don't see an obvious analog in the newest version of RxAndroid.  It seems strange that there's no onClickEvent that passes in the clicked view.  I will investigate!

Joshua Pierce

unread,
Dec 27, 2015, 10:42:01 AM12/27/15
to RxJava
Well it looks like you are on the right track with  mapping in the view, as stated in the commit history of RxBinding (https://github.com/JakeWharton/RxBinding/commit/fb98d4362a3ea594981cb87cde189403639c0abf).  But in my opinion it's easier to use the PublishSubject in the adapter as stated earlier:

private PublishSubject<View> clickSubject = PublishSubject.create();
public Observable<View> getItemClickSignal() {
   
return clickSubject;
}

@Override
public InstallationViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

   
final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_installation, parent, false);

   
RxView.clickEvents(view)

           
.takeUntil(RxView.detaches(parent))
           
.map(new Func1<Void, View>() {
               
@Override

               
public View call(Void empty) {
                   
return view;
Reply all
Reply to author
Forward
0 new messages