Reputation: 717
I am trying to implement a search feature within my app where the user can search for different types of data: track, album, or artist. The user selects what type of data they'll be searching for in activity A and then the actual search is performed in activity B.
Within activity A:
Intent intent = new Intent(this, PostSearch.class);
intent.putExtra(PostSearch.EXTRA_POST_TYPE, postType);
startActivityForResult(intent, RC_NEW_POST);
EXTRA_POST_TYPE
specifies the kind of search that will be performed. This extra is then extracted within activity B. I have classes already defined for each data type: Track
, Album
, and Artist
. Within activity B, I have a RecyclerView
backed by an adapter. I was hoping to use the same adapter definition for each data type. However, I am not sure if this is possible.
I've defined my adapter as follows (shortened for brevity):
public class PostSearchAdapter extends RecyclerView.Adapter<PostSearchAdapter.PostSearchViewHolder> {
private Context mContext;
private int mSearchType;
private List<?> mSearchResults;
public PostSearchAdapter(Context context, int searchType) {
this.mContext = context;
mSearchType = searchType;
switch (mSearchType) {
case 0:
mSearchResults = new ArrayList<Track>();
break;
case 1:
mSearchResults = new ArrayList<Album>();
break;
case 2:
mSearchResults = new ArrayList<Album>();
break;
default:
Log.i(TAG, "Invalid search type for adapter. Uh oh...");
break;
}
}
@Override
public int getItemCount() {
if (mSearchResults == null) {
return 0;
} else {
return mSearchResults.size();
}
}
@NonNull
@Override
public PostSearchViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View searchResult = LayoutInflater
.from(parent.getContext())
.inflate(R.layout.list_item_search_result, parent, false);
return new PostSearchViewHolder(searchResult);
}
@Override
public void onBindViewHolder(PostSearchViewHolder viewHolder, int position) {
switch (mSearchType) {
case 0:
Track track = (Track) mSearchResults.get(position);
viewHolder.mTitle.setText(track.getTitle());
viewHolder.mArtist.setText(track.getArtistNames());
if (!TextUtils.isEmpty(track.getAlbum().getLargeAlbumCover())) {
Picasso.get()
.load(track.getAlbum().getLargeAlbumCover())
.error(R.drawable.ic_no_cover)
.into(viewHolder.mCover);
}
break;
case 1:
// Handle album results here
break;
case 2:
// Handle artist results here
break;
default:
break;
}
}
public void addData(List<?> newData) {
mSearchResults.add(newData);
notifyDataSetChanged();
}
public void clearData(){
mSearchResults.clear();
}
}
I've defined my adapter this way because when I instantiate it within activity B, I will know what type of data I'm dealing with from EXTRA_POST_TYPE
and can do something like:
mAdapter = new PostSearchAdapter(this, mSearchType);
...where mSearchType
has the value stored by EXTRA_POST_TYPE
. However, my issues now has to deal with mSearchResults
within my adapter. I've defined this member variable like this using the Java ?
wildcard type:
private List<?> mSearchResults;
I've done it this since I don't know what type of data the list will contain until the adapter is instantiated. I then instantiate mSearchResults
within the overriden constructor of the adapter since I now know what kind of data to put into the array based on the value of mSearchType
:
public PostSearchAdapter(Context context, int searchType) {
this.mContext = context;
mSearchType = searchType;
switch (mSearchType) {
//Instantiate mSearchResults here
}
}
There's 2 big issues that I'm facing with this. The first is that I still have to cast for the object that is being stored within the list in onBindViewHolder()
:
Track track = (Track) mSearchResults.get(position);
I know that this is a problem because one of the main advantages of using parameterized data types is so that we no longer have to do casting like so in the above. Second, I'm still getting a compile-time error within addData()
. Android Studio complains about something to with capture and the wildcard character:
I don't completely understand the error, which has left me at a standstill.
I should mention that I don't have the most thorough understanding of generics within Java, but I felt like the problem that I'm trying to solve could be accomplished by using them. Any sort of insight into this would be greatly appreciated!
Upvotes: 2
Views: 3806
Reputation: 17248
You are not properly using generics.
Does your Track
and Album
share a superclass or any interface?
If not create an interface and add it to both classes, otherwise proceed to use your superclass:
public interface ISearchResult{ }
Then change your adapter into an abstract
class, add proper type declaration and a template for ViewHolder
as well:
public abstract class PostSearchAdapter<T extends ISearchResult> extends RecyclerView.Adapter<PostSearchAdapter.PostSearchViewHolder> {
// note change from private to protected
protected final Context mContext;
protected final ArrayList<T> mSearchResults = new ArrayList<T>();
// constructor no longer handles weird type variable
public PostSearchAdapter(Context context) {
this.mContext = context;
}
// oncreate viewholder is removed on purpose
@Override
public void onBindViewHolder(PostSearchAdapter.PostSearchViewHolder viewHolder, int position) {
viewHolder.bind(mSearchResults.get(position));
}
public void addData(List<T> newData) {
mSearchResults.addAll(newData);
notifyDataSetChanged();
}
public void clearData(){
mSearchResults.clear();
}
@Override
public int getItemCount() {
return mSearchResults.size(); // this is never null
}
// ViewHolder template
protected abstract class PostSearchViewHolder extends RecyclerView.ViewHolder{
protected PostSearchViewHolder(View itemView) {
super(itemView);
// this can have some or all views that are shared between search results
}
protected abstract void bind(T data);
}
}
That takes care of basic methods. Now you need to create a subclass of this adapter for each search result type (note the type argument is used in extends):
public class TrackAdapter extends PostSearchAdapter<Track> {
public TrackAdapter(Context context) {
super(context);
}
@NonNull
@Override
public PostSearchViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// if all types share layout You can create helper method in superclass to inflate it
View searchResult = LayoutInflater
.from(parent.getContext())
.inflate(R.layout.list_item_search_result, parent, false);
return new TrackViewHolder(searchResult);
}
protected class TrackViewHolder extends PostSearchViewHolder{
protected TrackViewHolder(View itemView) {
super(itemView);
// store views not included in superclass in fields
}
@Override
protected void bind(Track data) {
// bind views to Track data...
}
}
}
Now that only leaves you with creation. Simple static factory method will take care of that instead, for example added to PostSearchAdapter
class:
public static PostSearchAdapter create(Context context, int resultType){
switch (resultType){
case 0:
return new TrackAdapter(context);
case 1:
return new AlbumAdapter(context);
default:
throw new IllegalArgumentException("Invalid resultType:"+resultType);
}
}
That lets you get proper type of your adapter with a simple call:
mAdapter = PostSearchAdapter.create(this, mSearchType);
Upvotes: 2
Reputation: 7479
EDIT
Not even adding elements 1 by 1 would work. The reason is explained thoroughly in the 2 links I attached, check them out. This doesn't work either:
List<?> list = new ArrayList<>();
List<?> list2 = new ArrayList<>();
list.add(list2.get(0)); // I thought for-each loop would work, this proves that it wouldn't
Because the types may not be the same. Not even getting an element from the same list works:
List<?> list = new ArrayList<>();
list.add(list.get(0)); // Same compile error
I thought using addAll
method would work. But it shows the same error.
This compiles, dunno if it works properly though:
List list = new ArrayList<>();
List<?> list2 = new ArrayList<>();
list.addAll(list2);
Upon digging a little deeper, I found this and this that may help us understand better.
Upvotes: 0