Epoxy: What's the equivalent of getAdapterPosition? (How to add listeners to models)

Created on 11 Oct 2016  Â·  10Comments  Â·  Source: airbnb/epoxy

I'm referring to https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ViewHolder.html#getAdapterPosition().

In particular, the readme suggests we should save click listeners as fields to reuse with each model. How do you get a reference to the model from a re-usable click listener?

Most helpful comment

There isn't one right way, it depends on what data you need and personal preference. It's very flexible, you would just set up the interface you want. This example doesn't reference the model directly in the click listener, it just needs to know about what data the model is based on. Inside the model the listener is set to hash=false so that changing the listener doesn't trigger an update. This is safe since any click listener will still point to the same saveVoteForCurrentUser method.

models.add(new WLVotingRowModel_()
                        .upVoteCount(listing.getUpVotes().size())
                        .downVoteCount(listing.getDownVotes().size())
                        .id(listing.getId())
                        .listener(new WLVotingClickListener() {
                            @Override
                            public void onVoteChanged(Vote vote) {
                                saveVoteForCurrentUser(listing, vote);
                            }

                            @Override
                            public void onVoteCountClicked() {
                                adapterInterface.showVotesForListing(listing);
                            }
                        }));

If you want the listener to be a reusable and you can define a custom interface like this

   public interface OnModelClickListener {
        void onModelClicked(EpoxyModel model);
    }

    private final OnModelClickListener modelClickListener = model -> {

    };

    new MyModel_().clickListener(modelClickListener);

    public class MyModel extends EpoxyModel<View> {
        @EpoxyAttribute(hash=false) OnModelClickListener clickListener;

        @Override
        public void bind(View view) {
            super.bind(view);
            view.setOnClickListener(v -> clickListener.onModelClicked(MyModel.this));

        }

        @Override
        protected int getDefaultLayout() {
            return 0;
        }
    }

In my experience it is best to make specific interfaces or listeners for the model or data you need. It is very flexible and use case specific, that is why we don't provide a general solution.

Does that make sense?

All 10 comments

There is no direct equivalent. getAdapterPosition is used on the view holder, which doesn't apply with Epoxy. What's the context of how you want to use it. Do you have a code sample?

You can also not have a reusable listener and do @EpoxyAttribute(hash=false) so that the changing the listener doesn't affect the model's diff state

What's the recommended way to access the model from a click listener?

On Tue, Oct 11, 2016, 12:13 AM Eli Hart [email protected] wrote:

There is no direct equivalent. getAdapterPosition is used on the view
holder, which doesn't apply with Epoxy. What's the context of how you want
to use it. Do you have a code sample?

You can also not have a reusable listener and do
@EpoxyAttribute(hash=false) so that the changing the listener doesn't
affect the model's diff state

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/airbnb/epoxy/issues/60#issuecomment-252807111, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAjAhqmDJYfc_KLihBHiAPub3RCdDbS4ks5qywz8gaJpZM4KTNvj
.

There isn't one right way, it depends on what data you need and personal preference. It's very flexible, you would just set up the interface you want. This example doesn't reference the model directly in the click listener, it just needs to know about what data the model is based on. Inside the model the listener is set to hash=false so that changing the listener doesn't trigger an update. This is safe since any click listener will still point to the same saveVoteForCurrentUser method.

models.add(new WLVotingRowModel_()
                        .upVoteCount(listing.getUpVotes().size())
                        .downVoteCount(listing.getDownVotes().size())
                        .id(listing.getId())
                        .listener(new WLVotingClickListener() {
                            @Override
                            public void onVoteChanged(Vote vote) {
                                saveVoteForCurrentUser(listing, vote);
                            }

                            @Override
                            public void onVoteCountClicked() {
                                adapterInterface.showVotesForListing(listing);
                            }
                        }));

If you want the listener to be a reusable and you can define a custom interface like this

   public interface OnModelClickListener {
        void onModelClicked(EpoxyModel model);
    }

    private final OnModelClickListener modelClickListener = model -> {

    };

    new MyModel_().clickListener(modelClickListener);

    public class MyModel extends EpoxyModel<View> {
        @EpoxyAttribute(hash=false) OnModelClickListener clickListener;

        @Override
        public void bind(View view) {
            super.bind(view);
            view.setOnClickListener(v -> clickListener.onModelClicked(MyModel.this));

        }

        @Override
        protected int getDefaultLayout() {
            return 0;
        }
    }

In my experience it is best to make specific interfaces or listeners for the model or data you need. It is very flexible and use case specific, that is why we don't provide a general solution.

Does that make sense?

Closing for now. Feel free to open up again if you have more questions

@elihart from your comment, do you think getting data from an edit text like this is a good approach -

In my Epoxy Adapter -

public interface OnEditTextListener {
        void onTextEntered(String text);
    }

    private final OnEditTextListener listener = new OnEditTextListener() {
        @Override
        public void onTextEntered(String text) {
            Timber.d(text);
        }
    };
EditTextModel editTextModel = new EditTextModel();
editTextModel.setEditTextListener(listener);

Then in the EditTextModel -

@EpoxyAttribute(hash = false)
InputFieldAdapter.OnEditTextListener editTextListener;

@Override
    public void bind(EditTextHolder holder) {
        super.bind(holder);
        holder.textInputEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                editTextListener.onTextEntered(s.toString());
            }
        });
    }

Also how to maintain a clean adapter if we follow this. Suppose i have one edit text, one radio, one checkbox field then should i define all the listeners like this in the same adapter?

Edit - Just gave this a rethought, this seems like a good use case for a event bus. Instead of passing listener from adapter to model, maybe we can pass event directly to our fragment from the Model

What you have definitely works. If all you need is the string that was entered you could skip the interface and just pass the text watcher from the adapter to the model directly. If you need other information about the model then the interface is necessary.

I think passing the event directly to the fragment is a good approach. In my opinion an event bus is a bit heavy handed for that since it exposes the events globally. What I like to do is to have an interface in the fragment or adapter that defines all the callbacks the fragment will need to know about. Something like:

public interface MyAdapterInterface {
void onSearchTextEntered(String text);
void onSearchPressed();
void onSavedSearchClicked(SavedSearch search);
}

The fragment would implement this interface and pass it to the adapter. If your models are very specific to your fragment you could pass the interface directly to the model, otherwise if they are generic models you can wrap the interface in a click listener

models.add(new TextInputModel_()
.hint(...)
.onEnter(v -> fragmentInterface.onSearchTextEntered(((EditText)v).getText())));

There's a lot of options here and not a one size fits all solution. Let me know if you have more questions or specific examples.

I will try to update the sample app with examples of different ways to use listeners with models.

@elihart thank you for your response. I went with the event bus since there a bunch of things i'm updating across fragments when text is entered. I have already implemented radio, checkbox, expandable items. Just a bit crunched on time right now. Will definitely send in a pull request to add more examples to the sample app.

Great, hope you can get everything working without much trouble. let me know if you have any other questions on best practices to get your adapter set up.

A PR to add examples to the sample app would be much appreciated :)

@elihart How to get the position of the item clicked with the PagedListEpoxyController<T>? The position i am getting with the OnModelClickListener<T extends EpoxyModel<?>, V> is sometimes wrong. Any suggestions?

@doomtrooper By its nature, the paging library doesn't necessarily have all items loaded. The adapter position is only for items loaded into the recyclerview. It might be more accurate if you use placeholders, but even then if you start at the end of the list the paged list isn't going to load the beginning. Best bet is probably to include the item position as a prop on the model if you really need the position in the paged list

Was this page helpful?
0 / 5 - 0 ratings