Drebin is a binder framework for Android's RecyclerView. The name comes from the Lt. Frank Drebin. First, because Leslie Nielsen is godlike. And second, because Drebin is an anagram for Binder.

The binders are defined as stateless templates, can be easily injected if you are using a DI framework, and they are easy to composite.

Why

I built it to leave behind some of the problems I was facing with SmartAdapters. This could be considered the new version for it; I built it from scratch and they are completely different approaches, so it made sense to keep them separated.

The caveat I was finding when working with SmartAdapters was all the heavy lifting involved in working with BindableLayouts. As we were working with custom views, and didn't have access to the adapter, using an inflated layout was bothersome if you wanted to get rid of the 1st layer in the view hierarchy. So you had to do things like setting orientation in a LinearLayout programmatically, and working with <merge/> tags. Which is uncomfortable as hell.

With this approach the Binder, which roughly serves the same purpose as a BindableLayout, is apart from the view and is easily injectable. Also, it's easy to do composition between binders. And using BinderEnvironments is pretty flexible for things like multiple actions when tapping in different views. The environments and the selectors are easily injectable as well.

And we get rid of all the reflection magic of the builders, which is nice as well.

So, how?

The basics are: you create a binder for populating a View and you call the fluid api on the Drebin class.

So, the basic plumbing would be something like this:

    Drebin.with(getActivity())
        .items(mItems)
        .environment(mHasClickableChampion)
        .binder(ChampionRedux.class, mChampionBinder)
        .into(mRecyclerView);

The binder basic interface is like this:

public interface Binder<V extends View, VH extends ViewHost<V>, M, BE extends BinderEnvironment> {  
  ViewFactory<V> getViewFactory();
  VH createViewHost(V view);
  void bind(M model, VH viewHost, BE environment);
  void unbind(VH viewHost, BE environment);
}

An example implementation from SalsaLoL:

public class ChampionBinder implements Binder<LinearLayout, ChampionBinder.ChampionViewHost, ChampionBasic, HasClickableChampion> {

  public static final ViewFactory<LinearLayout> VIEW_FACTORY = ViewFactory.INFLATE.fromLayout(R.layout.exp_view_champion_redux);

  @Inject
  public ChampionBinder() {
  }

  @Override public ViewFactory<LinearLayout> getViewFactory() {
    return VIEW_FACTORY;
  }

  @Override public ChampionViewHost createViewHost(final LinearLayout view) {
    return new ChampionViewHost(view);
  }

  @Override public void bind(final ChampionBasic model, final ChampionViewHost viewHost, final HasClickableChampion binderContext) {
    viewHost.image.setImageURI(RelativeLinks.buildUri(model.getImage(), RelativeLinks.Source.CHAMPION));
    viewHost.title.setText(model.getName());
    viewHost.rootView.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(final View view) {
        binderContext.championClicked(model);
      }
    });
  }

  @Override public void unbind(final ChampionViewHost viewHost, final HasClickableChampion binderContext) {
    viewHost.rootView.setOnClickListener(null);
  }

  static class ChampionViewHost extends ViewHost<LinearLayout> {
    @Bind(R.id.default_title) TextView title;
    @Bind(R.id.default_image) SimpleDraweeView image;

    ChampionViewHost(final LinearLayout view) {
      super(view);
      ButterKnife.bind(this, view);
    }
  }
}

The ChampionViewHost in here is not really necessary, you could just use ViewHost directly instead. But you would have to access the view on the bind(...) method, which is pretty bothersome.

When you want to spice things up: BinderSelectors

In situations where you have different renderings for the same model object class, BinderSelectors come to the rescue. They will allow you to decide which Binder to be used for a particular model.

public interface BinderSelector<M, BE extends BinderEnvironment> {  
  Binder select(M model, BE environment);
}

An example from SalsaLoL, deciding between featured and normal renderings for the same video object:

public class CoverVideoBinderSelector implements BinderSelector<CoverItem.Video, BinderEnvironment> {

  @Inject CoverVideoBinder mCoverVideoBinder;
  @Inject CoverVideoFeaturedBinder mCoverVideoFeaturedBinder;

  @Inject
  public CoverVideoBinderSelector() {
  }

  @Override public Binder select(final CoverItem.Video item, final BinderEnvironment environment) {
    if (item.isFeatured()) {
      return mCoverVideoFeaturedBinder;
    }
    return mCoverVideoBinder;
  }
}

Being binders templates, how can we get to the complex stuff?

If you want to let the binder know about more stuff going on with your app apart from the model itself, you might want to create your own BinderEnvironment. It works both ways, from the app to the row being bound, and from the row being bound to the app.

The most typical case would be using the environment for receiving information triggered by user actions on your bound view, like a click or a long press.

An example interface could be:

public interface HasUsersEnvironment extends BinderEnvironment {  
  boolean isCurrentUser(User user);
  void userSelected(User user);
}

I usually build a lot of different traits and create a common implementation for a bunch of them at the same time.

An incomplete example, you can infer what the interfaces do easily:

public class CoverEnvironment implements BinderEnvironment, HasContext, HasClickableChampion, HasClickableGuide, HasClickableNews, HasClickableVideo {

  @Inject HasClickableChampionImpl mHasClickableChampionImpl;
  @Inject HasClickableGuideImpl mHasClickableGuideImpl;
  @Inject HasContextImpl mHasContextImpl;

  private Listener mListener;

  @Inject
  public CoverEnvironment() {

  }

  public void setListener(Listener listener) {
    mListener = listener;
    mHasClickableChampionImpl.setListener(listener);
    mHasClickableGuideImpl.setListener(listener);
  }

  @Override public void championClicked(final ChampionBasic champion) {
    mHasClickableChampionImpl.championClicked(champion);
  }

  @Override public void guideClicked(final GuideBasic guide) {
    mHasClickableGuideImpl.guideClicked(guide);
  }

  @Override public void newsClicked(final CoverItem.News news) {
    if (mListener != null) {
      mListener.newsClicked(news);
    }
  }

  @Override public void videoClicked(final CoverItem.Video video) {
    if (mListener != null) {
      mListener.videoClicked(video);
    }
  }

  @Override public Context getContext() {
    return mHasContextImpl.getContext();
  }

  public interface Listener extends HasClickableChampionImpl.OnChampionClickedListener, HasClickableGuideImpl.OnGuideClickedListener {
    void newsClicked(CoverItem.News news);

    void videoClicked(CoverItem.Video video);
  }
}

This way we can obtain things that we might need for the environment (like a particular context), or we can give back control to the activity/fragment after some user action in our items at the list (all the xxxClicked stuff).

So...

Well, just play around with it, use it if you see it fit. I am surely doing it!

Tags

android , drebin

About the author
comments powered by Disqus