Tel +49 (30) 814504070

Dennis Bläsing
17.02.2012 09:07 Uhr

Intelligente Android REST Autocompletion

Tags:

Eine Autocompletion erleichtert dem Nutzer die Eingabe von z.B. Suchbegriffen enorm. Gefüttert wird sie indes über lokal vorliegende statische Daten, Datenbankergebnisse oder auch über eine REST API. Ich möchte euch in diesem Artikel eine Variante einer Autocompletion vorstellen, die speziell auf ein System mit REST Backend zugeschnitten ist.

Android bietet von Hause aus schon alle nötigen Komponenten um sehr einfache bis hin zu komplexen Autocompletions zu ermöglichen, indem es dem Entwickler die Wahl der Datenherkunft lässt.

Ein Beispielprojekt gibt es auf Github1), Link gibts am Ende des Artikels.

Autocompletion auf Android

Das zentrale Element ist ein View namens AutoCompleteTextView

<AutoCompleteTextView
     android:id="@+id/simple_rest_autocompletion"
     android:layout_height="wrap_content"
     android:layout_width="fill_parent"
     android:completionThreshold="3"
     android:inputType="textImeMultiLine"
     />

Zwei Attribute haben besondere Relevanz:

android:completionThreshold

Dieses Attribut besagt ab welcher Anzahl von Zeichen die Autocompletion starten soll.

android:inputType

Der inputType textImeMultiLine sorgt dafür, dass unser AutoCompleteTextView in der Größe die wir definert haben bleibt und nicht ungewollt in der Zeile umbricht und unser Layout kaputt macht.

Bestandsaufnahme

Der Lebenszyklus unserer Autocompletion wird drei Basis Komponenten umfassen:

  • Ein Adapter, der die Daten entgegen nimmt und dafür sorgt, dass sie der AutoCompleteTextView bekommt, verwaltet ebenfalls die Items Views.
  • Ein TextWatcher, der die Key Events entgegen nimmt und dafür sorgt, dass die Daten zur richtigen Zeit geladen und an den Adapter weitergegeben werden.
  • Ein ItemClickListener, der die Logik für den Klick auf ein Item in der Liste mit Vorschlägen enthält.

Adapter

Wie schon erwähnt wird unser Adapter die Aufgabe haben unseren AutoCompleteTextView mit Daten zu versorgen. In unserer Ausführung wird er pro Item aus der Trefferliste gleich einen fertig zusammen gestellten View zurückgeben.

Für unseren Zweck benutzen wir ein ArrayAdapter. Wir erstellen also eine neue Java Datei und in ihr unsere Adapter Definition:

public class MyAutocompleteAdapter<T> extends ArrayAdapter<Item> implements Filterable {

Neben des erweiterns der ArrayAdapter Klasse implementieren wir ebenfalls das Filterable Interface, welches uns ermöglichen wird unseren eigenen Filter für den AutoCompletion Prozess zu definieren.

Der übergebene Typ definiert in welchem Format die einzelnen Einträge im Adapter gespeichert werden. Falls ihr komplexere Objekte für eure Daten benötigt möchtet ihr dort vielleicht einen selbst erstellen Typ eintragen.

Unser Konstruktor sieht folgendermaßen aus:

    private int mLayoutResourceID;
 
    public MyAutocompleteAdapter(Context context, int textViewResourceId) {
        super(context, textViewResourceId);
        mLayoutResourceID = textViewResourceId;
    }

Item View initialisierung

Als nächstes müssen wir die getView Methode überschreiben, damit wir einen eigenen View für die Listen Einträge zur Verfügung stellen können.

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if(convertView == null) {
            LayoutInflater inflater = LayoutInflater.from(getContext());
            convertView = inflater.inflate(mLayoutResourceID, parent, false);
        }
 
         // View initialisieren, Daten in TextViews, Images, etc
 
        return convertView;
    }

In dieser Methode initialisieren wir unseren View, aus Performance Gründen übergibt uns Android dafür einen recycelten View. Falls es keinen recyclebaren View gibt, laden wir ihn einfach neu mit Hilfe eines LayoutInflaters.

Danach können beliebige View Methoden auf dem Objekt ausgeführt werden, wie z.B. findViewById().

Das Filterable Interface

