getFilter() on a custom ArrayAdapter not working

3.9k views Asked by At

I am using a Custom ArrayAdapter to store User information for example sammy, robert, lizie are each one User objects and i am using a User type ArrayList to store all the User objects to ArrayList.

And because it is not a string or int (The ArrayList) the default getFilter does not work, and i have done my research but it is really confusing how the getFilter method works so i can modify myself.

I want to implement the searching based on the name property form the User class

I know i have to implement the Filterable interface in my CustomAdapter class, but the getFilter is really unintuitive.

Here is my CustomAdapter

class CustomArrayAdapter extends ArrayAdapter<User>  implements Filterable {


    CustomArrayAdapter(@NonNull Context context, ArrayList<User> users) {
        super(context, 0, users);
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {

        User innserUser = getItem(position);

        if (convertView == null){

            convertView = LayoutInflater.from(getContext()).inflate(R.layout.row_layout, parent, false);

        }

        TextView username = (TextView) convertView.findViewById(R.id.userNameContact);
        TextView userNumber = (TextView) convertView.findViewById(R.id.userNumberContact);
        ImageView userImage = (ImageView) convertView.findViewById(R.id.userImageContact);

        try {
            if(innserUser != null) {
                username.setText(innserUser.name);
                userNumber.setText(innserUser.number);
                userImage.setImageBitmap(innserUser.imageBitmap);
            }
        }catch (Exception e){
            e.printStackTrace();
        }

        return convertView;

    }


}

and here is the user class nothing special here

import android.graphics.Bitmap;

public class User {

    String id, name, number;
    Bitmap imageBitmap;

    User(String id, String name, String number, Bitmap imageBitmap){

        this.id = id;
        this.name = name;
        this.number = number;
        this.imageBitmap = imageBitmap;

    }
}

I tied alot of variations of the getFilter from many threads but none of them work for me ,and the one's with good explanations are for BaseAdapter not for ArrayAdapter

I have tried this question and i have tried this question but does not work for me.

I am new to android development field, and this seems particularly unintuitive. Any suggestions would be really appreciated, thank you.

EDIT 1: After the answer of jitesh mohite, Thanks for the replay jitesh mohite

class CustomArrayAdapter extends ArrayAdapter<User>  implements Filterable {


    ArrayList<User> users;

    CustomArrayAdapter(@NonNull Context context, ArrayList<User> users) {
        super(context, 0, users);
        this.users = users;
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {

        User innserUser = getItem(position);

        if (convertView == null){

            convertView = LayoutInflater.from(getContext()).inflate(R.layout.row_layout, parent, false);

        }

        TextView username = (TextView) convertView.findViewById(R.id.userNameContact);
        TextView userNumber = (TextView) convertView.findViewById(R.id.userNumberContact);
        ImageView userImage = (ImageView) convertView.findViewById(R.id.userImageContact);

        try {
            if(innserUser != null) {
                username.setText(innserUser.name);
                userNumber.setText(innserUser.number);
                userImage.setImageBitmap(innserUser.imageBitmap);
            }
        }catch (Exception e){
            e.printStackTrace();
        }

        return convertView;

    }


    Filter myFilter = new Filter() {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            FilterResults filterResults = new FilterResults();
            ArrayList<User> tempList=new ArrayList<User>();
            // Add the filter code here
            if(constraint != null && users != null) {
                int length= users.size();
                int i=0;
                while(i<length){
                    User item= users.get(i);
                    //do whatever you wanna do here
                    //adding result set output array

                    //item.name is user.name cause i want to search on name
                    if(item.name.toLowerCase().contains(constraint.toString().toLowerCase()) ) { // Add check here, and fill the tempList which shows as a result

                        tempList.add(item);
                    }

                    i++;
                }
                //following two lines is very important
                //as publish result can only take FilterResults users
                filterResults.values = tempList;
                filterResults.count = tempList.size();
            }
            return filterResults;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence contraint, FilterResults results) {
            users = (ArrayList<User>) results.values;
            if (results.count > 0) {
                notifyDataSetChanged();
            } else {
                notifyDataSetInvalidated();
            }
        }
    };

