Smart Adapters Library
Android library project that intends to simplify the usage of Adapters for ListView/GridView and RecyclerView. You won't have to code any boring adapter again!
It helps to keep boilerplate to a minimum and adds the possibility of easily changing between BaseAdapter / RecyclerView.Adapter adapter styles without changing any code. It also allows painless usage of multiple models / view types for the same list or grid - just add the different mappings between model objects and view objects.
Formerly part of nl-toolkit.
Adding to your project
Add this to your dependencies:
compile 'io.nlopez.smartadapters:library:1.3.1'
Usage
Adapter creation
If we got the typical list with one object mapped to one view, for example Tweet -> TweetView, it's as simple as this:
SmartAdapter.items(myObjectList).map(Tweet.class, TweetView.class).into(myListView);
Note that we have to prepare a bit the view (TweetView in this case). Please read the "Preparing your view classes" section.
If we need to do a more complex list, with different models mapped to different views, we just have to add more map
calls. Here is an example:
SmartAdapter.items(myObjectList)
.map(Tweet.class, TweetView.class)
.map(String.class, ListHeaderView.class)
.map(User.class, UserView.class)
.into(myListView);
You can pass an AbsListView based control (ListView, GridView) or a RecyclerView to the into
methods. The class will use the appropriate adapter class depending on which control you pass in there.
We can change the items(...)
call for empty()
if we want an adapter initialized with an empty array.
The calls from before return the adapter, so if you want to store it for manipulating it afterwards you can do it. For example:
MultiAdapter adapter = SmartAdapter.empty().map(Tweet.class, TweetView.class).into(myListView);
// We can add more stuff. The list will update and refresh its views. adapter.addItems(moreItems);
// And delete it if we want! adapter.clearItems();
Preparing your view classes
All your view classes must implement the BindableLayout interface so we got a common entrypoint for binding the model data to the view.
If looks like a lot of work, but you have some already implemented base classes depending on which you want to use as your base View. You can take a look at some implementation examples on the sample project.
- BindableFrameLayout
- BindableLinearLayout (you have to implement getOrientation() here)
- BindableRelativeLayout
- BindableViewLayout for the adventurous not relying on ViewGroup
public class TweetView extends BindableFrameLayout<Tweet> {
// [...]
public TweetView(Context context) {
// This is the constructor that should be implemented, because it's the one used internally
// by the default builder.
super(context);
}
@Override
public int getLayoutId() {
// This is mandatory, and should return the id for the view layout of this view
return R.layout.view_tweet;
}
@Override
public void onViewInflated() {
// Here we should assign the views or use ButterKnife
ButterKnife.inject(this);
// Fixes some horizontal fill layout
setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
@Override
public void bind(Tweet tweet) {
// In here we assign the model information values to our view widgets
// Examples:
tweetText.setText(tweet.getText());
authorText.setText(tweet.getAuthor());
// and so on!
}
}
I use an Android Studio template for creating the BindableLayouts. You can find it here.
A nice side effect is that we can pretty much switch back and forth to using ListView or RecyclerView without having to change anything in these views. We also got some more granular control over the events for those view widgets. We could pretty much add different onClick events to the row, to some button inside of it, etc.
We can notify the listener with some specific calls to be able to handle the events wherever the adapter is being instantiated, instead of doing it all inside the view code (which would be pretty messy).
public void bind(MyModel model) {
// Set a global click handler
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
notifyItemAction(ROW_PRESSED);
}
}
);
favoriteButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
notifyItemAction(FAVORITE_PRESSED);
}
}
);
}
Assigning listeners
If we want to control any event in our view classes at the adapter level, we can do it. We just have to add a listener to the adapter, and it could be done when instantaiting it or after.
For example:
SmartAdapter.items(myObjectList)
.map(Tweet.class, TweetView.class)
.listener(myViewListener)
.into(myListView);
The listener would be like this:
myViewListener = new ViewEventListener<Tweet>() {
@Override
public void onViewEvent(int actionId, Tweet item, View view) {
// actionId would be any constant value you used in notifyItemAction.
}
}
;
Advanced usage with custom builders
If we want to display different cells depending on the data of a single model or something more convoluted, we can specify our own BindableLayoutBuilder interface for the classes to be instantiated. The library allows multiple views mapped to the same view object as long as you provide a specific implementation for the viewType method in a BindableLayoutBuilder.
Here we have an example of a custom BindableLayoutBuilder created for a hypothetical case where the view class depends on the values of the object. Ideally it's enough to just overwrite the viewType method and return values depending on the desired view class. Also, allowing multiple views for the same type of object has to return true in the allowsMultimapping method.
public class TweetBindableLayoutBuilder extends DefaultBindableLayoutBuilder {
@Override
public Class<? extends BindableLayout> viewType(@NonNull Object item, int position, @NonNull Mapper mapper) {
if (item instanceof Tweet) {
// All the multiple bindings must be dealt with here and NOT get into the fallback
Tweet tweet = (Tweet) item;
if (tweet.hasGallery()) {
return TweetGalleryView.class;
}
else if (tweet.hasImage()) {
return TweetImageView.class;
}
else if (tweet.hasEmbeds()) {
return TweetEmbedView.class;
}
else {
return TweetView.class;
}
}
// With this fallback we return control for all the other cases to be handled as the default use.
return super.viewType(item, position, mapper);
}
@Override
public boolean allowsMultimapping() {
return true;
}
}
We can add it to the adapter like this:
SmartAdapter.items(myObjectList)
.listener(myViewListener)
.map(Tweet.class, TweetView.class)
.map(Tweet.class, TweetGalleryView.class)
.map(Tweet.class, TweetImageView.class)
.map(Tweet.class, TweetEmbedView.class)
.map(OtherThing.class, OtherThingView.class)
.builder(new TweetBindableLayoutBuilder())
.into(myListView);
You have a working example of this particular case in the samples.
You can also check more examples on how to implement builders in the default builders included with the library.
The idea behind the builders is that you can, given the object and its class, create the view class by yourself and return to the adapter.
Common issues
If you are already using RecyclerView in your project and have problems compiling, you can try setting the transitive property to false:
compile ('io.nlopez.smartadapters:library:1.3.1') {
transitive = false
}
If your row doesn't fill horizontally your RecyclerView, you should specify the LayoutParams to MATCH_PARENT.
setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
Please be aware that the type of the LayoutParams might have to be changed depending on where you are inflating the view. If you got ClassCastException on this, you know you have to change the type of the LayoutParms here.
Contributing
Forks, patches and other feedback are welcome.
Creators
Nacho López - Github @mrmans0n - Twitter @mrmans0n - Blog nlopez.io