ReactiveCircus/FlowBinding

Question: Handling clicks in RecyclerView Adapter

ThanosFisherman opened this issue · 7 comments

Hello,

I was wondering How can I handle clicks from a view inside a RecyclerView Adapter and later collect them outside this adapter for instance in a Fragment/Activity? I'm probably looking for something like PublishSubject in RxJava

For instance

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DiscoverHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.section_title, parent, false)
        view.clicks().collect() //How can I somehow propagate those clicks to a property?
        ..............
    }

I used to do this with callbacks but I want to achieve a similar thing using this library.

The equivalence of RxJava Subject in Coroutines is Channel.

You could use a Channel in your adapter to collect the view.clicks() Flows emitted by each item, and expose it as a single itemClicks: Flow<MyItem> for Fragment / Activity to consume:

class MyListAdapter(
    private val coroutineScope: CoroutineScope // pass in the `lifecycleScope` from Activity / Fragment
) : ListAdapter<MyItem, MyViewHolder>(diffCallback) {

    private val itemClicksChannel: Channel<MyItem> = Channel(Channel.UNLIMITED)
    val itemClicks: Flow<MyItem> = itemClicksChannel.consumeAsFlow()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        ...
        return MyViewHolder(view, itemClicksChannel, coroutineScope)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
}

class MyViewHolder(
    private val view: View,
    private val itemClicksChannel: Channel<MyItem>,
    private val coroutineScope: CoroutineScope
) : RecyclerView.ViewHolder(view) {

    fun bind(myItem: MyItem) {
        ....
        view.clicks().onEach {
            clickChannel.offer(myItem)
        }.launchIn(coroutineScope)
    }
}

In Activity / Fragment:

...
myAdapter = MyListAdapter(lifecycleScope)
myAdapter.itemClicks.onEach { myItem ->
    // handle clicked item
}.launchIn(lifecycleScope)

Thank you that's exactly what I needed.

Passing lifecycleScope in MyListAdapter, then re-emitting clicks in holder through channel and lastly converting the channel to Flow was something I never thought of until now.

I must admit the whole thing is a bit verbose compared to the equivalent of rxjava though.

Thanks again!

How would you do the same with RxJava? I’d imagine the RxJava equivalence basically uses a PublishSubject instead of channel, and exposed an Observable instead of Flow. Are there any other differences?

Yes exactly. Now that I'm thinking of it there is not much difference whatsoever. If anything, in RxJava you wouldn't collect (subscribe) the events from clicks() in order to offer them again. Instead you'd just do this

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

where mViewClickSubject is a PublishSubject exposed as Observable

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

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

Which is exactly the same thing but maybe you save a few curly braces along the way I guess :)

Reference: https://stackoverflow.com/questions/36497690/how-to-handle-item-clicks-for-a-recycler-view-using-rxjava

Oh now I remember Subject is both a Subscriber and an ObservableSource:)

You can implement a similar API with Flow with an extension function:

fun <T> Flow<T>.transferToChannel(channel: Channel<T>): Flow<T> = onEach {
    channel.offer(it)
}

itemView.clicks().map { myItem }
            .transferToChannel(itemClicksChannel)
            .launchIn(coroutineScope)

Only saved a few characters though:)

It's good enough for me. I'm pretty new to coroutines and these little snippets help me better understand how things work :)

Cool. Let me know if you have more questions. Closing now.