How do I Use AutoCompleteTextView and populate it with data from a web API?

AndroidNetworkingFilterAutocompletetextview

Android Problem Overview


I want to use an AutoCompleteTextView in my activity and populate the data as the user types by querying a web API. How do I go about doing this?

Do I create a new class and override AutoCompleteTextView.performFiltering, or do I use a custom list adapter and provide a custom android.widget.Filter that overrides performFiltering?

Or is there a better way to obtain my end goal?

I've done something somewhat similar, but it was for the Quick Search box and it involved implementing a service, but I believe that's not what I want to do here.

Android Solutions


Solution 1 - Android

I came up with a solution, I don't know if it is the best solution, but it appears to work very well. What I did was created a custom adapter that extends ArrayAdapter. In the custom adapter I overrode getFilter and created my own Filter class that overrides performFiltering. This starts a new thread so it doesn't interrupt the UI. Below is a barebones example.

MyActivity.java

public class MyActivity extends Activity {
    private AutoCompleteTextView style;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        style = (AutoCompleteTextView) findViewById(R.id.style);
        adapter = new AutoCompleteAdapter(this, android.R.layout.simple_dropdown_item_1line); 
        style.setAdapter(adapter);
    }
}

AutoCompleteAdapter.java

public class AutoCompleteAdapter extends ArrayAdapter<Style> implements Filterable {
    private ArrayList<Style> mData;

    public AutoCompleteAdapter(Context context, int textViewResourceId) {
        super(context, textViewResourceId);
        mData = new ArrayList<Style>();
    }

    @Override
    public int getCount() {
        return mData.size();
    }

    @Override
    public Style getItem(int index) {
        return mData.get(index);
    }

    @Override
    public Filter getFilter() {
        Filter myFilter = new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                FilterResults filterResults = new FilterResults();
                if(constraint != null) {
                    // A class that queries a web API, parses the data and returns an ArrayList<Style>
                    StyleFetcher fetcher = new StyleFetcher();
                    try {
                        mData = fetcher.retrieveResults(constraint.toString());
                    }
                    catch(Exception e) {
                        Log.e("myException", e.getMessage());
                    }
                    // Now assign the values and count to the FilterResults object
                    filterResults.values = mData;
                    filterResults.count = mData.size();
                }
                return filterResults;
            }

            @Override
            protected void publishResults(CharSequence contraint, FilterResults results) {
                if(results != null && results.count > 0) {
	            notifyDataSetChanged();
                }
                else {
                    notifyDataSetInvalidated();
                }
            }
        };
        return myFilter;
    }
}

Solution 2 - Android

Expanding on AJ.'s answer above, the following custom adapter includes the handling of the server requests and json parsing as well:

class AutoCompleteAdapter extends ArrayAdapter<String> implements Filterable
{
	private ArrayList<String> data;
	private final String server = "http://myserver/script.php?query=";

	AutoCompleteAdapter (@NonNull Context context, @LayoutRes int resource)
	{
		super (context, resource);
		this.data = new ArrayList<>();
	}

	@Override
	public int getCount()
	{
		return data.size();
	}

	@Nullable
	@Override
	public String getItem (int position)
	{
		return data.get (position);
	}

	@NonNull
	@Override
	public Filter getFilter()
	{
		return new Filter()
		{
			@Override
			protected FilterResults performFiltering (CharSequence constraint)
			{
				FilterResults results = new FilterResults();
				if (constraint != null)
				{
					HttpURLConnection conn = null;
                    InputStream input = null;
					try
					{
						URL url = new URL (server + constraint.toString());
						conn = (HttpURLConnection) url.openConnection();
						input = conn.getInputStream();
						InputStreamReader reader = new InputStreamReader (input, "UTF-8");
						BufferedReader buffer = new BufferedReader (reader, 8192);
						StringBuilder builder = new StringBuilder();
						String line;
						while ((line = buffer.readLine()) != null)
						{
							builder.append (line);
						}
						JSONArray terms = new JSONArray (builder.toString());
						ArrayList<String> suggestions = new ArrayList<>();
						for (int ind = 0; ind < terms.length(); ind++)
						{
							String term = terms.getString (ind);
							suggestions.add (term);
						}
						results.values = suggestions;
						results.count = suggestions.size();
						data = suggestions;
					}
					catch (Exception ex)
					{
						ex.printStackTrace();
					}
					finally
					{
						if (input != null)
						{
						    try
						    {
						        input.close();
						    }
						    catch (Exception ex)
						    {
						        ex.printStackTrace();
						    }
						}
						if (conn != null) conn.disconnect();
					}
				}
				return results;
			}

			@Override
			protected void publishResults (CharSequence constraint, FilterResults results)
			{
				if (results != null && results.count > 0)
				{
					notifyDataSetChanged();
				}
				else notifyDataSetInvalidated();
			}
		};
	}

and use it the same way:

public class MyActivity extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        AutoCompleteTextView textView = (AutoCompleteTextView) findViewById (R.id.style);
        int layout = android.R.layout.simple_list_item_1;
        AutoCompleteAdapter adapter = new AutoCompleteAdapter (this, layout); 
        textView.setAdapter (adapter);
    }
}