    @Override
    public Filter getFilter() {
        return myFilter;
    }


}

the search is not working on the customadapter still i think i am doing something wrong.

here i am typing something in the search bar but no filtering happens

enter image description here

and if you want to see the searchbar code its nothing special just the usual

@Override
public boolean onCreateOptionsMenu(Menu menu) {

    MenuInflater inflater = getMenuInflater();

    inflater.inflate(R.menu.search_box, menu);

    MenuItem item = menu.findItem(R.id.app_bar_search);

    SearchView searchView = (SearchView)item.getActionView();

    searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String query) {
            return false;
        }

        @Override
        public boolean onQueryTextChange(String newText) {

            customArrayAdapter.getFilter().filter(newText);

            return false;
        }
    });


    return true;



}
2

There are 2 answers

1
Mike M. On BEST ANSWER

ArrayAdapter's built-in Filter uses the toString() return from the model class (i.e., its type parameter) to perform its filtering comparisons. You don't necessarily need a custom Filter implementation if you're able to override User's toString() method to return what you want to compare (provided its filtering algorithm is suitable to your situation). In this case:

@Override
public String toString() {
    return name;
}

To be clear on exactly what that algorithm is, ArrayAdapter's default filtering goes as follows:

The filter String is first converted to lowercase. Then, looping over the dataset, each value's toString() return is converted to lowercase, and checked to see if it startsWith() the filter String. If so, it's added to the result set. If not, a second check is performed, whereby the value's lowercase String is split on a space (" "), and each value from that is compared to the filter, again using startsWith(). Basically, it first checks if the whole thing starts with the filter text, and then checks each word, if necessary.

If that's a suitable filter, then this solution is by far the simplest.


If that does not meet your needs, and you do actually need a custom Filter implementation, then you should just not use ArrayAdapter to begin with. ArrayAdapter maintains internal, private Lists for the original and filtered collections – initially populated from the collection passed in the constructor call – and you do not have access to those. This is why the custom Filter attempt shown does not work, as the displayed item count and the item returned from getItem(position) are coming from that internal filter List, not the one built in the custom Filter.

In that case, you should directly subclass BaseAdapter instead, maintaining your own Lists for the original and filtered collections. You can use ArrayAdapter's source as a guide.

Indeed, ArrayAdapter is often the wrong choice when choosing an Adapter to extend. ArrayAdapter is designed for a singular, somewhat simplistic goal: setting a flat String on a single TextView in each list item. There are several cases in which subclassing ArrayAdapter instead of BaseAdapter is rather pointless and/or redundant. For example:

  • Overriding getView() and not using the View returned from a call to super.getView().
  • Manually setting the text on the TextView yourself, for whatever reason.
  • Maintaining and using your own collections; i.e., the arrays, or Lists, or what have you.

In these and certain other cases, it's arguably better to use BaseAdapter from the start. Using ArrayAdapter for anything much more complex than single text items with basic functionality can quickly become cumbersome and error-prone, and is often more trouble than it's worth.


Lastly, I would mention that ListView is basically deprecated, at this point, though not yet officially, at the time of this writing. Current recommendations are to use RecyclerView instead. However, for those brand new to Android programming, ListView can still be useful as a beginning step in understanding the overall design of this type of recycling adapter View. RecyclerView can be a little overwhelming to start with.

7
Jitesh Mohite On
Filter myFilter = new Filter() {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
         FilterResults filterResults = new FilterResults();   
         ArrayList<User> tempList=new ArrayList<User>();
         // Add the filter code here
         if(constraint != null && users!=null) {
             int length= users.size();
             int i=0;
                while(i<length){
                    User item= users.get(i);
                    //do whatever you wanna do here
                    //adding result set output array    
                    if()  {  // Add check here, and fill the tempList which shows as a result

                    tempList.add(item);
                  }

                    i++;
                }
                //following two lines is very important
                //as publish result can only take FilterResults users
                filterResults.values = tempList;
                filterResults.count = tempList.size();
          }
          return filterResults;
      }

      @SuppressWarnings("unchecked")
      @Override
      protected void publishResults(CharSequence contraint, FilterResults results) {
          users = (ArrayList<User>) results.values;
          if (results.count > 0) {
           notifyDataSetChanged();
          } else {
              notifyDataSetInvalidated();
          }  
      }
     };

Lastly, Override this method and return filter instance.

@Override
     public Filter getFilter() {
        return myFilter;
    }

For more reference see https://gist.github.com/tobiasschuerg/3554252