Implementing Tweet Search Suggestions in Loklak Wok Android
Loklak Wok Android not only is a peer harvester for Loklak Server but it also provides users to search tweets using Loklak’s API endpoints. To provide a better search tweet search experience to the users, the app provides search suggestions using suggest API endpoint. The blog describes how “Search Suggestions” is implemented.
Third Party Libraries used to Implement Suggestion Feature
- Retrofit2: Used for sending network request
- Gson: Used for serialization, JSON to POJOs (Plain old java objects).
- RxJava and RxAndroid: Used to implement a clean asynchronous workflow.
- Retrolambda: Provides support for lambdas in Android.
These libraries can be installed by adding the following dependencies in app/build.gradle
android { …. // removes rxjava file repetations packagingOptions { exclude 'META-INF/rxjava.properties' } } dependencies { // gson and retrofit2 compile 'com.google.code.gson:gson:2.8.1' compile 'com.squareup.retrofit2:retrofit:2.3.0' compile 'com.squareup.retrofit2:converter-gson:2.3.0' compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' // rxjava and rxandroid compile 'io.reactivex.rxjava2:rxjava:2.0.5' compile 'io.reactivex.rxjava2:rxandroid:2.0.1' compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0' }
To add retrolambda
// in project's build.gradle dependencies { … classpath 'me.tatarka:gradle-retrolambda:3.2.0' } // in app level build.gradle at the top apply plugin: 'me.tatarka.retrolambda'
Fetching Suggestions
Retrofit2 sends a GET request to search API endpoint, the JSON response returned is serialized to Java Objects using the models defined in models.suggest package. The models can be easily generated using JSONSchema2Pojo. The benefit of using Gson is that, the hard work of parsing JSON is easily handled by it. The static method createRestClient creates the retrofit instance to be used for network calls
private static void createRestClient() { sRetrofit = new Retrofit.Builder() .baseUrl(BASE_URL) // base url : https://api.loklak.org/api/ .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); }
The suggest endpoint is defined in LoklakApi interface
public interface LoklakApi { @GET("/api/suggest.json") Observable<SuggestData> getSuggestions(@Query("q") String query); @GET("/api/suggest.json") Observable<SuggestData> getSuggestions(@Query("q") String query, @Query("count") int count); …. }
Now, the suggestions are obtained using fetchSuggestion method. First, it creates the rest client to send network requests using createApi method (which internally calls creteRestClient implemented above). The suggestion query is obtained from the EditText. Then the RxJava Observable is subscribed in a separate thread which is specially meant for doing IO operations and finally the obtained data is observed i.e. views are inflated in the MainUI thread.
private void fetchSuggestion() { LoklakApi loklakApi = RestClient.createApi(LoklakApi.class); // rest client created String query = tweetSearchEditText.getText().toString(); // suggestion query from EditText Observable<SuggestData> suggestionObservable = loklakApi.getSuggestions(query); // observable created Disposable disposable = suggestionObservable .subscribeOn(Schedulers.io()) // subscribed on IO thread .observeOn(AndroidSchedulers.mainThread()) // observed on MainUI thread .subscribe(this::onSuccessfulRequest, this::onFailedRequest); // views are manipulated accordingly mCompositeDisposable.add(disposable); }
If the network request is successful onSuccessfulRequest method is called which updates the data in the RecyclerView.
private void onSuccessfulRequest(SuggestData suggestData) { if (suggestData != null) { mSuggestAdapter.setQueries(suggestData.getQueries()); // data updated. } setAfterRefreshingState(); }
If the network request fails then onFailedRequest is called which displays a toast saying “Cannot fetch suggestions, Try Again!”. If requests are sent simultaneously and they fail, the previous message i.e. the previous toast is removed.
private void onFailedRequest(Throwable throwable) { Log.e(LOG_TAG, throwable.toString()); if (mToast != null) { // checks if a previous toast is present mToast.cancel(); // removes the previous toast. } setAfterRefreshingState(); // value of networkRequestError: "Cannot fetch suggestions, Try Again!" mToast = Toast.makeText(getActivity(), networkRequestError, Toast.LENGTH_SHORT); // toast is crated mToast.show(); // toast is displayed }
Lively Updating suggestions
One way to update suggestions as the user types in, is to send a GET request with a query parameter to suggest API endpoint and check if a previous request is incomplete cancel it. This includes a lot of IO work and seems unnecessary because we would be sending request even if the user hasn’t completed typing what he/she wants to search. One probable solution is to use a Timer.
private TextWatcher searchTextWatcher = new TextWatcher() { @Override public void afterTextChanged(Editable arg0) { // user typed: start the timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { String query = editText.getText() // send network request to get suggestions } }, 600); // 600ms delay before the timer executes the „run" method from TimerTask } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // user is typing: reset already started timer (if existing) if (timer != null) { timer.cancel(); } } };
This one looks good and eventually gets the job done. But, don’t you think for a simple stuff like this we are writing too much of code that is scary at the first sight.
Let’s try a second approach by using RxBinding for Android views and RxJava operators. RxBinding simply converts every event on the view to an Observable which can be subscribed and worked upon.
In place of TextWatcher here we use RxTextView.textChanges method that takes an EditText as a parameter. textChanges creates Observable of character sequences for text changes on view, here EditText. Then debounce operator of RxJava is used which drops observables till the timeout expires, a clean alternative for Timer. The second approach is implemented in updateSuggestions.
private void updateSuggestions() { Disposable disposable = RxTextView.textChanges(tweetSearchEditText) // generating observables for text changes .debounce(400, TimeUnit.MILLISECONDS) // droping observables i.e. text changes within 4 ms .subscribe(charSequence -> { if (charSequence.length() > 0) { fetchSuggestion(); // suggestions obtained } }); mCompositeDisposable.add(disposable); }
CompositeDisposable is a bucket that contains Disposable objects, which are returned each time an Observable is subscribed to. So, all the disposable objects are collected in CompositeDisposable and unsubscribed when onStop of the fragment is called to avoid memory leaks.
A SwipeRefreshLayout is used, so that user can retry if the request fails or refresh to get new suggestions. When refreshing is not complete a circular ProgressDialog is shown and the RecyclerView showing old suggestions is made invisible, executed by setBeforeRefreshingState method
private void setBeforeRefreshingState() { refreshSuggestions.setRefreshing(true); tweetSearchSuggestions.setVisibility(View.INVISIBLE); }
Similarly, once refreshing is done ProgessDialog is stopped and the visibility of RecyclerView which now contains the updated suggestions is changed to VISIBLE. This is executed in setAfterRefreshingState method
private void setAfterRefreshingState() { refreshSuggestions.setRefreshing(false); tweetSearchSuggestions.setVisibility(View.VISIBLE); }
References:
- Retrofit2: http://square.github.io/retrofit/
- RxJava: https://github.com/ReactiveX/RxJava/wiki
- Retrolambda: https://github.com/orfjackal/retrolambda
- RxBinding: https://github.com/JakeWharton/RxBinding
- TextWatcher and Timer: https://futurestud.io/tutorials/android-how-to-delay-changedtextevent-on-androids-edittext
- Debounce operator: http://reactivex.io/documentation/operators/debounce.html
You must be logged in to post a comment.