Solution 3 - Android

Chu: To customize how the view looks and get more control over unwrapping the object, do the following...

    @Override
    public View getView (int position, View convertView, ViewGroup parent) {
        TextView originalView = (TextView) super.getView(position, convertView, parent); // Get the original view

        final LayoutInflater inflater = LayoutInflater.from(getContext());
        final TextView view = (TextView) inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);

        // Start tweaking
        view.setText(originalView.getText());
        view.setTextColor(R.color.black);  // also useful if you have a color scheme that makes the text show up white
        view.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10); // override the text size
        return view;
    }

Solution 4 - Android

private AutoCompleteUserAdapter userAdapter;
private AutoCompleteTextView actvName;
private ArrayList<SearchUserItem> arrayList;

actvName = findViewById(R.id.actvName);

actvName.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        actvName.setText(userAdapter.getItemNameAtPosition(position));
        actvName.setSelection(actvName.getText().toString().trim().length());
    }
});

actvName.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(final CharSequence s, int start, int before, int count) {
        if (actvName.isPerformingCompletion()) {
            // An item has been selected from the list. Ignore.
        } else {
            if (s.toString().toLowerCase().trim().length() >= 2) {
                getUserList(s.toString().toLowerCase().trim());
            }
        }
    }

    @Override
    public void afterTextChanged(Editable s) {

    }
});

private void getUserList(String searchText) {
	//Add data to your list after success of API call
	arrayList = new ArrayList<>();
    arrayList.addAll(YOUR_LIST);
    userAdapter = new AutoCompleteUserAdapter(context, R.layout.row_user, arrayList);
    getActivity().runOnUiThread(new Runnable() {
        @Override
        public void run() {
            actvName.setAdapter(userAdapter);
            userAdapter.notifyDataSetChanged();
            actvName.showDropDown();
        }
    });        
}

AutoCompleteUserAdapter

/**
 * Created by Ketan Ramani on 11/07/2019.
 */
public class AutoCompleteUserAdapter extends ArrayAdapter<SearchUserItem> {

    private Context context;
    private int layoutResourceId;
    private ArrayList<SearchUserItem> arrayList;

    public AutoCompleteUserAdapter(Context context, int layoutResourceId, ArrayList<SearchUserItem> arrayList) {
        super(context, layoutResourceId, arrayList);
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.arrayList = arrayList;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        try {
            if (convertView == null) {
                convertView = LayoutInflater.from(parent.getContext()).inflate(layoutResourceId, parent, false);
            }

            SearchUserItem model = arrayList.get(position);

            AppCompatTextView tvUserName = convertView.findViewById(R.id.tvUserName);
            tvUserName.setText(model.getFullname());
        } catch (NullPointerException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return convertView;
    }

    public String getItemNameAtPosition(int position) {
        return arrayList.get(position).getName();
    }

    public String getItemIDAtPosition(int position) {
        return arrayList.get(position).getId();
    }
}

Solution 5 - Android

Here is a Kotlin version of the Adapter Class that loads data from a local database via Room:

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Filter
import android.widget.Filterable
import android.widget.TextView
import ...MyFinderDatabase
import ...R
import ...model.SearchResult

class SearchCompleteAdapter(context: Context, val resourceId: Int): ArrayAdapter<SearchResult>(context, resourceId), Filterable {
    private val results = mutableListOf<SearchResult>()

    override fun getCount() = results.size

    override fun getItem(position: Int) = results[position]

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view = convertView ?: LayoutInflater.from(context).inflate(resourceId, parent, false)
        val textView = view.findViewById<TextView>(R.id.autocomplete_name)
        textView.text = getItem(position).fullName
        return view
    }

    override fun getFilter() = object : Filter(){
        override fun performFiltering(constraint: CharSequence?): FilterResults {
            val filterResults = FilterResults()
            val db = MyRoomDatabase.getDatabase(context.applicationContext)
            val dbResults = db.resultDao().findWithNameLike(String.format("%%%s%%", constraint.toString()))
            filterResults.values = dbResults
            filterResults.count = dbResults.size
            results.clear()
            results.addAll(dbResults)
            return filterResults
        }

        override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
            if((results != null) && (results.count > 0)){
                notifyDataSetChanged()
            }
            else{
                notifyDataSetInvalidated()
            }
        }

        override fun convertResultToString(resultValue: Any?): CharSequence {
            val searchResult = resultValue as SearchResult
            return searchResult.fullName
        }
    }
}

DAO method definition:

    @Query("select * from SearchResult where full_name like :name and type = 'USER_TYPE'")
fun findWithNameLike(name: String): List<SearchResult>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAJ.View Question on Stackoverflow
Solution 1 - AndroidAJ.View Answer on Stackoverflow
Solution 2 - AndroidrazzView Answer on Stackoverflow
Solution 3 - AndroidDanielView Answer on Stackoverflow
Solution 4 - AndroidKetan RamaniView Answer on Stackoverflow
Solution 5 - AndroidInnovaView Answer on Stackoverflow