In this blog post I describe how the GlobalSearchAdapter in Open Event Android was made which enabled users to search quickly within the app. This post also outlines how to create Recycler Views with heterogenous layouts and explains how to write ViewHolders.
Adapter Logic
A custom adapter was built for the population of views in the Recycler View in the SearchActivity.
private List<Object> filteredResultList = new ArrayList<>(); //ViewType Constants private final int TRACK = 0; private final int SPEAKER = 2; private final int LOCATION = 3; private final int DIVIDER = 4;
The DIVIDER constant was assigned to the Result Type Header View.
In a gist all the item types such as Speaker, Track, Location, Divider etc have been designated some constants.
Getting the ItemViewType
@Override public int getItemViewType(int position) { if(filteredResultList.get(position) instanceof Track){ return TRACK; } else if(filteredResultList.get(position) instanceof String){ return DIVIDER; } ...Similarly for other ItemTypes such as Session or Location else{ return 1; } }
As the filteredResultList is of type Object we can insert objects of any type into the list as Object is a superclass of all classes. We would want a view which represents a TRACK if we have an object of type Track in the filteredResultList. And similarly for the other result types we could insert objects of type LOCATION, SPEAKER types in this list. getItemViewType() basically determines the type of the item that is visible to us. If the list consists of an item of type SPEAKER, in the RecyclerView.
Code for onCreateViewHolder in GlobalSearchAdapter for the Recycler View
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { RecyclerView.ViewHolder resultHolder = null; LayoutInflater inflater = LayoutInflater.from(parent.getContext()); switch(viewType) { case TRACK: View track = inflater.inflate(R.layout.item_track, parent, false); resultHolder = new TrackViewHolder(track,context); break; case SPEAKER: View speaker = inflater.inflate(R.layout.search_item_speaker, parent, false); resultHolder = new SpeakerViewHolder(speaker,context); break; //Similarly for other types default: break; } return resultHolder; }
Depending upon the the viewType returned the desired layout is inflated and the desired ViewHolder is returned.
Code for onBindViewHolder in GlobalSearchAdapter for the Recycler View
@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (holder.getItemViewType()){ case TRACK: TrackViewHolder trackSearchHolder = (TrackViewHolder)holder; final Track currentTrack = (Track)getItem(position); trackSearchHolder.setTrack(currentTrack); trackSearchHolder.bindHolder(); break; //Similarly for all the other View Types default: break; } }
These functions are being used to bind the data to the layouts that have been inflated already in the earlier snippet of code of onCreateViewHolder.
The bindHolder functions of each ViewHolder type are being used to do the view binding i.e converting the information in the Object Track into what we see in the TrackViewHolder as seen in TrackViewFormat.
All ViewHolders have been defined as separate classes in order to enable re usability of these classes.
ViewHolder Implementation
There are 4 main ViewHolders that were made to enable such a search. I’ll be talking about the TrackViewHolder in detail.
public class TrackViewHolder extends RecyclerView.ViewHolder { @BindView(R.id.imageView) ImageView trackImageIcon; @BindView(R.id.track_title) TextView trackTitle; @BindView(R.id.track_description) TextView trackDescription; private Track currentTrack; private Context context; private TextDrawable.IBuilder drawableBuilder = TextDrawable.builder().round(); public void setTrack(Track track) { this.currentTrack = track; } public TrackViewHolder(View itemView,Context context) { super(itemView); ButterKnife.bind(this, itemView); this.context = context; } public void bindHolder(){ //Set all Views to their correct configurations itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(context, TrackSessionsActivity.class); intent.putExtra(ConstantStrings.TRACK, currentTrack.getName()); // Send Track ID to Activity to leverage color cache intent.putExtra(ConstantStrings.TRACK_ID, currentTrack.getId()); context.startActivity(intent); } }); } }
Those @BindView annotations that we can see are the result of a library called as Butterknife which is used to reduce standard boilerplate findViewById lines.
@BindView(R.id.imageView) ImageView trackImageIcon;
IS THE SAME AS THIS
ImageView trackImageIcon = (ImageView)findViewById(R.id.imageView);
The advantage of such a ViewHolder is that it knows what kind of data it stores as compared to traditional ViewHolders which do not know the kind of data it stores.
By making ViewHolders separate from the RecyclerViewAdapter we are essentially decoupling classes and are enabling reusability of code. Also we make the ViewHolder a bit more intelligent by storing the object it binds in the ViewHolder itself. In the above example we are storing an object of Track which is bind to the ViewHolder. We also see that we do the view binding inside the viewholder itself. All this helps us to reduce code inside the adapter class.
A recent addition to the app was custom colors for all TRACKS in the app that improved the visual feel of the app. So basically, for example if a SESSION has been associated with the track of Blockchain it would be given a color such as purple. onClickListeners are also being set with some extras which are self-descriptive in nature. Similarly the other ViewHolders have been implemented.
Resources
- Heterogenous Recycler View : https://guides.codepath.com/android/Heterogenous-Layouts-inside-RecyclerView
- ViewHolder Pattern : https://willowtreeapps.com/ideas/android-fundamentals-working-with-the-recyclerview-adapter-and-viewholder-pattern/