12

This will be theoretical question.

As everyone we use RecyclerView in many parts of the app. Sometimes RecyclerView contains different items, not only image for example, but ads, hints etc. And that's why we can use getViewType() method in Adapter.

But problem occurs when we have many viewTypes and binding this in Adapter is not elegant. So here is the question, is it nice and good pattern to bind data in ViewHolder?

Let's say we have list of apps.

Every app has name for simplicity. Our ViewHolder looks like this:

class AppViewHolder extends RecyclerView.ViewHolder {

     public TextView nameText;

     AppViewHolder(View itemView) {
          super(itemView)
          nameText = (TextView) itemView.findViewById(R.id.text_name);
     }
}

Now we could add bind method:

public void bind(App app) {
     nameText.setText(app.getName());
}

Is it good pattern?

Another solution would be using ViewModel. Because we have different items in RecyclerView, our Adapter could contain list of class which is base class for every ViewModel.

So basic class is:

class RecyclerViewItem {}

Now class which is ViewModel for App.

class AppRecyclerViewItem extends RecyclerViewItem {

     App app;

     ...
}

and our Adapter just has list of RecyclerViewItems:

class Adapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    List<RecyclerViewItem> items;

    ...
}

So with this approach (I mean using ViewModel) is it better to add bind method in ViewHolder, or in ViewModel?

Nominalista
  • 4,632
  • 11
  • 43
  • 102

3 Answers3

9

I would say that none of your solutions is good. First one is good only for items, that appear only once in adapter. Creating a method to fill row inside ViewHolder is not a good solution as different type of ViewHolders would grow and your RecyclerView.Adapter would gain more and more lines.

Second method is also not good. Model is the model, should only contain data and not business logic of application.

I would suggest a solution to keep RecyclerView.Adapter clean and pass logic of creating ViewHolder and filling them with data to delegates, that would be registered in ie. RecyclerView.Adapter constructor. This technique is explained more here

This way, registering even over a doven of different ViewHolder does not expecially add boilerplate to RecyclerView.Adapter, but delegates the logic of creating and filling RecyclerView.Adapter rows to those delegates.

But that is just a suggestion.

R. Zagórski
  • 20,020
  • 5
  • 65
  • 90
  • I think first method is good especially because in recyclerview onBindViewHolder we call bind in viewholder, so adapter won't gain many lines. But I like the idea you suggested, I will dive into it. – Nominalista Oct 12 '16 at 10:53
  • 2
    Are you using this approach at your work? Can you share your experience? – Nominalista Oct 12 '16 at 11:18
  • 1
    Can you explain this: "First one is good only for items, that appear only once in adapter." – Hamy May 24 '20 at 15:49
0

For my apps I've been using something like this:

public abstract class BindableViewHolder<T> extends RecyclerView.ViewHolder {
  public BindableViewHolder(View itemView) {
      super(itemView);
  }

  public abstract void bind(T thing);
}

The idea is to delegate the implementation of binding to the ViewHolder. This solution is meant for cases when you want to bind the same kind of object to different viewholders, but I think it's easily expandable to more general cases.

Here's an example of a really simple adapter. I think it explains itself, hope it helps!

public class ShowsAdapter extends RecyclerView.Adapter<BindableViewHolder<Show>> {

  private final DataProvider<Show> showsProvider;

  public ShowsAdapter(DataProvider<Show> showsProvider) {
      this.showsProvider = showsProvider;
  }

  @Override
  public BindableViewHolder<Show> onCreateViewHolder(ViewGroup parent, int viewType) {
      return ShowViewHolder.create(parent);
  }

  @Override
  public void onBindViewHolder(BindableViewHolder<Show> holder, int position) {
      holder.bind(showsProvider.get(position));
  }

  @Override
  public int getItemCount() {
      return showsProvider.size();
  }
}

Here, ShowViewHolder is a concrete subclass of BindableViewHolder,

Pato94
  • 784
  • 5
  • 6
0

There is great solution using delegates - http://hannesdorfmann.com/android/adapter-delegates and it can be even used for DataBinding (with simple changes, https://github.com/drstranges/DataBinding_For_RecyclerView). In this way data binded not in ViewHolder and not in RecyclerView Adapter but ItemDelegate do this, so your code remains clear and readable.

Following this approach all work with view-types delegated to the DelegateManager:

public abstract class AbsDelegationAdapter<T> extends RecyclerView.Adapter {

protected AdapterDelegatesManager<T> delegatesManager;
protected T items;
//...

    @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return delegatesManager.onCreateViewHolder(parent, viewType);
    }

    @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        delegatesManager.onBindViewHolder(items, position, holder);
    }

    @Override public int getItemViewType(int position) {
        return delegatesManager.getItemViewType(items, position);
    }
}

And your code will look like this:

mAdapter = new BindableAdapter<>(
        new UserDelegate(actionHandler), // you can create custom delegate
        //new ModelActionItemDelegate<BaseModel>(actionHandler, User.class, R.layout.item_user, BR.user), // or use generic
        new AnotherItemDelegate());

Where ItemDelegate can look like this:

public class UserDelegate extends BaseAdapterDelegate<BaseModel, ItemUserBinding> {

    @Override
    public boolean isForViewType(@NonNull final List<BaseModel> items, final int position) {
        return items.get(position) instanceof User;
    }

    @NonNull
    @Override
    public BindingHolder<ItemUserBinding> onCreateViewHolder(final ViewGroup parent) {
        // create ViewHolder
        return BindingHolder.newInstance(R.layout.item_user, LayoutInflater.from(parent.getContext()), parent, false);
    }

    @Override
    public void onBindViewHolder(@NonNull final List<BaseModel> items, final int position, @NonNull final BindingHolder<ItemUserBinding> holder) {
        // Just bind data
        final User user = (User) items.get(position);
        holder.getBinding().setUser(user);
    }

}
Roman_D
  • 4,680
  • 2
  • 14
  • 17