How to handle Item clicks for a recycler view using RxJava
Asked Answered
E

5

16

I was interested to find out what is the best way to respond to a item click of a recycler view.

Normally I would add a onclick() listener to the ViewHolder and pass back results to the activity/fragment through a interface.

I thought about adding a Observable in the onBindViewHolder but i do not want to create a new Observable for every item binding.

Epigenesis answered 8/4, 2016 at 10:45 Comment(1)
You can use a PublishSubject. Then you create it only once and you just emit data on it, while having subscribed only once.Melvin
A
15

You can use RxBinding and then create a subject inside of your adapter, then redirect all the events to that subject and just create a getter of the subject to act as an observable and finally just subscribe you on that observable.

private PublishSubject<View> mViewClickSubject = PublishSubject.create();

public Observable<View> getViewClickedObservable() {
    return mViewClickSubject.asObservable();
}

@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup pParent, int pViewType) {
    Context context = pParent.getContext();
    View view = (View) LayoutInflater.from(context).inflate(R.layout.your_item_layout, pParent, false);
    ViewHolder viewHolder = new ViewHolder(view);

    RxView.clicks(view)
            .takeUntil(RxView.detaches(pParent))
            .map(aVoid -> view)
            .subscribe(mViewClickSubject);

    return viewHolder;
}

An usage example could be:

mMyAdapter.getViewClickedObservable()
        .subscribe(view -> /* do the action. */);
Anaesthetize answered 10/10, 2016 at 16:10 Comment(6)
Where do you unsubscribe?Mach
Yo can handle that where you do the subscription. Probably using a Composi teSubscription if you have more than one subscription there.Anaesthetize
if u need only observable(in general way!) - u need to call mViewClickSubject.asObservable(). otherwise every subscriber can call onErro()(for example, which is not ok!Intercessor
@Anaesthetize What is the use of .takeUntil(RxView.detaches(pParent))? Any way once view gets recycled there will be no click emits, so do we have to stop the click stream once the view get detached from the parent?Crotchet
@RuwankaMadhushan most likely is in order to unsubscribe when the view is detached. This happens when the item is scrolled out of screen.Huoh
@Huoh That's true. But I have an issue with using this approach in fragments. And I couldn't get this takeUntil() work in that case, as in here. Is it enough to call onCompleted on onDetachedFromRecyclerView?Crotchet
T
0

Step 1: Move the business logic out of the activities to domain classes/services

Optional: Use https://github.com/roboguice/roboguice to easily wire up your services with each other.

Step 2: @Inject (or just set) your service into your adapter

Step 3: Grab https://github.com/JakeWharton/RxBinding and use the super powers in your adapter:

RxView.clicks(button).subscribe(new Action1<Void>() {
    @Override
    public void call(Void aVoid) {
        myCoolService.doStuff();
    }
});

Step 4: Get a runtime crash and learn how to deal with subscriptions

Step 5: PROFIT :)

Tybie answered 18/4, 2016 at 18:5 Comment(0)
M
0

I would suggest you to do with your initial aproach of an observable per element on click, but in order to avoid create a new observable every time you can just cache the items emitted the first time using cache.

      /**
 * Here we can prove how the first time the items are delayed 100 ms per item emitted but second time becuase it´s cached we dont have any delay since 
 * the item emitted are cached
 */
@Test
public void cacheObservable() {
    Integer[] numbers = {0, 1, 2, 3, 4, 5};

    Observable<Integer> observable = Observable.from(numbers)
                                               .doOnNext(number -> {
                                                   try {
                                                       Thread.sleep(100);
                                                   } catch (InterruptedException e) {
                                                       e.printStackTrace();
                                                   }
                                               })
                                               .cache();
    long time = System.currentTimeMillis();
    observable.subscribe(System.out::println);
    System.out.println("First time took:" + (System.currentTimeMillis() - time));
    time = System.currentTimeMillis();
    observable.subscribe(System.out::println);
    System.out.println("Second time took:" + (System.currentTimeMillis() - time));

}
Monterrey answered 19/4, 2016 at 8:31 Comment(0)
M
0

My solution was much like @epool 's except use EventBus model.

First, create a RxBus: RxBus.java

public class RxBus {
    private final Subject<Object, Object> _bus = new SerializedSubject<>(PublishSubject.create());
    public void send(Object o) { _bus.onNext(o); }
    public Observable<Object> toObserverable() { return _bus; }
    public boolean hasObservers() { return _bus.hasObservers(); }
}

Then, you have two way to use RxBus. Create your custom Application class with RxBus reference or create RxBus in Activity/Fragment then pass it to adapter. I'm use the first.

MyApp.java

public class MyApp extends Application {
    private static MyApp _instance;
    private RxBus _bus;
    public static MyApp get() {  return _instance; }

    @Override
    public void onCreate() {
        super.onCreate();
        _instance = this;
        _bus = new RxBus();
    }

    public RxBus bus() { return _bus; }
}

then use

MyApp.get().bus() 

to get RxBus instance.

The usage of RxBus in Adpater was like this:

public class MyRecyclerAdapter extends ... {
    private RxBus _bus;

    public MykRecyclerAdapter (...) {
        ....
        _bus = MyApp.get().bus();
    }

    public ViewHolder onCreateViewHolder (...) {
        _sub = RxView.longClicks(itemView)  // You can use setOnLongClickListener as the same
              .subscribe(aVoid -> {
                        if (_bus.hasObservers()) { _bus.send(new SomeEvent(...)); }
                    });      
    }
}

You can send any class with _bus.send(), which We will recieve in the Activity:

RxBus bus = MyApp.get().bus();  // get the same RxBus instance
_sub = bus.toObserverable()
            .subscribe(e -> doSomething((SomeEvent) e));

About unsubscribe.

In MyRecyclerAdapter call _sub.unsubscribe() in clearup() methods and call _sub.unsubscribe() in Activity's onDestory().

@Override
public void onDestroy() {
    super.onDestroy();
    if (_adapter != null) {
        _adapter.cleanup();
    }
    if (_sub != null) {
         _sub.unsubscribe()
    }
}
Mcmurry answered 27/10, 2016 at 7:50 Comment(0)
C
0

We generally need the Pojo/Model class from list on clicked index. I do it in following way:

1) Create a BaseRecyclerViewAdapter

abstract class BaseRecyclerViewAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private val clickListenerPublishSubject = PublishSubject.create<T>()

    fun observeClickListener(): Observable<T> {
        return clickListenerPublishSubject
    }

    fun performClick(t: T?) {
        t ?: return
        clickListenerPublishSubject.onNext(t)
    }
}

2) In any adapter (For example MyAdapter)

class MyAdapter(private val events: List<Event>, context: Context) : BaseRecyclerViewAdapter<Event>() {
    //... Other methods of RecyclerView
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
        if (holder is EventViewHolder) {
            holder.eventBinding?.eventVm = EventViewModel(events[position])

            holder.eventBinding?.root?.setOnClickListener({ _ ->
                // This notifies the subscribers
                performClick(events[position])
            })
        }
    }
}

3) Inside the Activity or Fragment where click listener is needed

myAdapter?.observeClickListener()
                ?.subscribe({ eventClicked ->
                    // do something with clicked item

                })
Cissy answered 23/2, 2018 at 12:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.