Jetzt steht nur noch die Implementierung der nötigen Interface Methoden aus. Hierfür definieren wir ein Filter Objekt, welches später bei Bedarf mit Logik befüllt werden kann. Es ist wichtig wenigtens einen Platzhalter zu definieren, da die Autocompletion sonst nicht reibungslos funktioniert.

    private Filter filter = new Filter() {
 
        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            FilterResults filterResults = new FilterResults();
            if (constraint != null) filterResults.count = getCount();
 
            // Bei Bedarf hier Filter Logik implementieren
 
            return filterResults;
        }
 
        @Override
        protected void publishResults(CharSequence contraint, FilterResults results) {
            if (results != null && results.count > 0) {
                notifyDataSetChanged();
            }
        }
    };

Fehlen tut uns für unseren Adapter jetzt noch lediglich eine Methode:

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

Damit ist unser eigener Adapter vollständig. :-)

Der TextWatcher

Ein TextWatcher ist (wie der Name schon sagt ;-)) dafür zuständig Textveränderungen zu registrieren und basierend auf denen zu handeln. Dieser TextWatcher wird unser zentrales Element, in dem wir die Aufrufe an unsere REST API stellen.

Das TextWatcher Interface verpflichtet uns dazu drei Methoden zu implementieren, namentlich sind das beforeTextChanged(), onTextChanged() sowie afterTextChanged().

Benötigen werden wir hiervon lediglich die onTextChanged() Methode, die Logik der beiden anderen Methoden kann leer gelassen werden.

Im Wandel des Textes

Soweit sogut, wenn sich also ein Text ändert soll die onTextChanged() Methode aufgerufen werden. Doch fangen wir oben an!

public class MyTextWatcher implements TextWatcher {
    private AutoCompleteTextView mTextview;
    private final MyAutocompleteAdapter<T> mAdapter;
 
    public TLFTextWatcher(AutoCompleteTextView autocompleteTextview, MyAutocompleteAdapter<T> adapter) {
        this.mTextview = autocompleteTextview;
        this.mAdapter = adapter;
    }    

Wir definieren uns eine Klasse die das TextWatcher Interface implementiert, wir werden im Zuge der Entwicklung eine Referenz auf unseren autoCompleteTextView sowie unseren Adapter benötigen.

Ausstehend sind jetzt lediglich noch die zu implementierenden Interface Methoden.

    public void beforeTextChanged(CharSequence sequence, int start, int count, int after) {
        // empty implementation because we dont need to do anything here
    }
 
    public void onTextChanged(CharSequence currentSearchString, int start, int before, int count) {
        if(currentSearchString.length() < mTextview.getThreshold()) return;
        if(mTextView.isPerformingCompletion()) return;
 
         // Do REST API calls here
         // Fill Adapter with new data
    }
 
    public void afterTextChanged(Editable editable) {
        // empty implementation because we dont need to do anything here.
    }

In der onTextChanged Methode werden wir die zukünftigen API Calls ausführen und müssen danach lediglich unseren Adapter mit den neuen Werten befüllen.

Und es hat Klick gemacht..

Wir haben jetzt also unser XML Fundament mit unserem AutoCompleteTextview und haben uns einen Adapter gebaut um die Liste mit Daten zu füllen, doch was passiert wenn wir auf einen Eintrag in der Liste klicken? Hier kommt ein guter alter OnItemClickListener zum Einsatz. Die Benutzung ist, wie bei allen ClickListenern, denkbar einfach.

public class MyAutoCompleteItemClickListener implements AdapterView.OnItemCLickListener {
    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
       // do magic here
    }
}

Die Zusammenführung

Schlussendlich müssen wie die Komponenten nur noch zusammenführen indem wir, z.B. in der onCreate Methode einer Activity, unseren AutoCompleteTextView initialisieren. Das könnte z.B. so aussehen:

  AutoCompleteTextView autoCompleteTextView = (AutoCompleteTextView) findViewById(R.id.my_autocomplete_textview);
 
  final MyAutoCompleteAdapter<T> autocompleteAdapter = new MyAutoCompleteAdapter<T>(this, R.layout.my_autocomplete_item);
  autocompleteAdapter.setNotifyOnChange(true);
 
  // The items will be fetched from the API in the textChangeListener
  autoCompleteTextView.addTextChangedListener(new MyTextWatcher(autoCompleteTextView, autocompleteAdapter));
  autoCompleteTextView.setOnItemClickListener(new MyAutoCompleteItemClickListener());
  autoCompleteTextView.setAdapter(autocompleteAdapter);

