Smart Tech for a better Web

Android AutoComplete with JSON Data served by Struts 2

von

Recently I decided not to use Phonegap/Callback for my new Android app but to build it native. With IntelliJ the first steps were so straightforward and promising that I was excited to work in the Java language, which is more natural to me than JavaScript. So far I have not regret my decision, except that I currently do not know how I should bring my app to the iPhone. Anyway, as usual the pain comes with the first extraordinary requirement you have.

Requirement

jQuery provides me a nice looking autocomplete input field. I wanted something similar on my Android app. The data should come from my Struts 2 application, served as JSON. JSON, because other consumers might need that data too.

Androids AutoCompleteTextView

Android has already an AutoCompleteTextView:

<AutoCompleteTextView
        android:id="@+id/myautocomplete"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

But now the fun begins. Because there are two widely known strategies to fill it.

One is to use the ArrayAdapter. This is actually pretty nice already, if you have your data stored on your device. You can easily create an Array out of it and then everything works as expected. BUT if you try to load data from a server, this one will fail. Try to type quickly - it might happen that elements are already removed when Android tries to access them. This is the case when your server is not responding quickly enough. Using that approach, you have an application which might freeze at some time or fail completely.

The other approach is to use the CursorAdapter. As the name suggests, this approach is recommended if you are connecting to a database directly, because you are then able to work with the database cursor. As I cannot use SQL in my case, this one is not for me.

For both approaches are enough blog posts on the web.

Serverside: Struts and JJSON

I will keep this one pretty short. In my server I have used a Struts 2 action. This action selects some data and returns it as JSON via my own JJSON plugin. This pretty straightforward. Of course you can use the Struts 2 bundled JSON plugin if you prefer or any other web framework which is able to serve plain json.

Android: using the onTextChanged method with TextWatcher

I first tried to override the getFilter method in the ArrayAdapter class. This didn't work. It resulted in multiple request per keyboard click. It seems that every KEYUP and KEYDOWN event runs through this filter. Of course my server was not happy and my slow connection caused horrible problems on the Android side.

Then I went on with my own ArrayAdapter Implementation, but cut out everything which has do with the server. Probably you can use ArrayAdapter directly without using an extension, but in my case I needed to tweak something else, so it looks like:

public class MyAdapter<T> extends ArrayAdapter<MyPOJO> implements Filterable {
    public MyAdapter(Context context, int textViewResourceId) {
        super(context, textViewResourceId);
    }

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

    private Filter filter = new Filter() {
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            FilterResults filterResults = new FilterResults();
            if (constraint != null) {
                filterResults.count = getCount();
            }

            // do some other stuff

            return filterResults;
        }

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

So far so good - my filter is pretty simple. It is time to use it - but how? One could think you'll probably need to implement the onKey Listener. The direction is good and it will work on your Android Emulator probably. But when going to you device you will notice, that your requests are only sent when you hit the "back" key. Reason is, the onKey Listener does only work with hardware keyboards, not with the virtual keyboard on your device. The annoying thing is, the "back" key will still work - a source of confusion (and grief) for me.

The right way to implement it is to use the addTextChangedListener and add your own implementation of TextWatcher to it. Once the text has changed, your ArrayAdapter should be updated with new contents. This way you can use the standard behavior of AutoComplete with ArrayAdapter and just write the data loading yourself.

First we create the Adapter instance and add it to the Autocomplete view:

final MyAdapter<MyPOJO> adapter =
  new MyAdapter<MyPOJO>(this, android.R.layout.simple_spinner_dropdown_item);

AutoCompleteTextView textView =
  (AutoCompleteTextView) findViewById(R.id.myautocomplete);

textView.setAdapter(adapter);

As you can see, I have used my own Adapter I showed above. Now it is time to add the TextChangedListener:

textView.addTextChangedListener(new TextWatcher() {
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        // no need to do anything
    }

    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        if (((AutoCompleteTextView) textView).isPerformingCompletion()) {
            return;
        }
        if (charSequence.length() < 2) {
            return;
        }

        String query = charSequence.toString();
        adapter.clear();

        List<NameValuePair> data = new ArrayList<NameValuePair>();
        data.add(new BasicNameValuePair("keyword", query));

        String result = serverConnector.sendRequest(data, ServerConnector.SEARCH);
        result = result.substring(2, result.length() - 3);

        JSONDecoder decoder = new JSONDecoder(result);
        JSONObject value = (JSONObject) decoder.decode();
        Map<String, JSONValue> values = value.getValue();
        if (values.size() != 0) {
            // transform from json to your object
            adapter.add(myPOJO);
        }

    }

    public void afterTextChanged(Editable editable) { }
});

I need to explain: the easiest way to send my request is when text actually has been changed, and not "after". I get the search term as a CharSequence in line 6.

Look at line 7 then: once a user has chosen what he wants, the chosen text will be set into the textfield again. This will cause a new "TextChanged" event. Of course this probably leads to problems, because your label might differ from your possible search terms. With calling "isPerformingCompletion" you can check if TextView is going to complete and if the user has chosen a value or typed something.

At line 10 I would like to search only when the user has typed two characters.

Then I use the adapter for the first time in line 15: I remove all elements inside. When I do not find anything this is the expected result: nothing. From line 17 to line 29 I make up my request to the server (I wrote the ServerConnector class myself - it is a wrapper around the bundled Apache HttpClient). Finally in line 28 I add every found element to the adapter. This will then cause the Adapter to update the autocomplete list.

Things one should know with this approach

You could download the data to your device and get much faster results. Of course this is not always possible, then you should think about the chance if a time out occurs. Every time you type, a request will be send (of course with 2 letters or more only). One could improve my code with creating a new Thread which waits as long as the user has stopped typing for a second or something like that. Most people hit three of four keys very quickly, then they wait for a while. With the new "delaying" Thread only one request would be sent instead of four.

When a user typed a key, your interface is blocked until the result returns. It is bad, because it is blocked. It is good, because users cannot fire multiple requests while you are still performing the one. In my case it works, in yours probably not. But you can help yourself with creating a new Thread for the request.

Finally I would like to say I am glad about comments which improves my approach.

Tags: #Android #Apache Struts

Newsletter

ABMELDEN

BLOG-POST TEILEN

When you load these comments, you'll be connected to Disqus. Privacy Statement.