Und unsere eigene AutoCompletion mit REST Anbindung ist fertig, wäre da nicht..

... die Sache mit dem Timing

Der obige Code funktioniert wie erwartet, jedoch hat er einen Makel: bei jedem Tastendruck über dem Threshold wird unser API Call ausgeführt. Das ist natürlich nicht nur in Situationen, wo man nur eine begrenzte Internet Leitung zur Verfügung hat, ungünstig.

Wie schaffen wir es also, die Ausführung unseres API Calls bis die Eingabe des Suchbegriffes beendet ist zu verzögern?

Timer to the rescue

Folgender Ablauf soll realisiert werden:

Android bietet uns für diesen Ablauf einen einfachen und zuverlässigen Timer, den CountDownTimer.

Auswirkungen auf den Code haben die Änderungen lediglich für unseren TextWatcher.

public class MyTextWatcher implements TextWatcher {
    private final AutoCompleteTextView mTextview;
    private final MyAutocompleteAdapter<T> mAdapter;
 
    private CharSequence mLastSearchString;
    private static final int AUTOCOMPLETION_DELAY = 600; // in milliseconds
    private final CountDownTimer mTimer;
 
 
    public TLFTextWatcher(AutoCompleteTextView autocompleteTextview, MyAutocompleteAdapter<T> adapter) {
        this.mTextview = autocompleteTextview;
        this.mAdapter = adapter;
 
        mTimer = getAutoCompletionTimer();
    }
 
    private CountDownTimer getAutoCompletionTimer() {
        return new CountDownTimer(AUTOCOMPLETION_DELAY, AUTOCOMPLETION_DELAY) {
            @Override
            public void onTick(long l) {
            }
 
            @Override
            public void onFinish() {
                if(mLastSearchString.length() > 0) {
                   // Do the API call here
                }
            }
        };
    }
 
    @Override
    public void beforeTextChanged(CharSequence sequence, int start, int count, int after) {
        // empty implementation because we dont need to do anything here
    }
 
    @Override
    public void onTextChanged(CharSequence currentSearchString, int start, int before, int count) {
        if(currentSearchString.length() < mTextView.getThreshold()) {
            cancelCountDownTimer();
            if(!mAdapter.isEmpty()) mAdapter.clear();
            return;
        }
 
        // Remove the API call from here
 
        mLastSearchString = currentSearchString;
        resetCountDownTimer();
    }
 
    @Override
    public void afterTextChanged(Editable editable) {
        // empty implementation because we dont need to do anything here.
    }
 
     /**
     * Resets the AutoCompletion Timer
     */
    private void resetCountDownTimer() {
        cancelCountDownTimer();
        mTimer.start();
    }
 
    /**
     * Cancels the AutoCompletions Timer
     */
    private void cancelCountDownTimer() {
        mTimer.cancel();
    }
}

Besonders vorzuheben ist hier die Konstante AUTOCOMPLETION_DELAY, diese ist nämlich dafür zuständig wie lange dem Nutzer Zeit gegeben wird den nächsten Buchstaben zu klicken bzw die Verzögerung unseres API Calls. Der von mir gewählte Wert wird warscheinlich in vielen Situationen noch Fein Tuning bedürfen.

Jetzt besitzt unsere Autocompletion in der darauf geachtet wird, die Autocompletion nicht auszuführen bevor der Nutzer den Suchstring den er im Kopf hatte nicht zuende geschrieben hat.

Ich hoffe dieser Beitrag konnte euch die Entwicklung einer intelligenten Autocompletion mit komplexer Datenanbindung näher bringen, ohne über die üblichen Fallen zu stolpern. :-)

Mit Dank an Kai!

Bookmark and Share

Kommentare

Über CosmoCode

CosmoCode ist ein Berliner IT-Dienstleister mit starkem Fokus auf Webapplikationen. Zu unseren Schwerpunkten gehören dabei Content Management Systeme, Wikis und Individuallösungen.

Abonnieren

Abonnieren Sie mögen unser Blog? Bleiben Sie informiert mit RSS.
Freie Stelle: Fachinformatiker Freie Stelle: Fachinformatiker