I have a SearchView icon on the Toolbar via onCreateOptionsMenu(Menu menu). A click on the icon creates an EditText line for the user to enter search inputs. I have a TextWatcher() attached to the EditText line so that when text is added to the line (afterTextChanged Editable s) a searchQuery method is run via the ViewModel that searches a Room database.
My problem is that the observer is firing upon creation even before the user enters any query. Using Toasts, I was able to confirm that the observer is running a blank query "%%" right when the SearchView icon is pressed by the user and before the user enters any search query. And this is happening even though the observer is set up in the afterTextChanged(). How do I get the ViewModel observer to only fire after text is entered by the user?
Activity
...
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.mainactiv_menu, menu);
searchItem = menu.findItem(R.id.action_search);
menu.findItem(R.id.action_search).setVisible(false);
if (cardsAdapter != null && cardsAdapter.getItemCount() > 0) {
menu.findItem(R.id.action_search).setVisible(true);
}
SearchManager searchManager = (SearchManager) MainActivity.this.getSystemService(Context.SEARCH_SERVICE);
if (searchItem != null) {
mSearchView = (SearchView) searchItem.getActionView();
if (mSearchView != null) {
mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
EditText mSearchEditText = mSearchView.findViewById(androidx.appcompat.R.id.search_src_text);
mSearchEditText.setInputType(android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
mSearchEditText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// not needed
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// not needed
}
#Override
public void afterTextChanged(Editable s) {
String queryText = "%" + s.toString() + "%";
mQuickcardViewModel.searchQuery(queryText).observe(MainActivity.this, searchCards -> {
searchList = searchCards;
if (!mSearchView.isIconified() && searchList.size() > 0) {
// do something
}
...
How do I get the ViewModel observer to only fire after text is entered by the user?
Use an if statement to see if the string is empty before observing:
#Override
public void afterTextChanged(Editable s) {
if (s.toString().length() > 0) {
// the rest of your code goes here, preferably with some modifications
}
}
Other changes that I would recommend include:
Adding some measure of "debounce". Given your search expression, you appear to be hitting a database in searchQuery(). If the user types 10 characters in rapid succession, you will make 10 queries, which will make performance poor. "Debounce" says "only query after the user has paused for a bit", such as 500ms without any further input.
Adding in smarts to cancel an outstanding query when needed. Suppose the user types a bit, and then the debounce period elapses. So, you fire off a query. Because of your (presumed) LIKE expression, your query is going to do a "table scan", examining every row of the table. That could be slow, and the user might start typing some more before that first query completes. You no longer need that query, as its results are wrong (it is for the previous search expression, not the current one). So, you need a way to cancel that work.
Dealing with configuration changes. You might argue that you are using a ViewModel and LiveData, which should handle that... and it does, but only if you use them properly. In your case, you appear to be throwing away that LiveData after observing it, and so on a configuration change, you will not be in position re-observe it. This is why a lot of samples focus on decoupling the "please do the background work" from "please give me the results of the background work". You could still have a searchQuery() method that you call, but it would return void. The results would get piped through some LiveData held by the ViewModel, one that you observe in onCreate(). That way, after a configuration change, you once again observe that LiveData, and you get the data as it was prior to that configuration change.
Using FTS. LIKE is slow. If you are going to do this a lot consider using FTS3/FTS4 in SQLite for full-text searching. If you are using Room, there is built-in support for this in Room 2.2.0.
Related
My current Android application allows users to search for content remotely.
e.g. The user is presented with an EditText which accepts their search strings and triggers a remote API call that returns results that match the entered text.
Worse case is that I simply add a TextWatcher and trigger an API call each time onTextChanged is called. This could be improved by forcing the user to enter at least N characters to search for before making the first API call.
The "Perfect" solution would have the following features:-
Once the user starts entering search string(s)
Periodically (every M milliseconds) consume the entire string(s) entered. Trigger an API call each time the period expires and the current user input is different to the previous user input.
[Is it possible to have a dynamic timeout related to the entered texts length? e.g while the text is "short" the API response size will be large and take longer to return and parse; As the search text gets longer the API response size will reduce along with "inflight" and parsing time]
When the user restarts typing into the EditText field restart the Periodic consumption of text.
Whenever the user presses the ENTER key trigger "final" API call, and stop monitoring user input into the EditText field.
Set a minimum length of text the user has to enter before an API call is triggered but combine this minimum length with an overriding Timeout value so that when the user wishes to search for a "short" text string they can.
I am sure that RxJava and or RxBindings can support the above requirements however so far I have failed to realise a workable solution.
My attempts include
private PublishSubject<String> publishSubject;
publishSubject = PublishSubject.create();
publishSubject.filter(text -> text.length() > 2)
.debounce(300, TimeUnit.MILLISECONDS)
.toFlowable(BackpressureStrategy.LATEST)
.subscribe(new Consumer<String>() {
#Override
public void accept(final String s) throws Exception {
Log.d(TAG, "accept() called with: s = [" + s + "]");
}
});
mEditText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
}
#Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
publishSubject.onNext(s.toString());
}
#Override
public void afterTextChanged(final Editable s) {
}
});
And this with RxBinding
RxTextView.textChanges(mEditText)
.debounce(500, TimeUnit.MILLISECONDS)
.subscribe(new Consumer<CharSequence>(){
#Override
public void accept(final CharSequence charSequence) throws Exception {
Log.d(TAG, "accept() called with: charSequence = [" + charSequence + "]");
}
});
Neither of which give me a conditional filter that combines entered text length and a Timeout value.
I've also replaced debounce with throttleLast and sample neither of which furnished the required solution.
Is it possible to achieve my required functionality?
DYNAMIC TIMEOUT
An acceptable solution would cope with the following three scenarios
i). The user wishes to search for the any word beginning with "P"
ii). The user wishes to search for any word beginning with "Pneumo"
iii). The user wishes to search for the word "Pneumonoultramicroscopicsilicovolcanoconiosis"
In all three scenarios as soon as the user types the letter "P" I will display a progress spinner (however no API call will be executed at this point). I would like to balance the need to give the user search feedback within a responsive UI against making "wasted" API calls over the network.
If I could rely on the user entering their search text then clicking the "Done" (or "Enter") key I could initiate the final API call immediately.
Scenario One
As the text entered by the user is short in length (e.g. 1 character long) My timeout value will be at its maximum value, This gives the user the opportunity to enter additional characters and saves "wasted API calls".
As the user wishes to search for the letter "P" alone, once the Max Timeout expires I will execute the API call and display the results.
This scenario gives the user the worst user experience as they have to wait for my Dynamic Timeout to expire and then wait for a Large API response to be returned and displayed. They will not see any intermediary search results.
Scenario Two
This scenario combines scenario one as I have no idea what the user is going to search for (or the search strings final length) if they type all 6 characters "quickly" I can execute one API call, however the slower they are entering the 6 characters will increase the chance of executing wasted API calls.
This scenario gives the user an improved user experience as they have to wait for my Dynamic Timeout to expire however they do have a chance of seeing intermediary search results. The API responses will be smaller than scenario one.
Scenario Three
This scenario combines scenario one and two as I have no idea what the user is going to search for (or the search strings final length) if they type all 45 characters "quickly" I can execute one API call (maybe!), however the slower they type the 45 characters will increase the chance of executing wasted API calls.
I'am not tied to any technology that delivers my desired solution. I believe Rx is the best approach I've identified so far.
Something like this should work (didn't really try it)
Single<String> firstTypeOnlyStream = RxTextView.textChanges(mEditText)
.skipInitialValue()
.map(CharSequence::toString)
.firstOrError();
Observable<CharSequence> restartTypingStream = RxTextView.textChanges(mEditText)
.filter(charSequence -> charSequence.length() == 0);
Single<String> latestTextStream = RxTextView.textChanges(mEditText)
.map(CharSequence::toString)
.firstOrError();
Observable<TextViewEditorActionEvent> enterStream =
RxTextView.editorActionEvents(mEditText, actionEvent -> actionEvent.actionId() == EditorInfo.IME_ACTION_DONE);
firstTypeOnlyStream
.flatMapObservable(__ ->
latestTextStream
.toObservable()
.doOnNext(text -> nextDelay = delayByLength(text.length()))
.repeatWhen(objectObservable -> objectObservable
.flatMap(o -> Observable.timer(nextDelay, TimeUnit.MILLISECONDS)))
.distinctUntilChanged()
.flatMap(text -> {
if (text.length() > MINIMUM_TEXT_LENGTH) {
return apiRequest(text);
} else {
return Observable.empty();
}
})
)
.takeUntil(restartTypingStream)
.repeat()
.takeUntil(enterStream)
.mergeWith(enterStream.flatMap(__ ->
latestTextStream.flatMapObservable(this::apiRequest)
))
.subscribe(requestResult -> {
//do your thing with each request result
});
The idea is to construct the stream based on sampling rather then the text changed events itself, based on your requirement to sample each X time.
The way I did it here, is to construct one stream (firstTypeOnlyStream for the initial triggering of the events (the first time user input text), this stream will start the entire processing stream with the first typing of the user, next, when this first trigger arrives, we will basically sample the edit text periodically using the latestTextStream. latestTextStream is not really a stream over time, but rather a sampling of the current state of the EditText using the InitialValueObservable property of RxBinding (it simply emits on subscription the current text on the EditText) in other words it's a fancy way to get current text on subscription, and it's equivalent to:
Observable.fromCallable(() -> mEditText.getText().toString());
next, for dynamic timeout/delay, we update the nextDelay based on the text length and using repeatWhen with timer to wait for the desired time. together with distinctUntilChanged, it should give the desired sampling based on text length. further on, we'll fire the request based on the text (if long enough).
Stop by Enter - use takeUntil with enterStream which will be triggered on Enter and it also will trigger the final query.
Restarting - when the user 'restarts' typing - i.e. text is empty, .takeUntil(restartTypingStream) + repeat() will stop the stream when empty string enter, and restarts it (resubscribe).
Well, you could use something like this:
RxSearch.fromSearchView(searchView)
.debounce(300, TimeUnit.MILLISECONDS)
.filter(item -> item.length() > 1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(query -> {
adapter.setNamesList(namesAPI.searchForName(query));
adapter.notifyDataSetChanged();
apiCallsTextView.setText("API CALLS: " + apiCalls++);
});
public class RxSearch {
public static Observable<String> fromSearchView(#NonNull final SearchView searchView) {
final BehaviorSubject<String> subject = BehaviorSubject.create("");
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
subject.onCompleted();
return true;
}
#Override
public boolean onQueryTextChange(String newText) {
if (!newText.isEmpty()) {
subject.onNext(newText);
}
return true;
}
});
return subject;
}
}
blog referencia
your query can be easily solved by using RxJava2 methods, before i post code i will add the steps of what i am doing.
add an PublishSubject that will take your inputs and add a filter to it which will check if the input is greater than two or not.
add debounce method so that all input events that are fired before 300ms are ignored and the final query which is fired after 300ms is taken into consideration.
now add a switchmap and add your network request event into it,
Subscribe you event.
The code is as follows :
subject = PublishSubject.create(); //add this inside your oncreate
getCompositeDisposable().add(subject
.doOnEach(stringNotification -> {
if(stringNotification.getValue().length() < 3) {
getMvpView().hideEditLoading();
getMvpView().onFieldError("minimum 3 characters required");
}
})
.debounce(300,
TimeUnit.MILLISECONDS)
.filter(s -> s.length() >= 3)
.switchMap(s -> getDataManager().getHosts(
getDataManager().getDeviceToken(),
s).subscribeOn(Schedulers.io()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(hostResponses -> {
getMvpView().hideEditLoading();
if (hostResponses.size() != 0) {
if (this.hostResponses != null)
this.hostResponses.clear();
this.hostResponses = hostResponses;
getMvpView().setHostView(getHosts(hostResponses));
} else {
getMvpView().onFieldError("No host found");
}
}, throwable -> {
getMvpView().hideEditLoading();
if (throwable instanceof HttpException) {
HttpException exception = (HttpException) throwable;
if (exception.code() == 401) {
getMvpView().onError(R.string.code_expired,
BaseUtils.TOKEN_EXPIRY_TAG);
}
}
})
);
this will be your textwatcher:
searchView.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
subject.onNext(charSequence.toString());
}
#Override
public void afterTextChanged(Editable editable) {
}
});
P.S. This is working for me!!
You might find what you need in the as operator. It takes an ObservableConverter which allows you to convert your source Observable into an arbitrary object. That object can be another Observable with arbitrarily complex behavior.
public class MyConverter implements ObservableConverter<Foo, Observable<Bar>> {
Observable<Bar> apply(Observable<Foo> upstream) {
final PublishSubject<Bar> downstream = PublishSubject.create();
// subscribe to upstream
// subscriber publishes to downstream according to your rules
return downstream;
}
}
Then use it like this:
someObservableOfFoo.as(new MyConverter())... // more operators
Edit: I think compose may be more paradigmatic. It's a less powerful version of as specifically for producing an Observable instead of any object. Usage is essentially the same. See this tutorial.
My app has a SearchView. When the user types in the SearchView the onQueryTextChange passes the query to the presenter and then it calls the API. I am using Retrofit and RxJava for the calls. The calls return a json file with the words containing what the user typed so far. The problem is that, if the user is fast to type letters and the network is slow sometimes the SearchView doesn't show the results based on all the typed letters but maybe up to the second last because the last call was quicker to get the results compared to the second last.
Example:
the user start typing:
"cou" -> make a call to the API (first call after 3 letters) -> start returnin values
"n" -> make a call -> start returning values
"t" -> make a call -> start returning values
"r" -> make a call (the connection is slow)
"y" -> make a call -> start returning values
-> "r" get the results finally and the returns them
public Observable<List<MyModel>> getValues(String query) {
return Observable.defer(() -> mNetworkService.getAPI()
.getValues(query)
.retry(2)
.onErrorReturn(e -> new ArrayList<>()));
}
The call is very simple and whenever I get an error I don't want to display anything.
Is there a way to solve that? Or maybe this is not the case to use reactive programming?
EDIT:
Just to make more clear, the flow is the following:
Activity that uses a custom search view (https://github.com/Mauker1/MaterialSearchView)
the custom searchview has a listener when the user starts typing. Once the user starts typing the activity calls the Presenter.
the presenter will subscribe an observable returned by the interactor:
presenter:
addSubscription(mInteractor.getValues(query)
.observeOn(mMainScheduler)
.subscribeOn(mIoScheduler)
.subscribe(data -> {
getMvpView().showValues(data);
}, e -> {
Log.e(TAG, e.getMessage());
}));
interactor:
public Observable<List<MyModel>> getValues(String query) {
return Observable.defer(() -> mNetworkService.getAPI()
.getValues(query)
.debounce(2, TimeUnit.SECONDS)
.retry(2)
.onErrorReturn(e -> new ArrayList<>()));
So now either I change the custom search view in a 'normal' searchview and then use RxBinding or maybe I should use an handler or something like that (but still struggling how to fit it in my architecture)
Firstly make your Searchview as Observable so that you can apply Rx operators.
To convert searchview into Observable
public static Observable<String> fromview(SearchView searchView) {
final PublishSubject<String> subject = PublishSubject.create();
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String s) {
subject.onComplete();
searchView.clearFocus(); //if you want to close keyboard
return false;
}
#Override
public boolean onQueryTextChange(String text) {
subject.onNext(text);
return false;
}
});
return subject;
}
private void observeSearchView() {
disposable = RxSearchObservable.fromview(binding.svTweet)
.debounce(300, TimeUnit.MILLISECONDS)
.filter(text -> !text.isEmpty() && text.length() >= 3)
.map(text -> text.toLowerCase().trim())
.distinctUntilChanged()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe()
}
You can apply filter, condition
RxJava debounce() operator to delay taking any action until the user pauses briefly.
Use of distinctUntilChanged() ensures that the user can search for the same thing twice, but not immediately back to back
The filter operator is used to filter the unwanted string like the empty string in this case to avoid the unnecessary network call.
Handling searchview withRXJava
You're in luck there's an operator for that called debounce
Observable.defer(() -> mNetworkService.getAPI()
.getValues(query)
.debounce(3, TimeUnit.SECONDS)
.retry(2)
.onErrorReturn(e -> new ArrayList<>()));
What debounce does is wait N time units for more results prior to continuing. Say for example the network takes 2 seconds to return and you flood it with request after request, debounce will wait for 3 seconds of no results and then return the last result. Think of it as dropping everything but the one before N time of inactivity.
This solve your problem but will still flood the network, ideally you would use the excellent RxBinding library do the defer prior to making the request something like:
RxTextView.textChanges(searchView)
.debounce(3, TimeUnit.SECONDS)
.map(input->mNetworkService.getAPI().getValues(input.queryText().toString()))
.retry(2)
.onErrorReturn(e -> new ArrayList<>()))
With the current setup it will wait 3 seconds after a user types something and only then make the network call. If instead they start typing something new, the first pending search request gets dropped.
Edit: changed to RxTextView.textChanges(textview) based on OP not using an android SearchView widget
Extending on what #MikeN said, if you want to only use the results of the LAST input, you should use switchMap() (which is flatMapLatest() in some other Rx implementations).
I solved the flooding issue without using RxBinding and I want to post my solution just in case someone else needs it.
So whenever the onTextChanged is called I check, first of all, if the size is > 2 and if it is connected to the network (boolean updated by a BroadcastReceiver). Then I create message to be sent has delayed and I delete all the other messages in the queue. This means that I will execute only the queries that are not within the specified delay:
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (TextUtils.getTrimmedLength(s) > 2 && isConnected) {
mHandler.removeMessages(QUERY_MESSAGE);
Message message = Message.obtain(mHandler, QUERY_MESSAGE, s.toString().trim());
mHandler.sendMessageDelayed(message, MESSAGE_DELAY_MILLIS);
}
}
Then the Handler:
private Handler mHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
if (msg.what == QUERY_MESSAGE) {
String query = (String)msg.obj;
mPresenter.getValues(query);
}
}
};
Add rxbinding dependency to gradle implementation "com.jakewharton.rxbinding2:rxbinding-kotlin:2.1.1"
Use debounce and distinct for ignoring frequent key input and duplicate input
Dispose previous API call for getting only latest search result
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.toolbar_menu, menu)
// Associate searchable configuration with the SearchView
val searchManager = requireContext().getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchView = menu.findItem(R.id.action_search).actionView as SearchView
searchView.setSearchableInfo(
searchManager.getSearchableInfo(requireActivity().componentName)
)
searchView.maxWidth = Integer.MAX_VALUE
// listening to search query text change
disposable = RxSearchView.queryTextChangeEvents(searchView)
.debounce(750, TimeUnit.MILLISECONDS)
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
callApi(it.queryText().toString())
}, {
Timber.e(it)
})
super.onCreateOptionsMenu(menu, inflater)
}
private fun callApi(query: String){
if(!apiDisposable.isDisposed){
apiDisposable.dispose()
}
apiDisposable = mNetworkService.getAPI(query)
}
I've looked over SO and cannot find a simple answer. Is there an advantage to using an Intent over an event listener on the search box for sending the text to the query event?
SearchView searchView =
(SearchView) MenuItemCompat.getActionView(searchMenuItem);
if(searchView != null){
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener(){
#Override
public boolean onQueryTextSubmit(String s) {
Toast.makeText(getApplicationContext(),
"String entered is " + s, Toast.LENGTH_SHORT).show();
return true;
}
#Override
public boolean onQueryTextChange(String s) {
return false;
}
});
}
Versus using:
private void handleIntent(Intent intent){
if(Intent.ACTION_SEARCH.equals(intent.getAction())){
String query = intent.getStringExtra(SearchManager.QUERY);
Log.d(LOG_TAG, "QUERY: " + query);
new FetchArtistTask().execute(query);
}
}
Advantages of Intent:
The Intent can be directed to other Activities besides one with SearchView;
You can have single Activity handle search requests from multiple other Activities;
You can make full use of different Activity start modes and task stack while handling the Intent;
In addition to using SearchView the search Intent can triggered outside of Activities (e.g. in Dialogs and PopupWindows) by using The Search Dialog, making searching experience in your application more unified;
You can make a search Intent yourself to send from Service, when user clicks Notification/appwidget etc.
Advantages of listener:
Can be used to filter suggestion list as user types.
These two approaches aren't mutually exclusive, so you may just use both.
I have a custom search content provider that combines recent searches and specific results from making a network REST call.
The problem is that if the network hangs, or is slow, no results come back. I'm wondering if there's a way to progressively return results ... in my case, return previous searches immediately, and return network search results when they are available.
I'm not seeing how this would be possible, since this appears to be a pull model. the UI component is requesting the search results, and has no way to know when to re-request to obtain the network based results ... that's even if I could understand to hook into it to make it re-request the results.
Any ideas?
This is how I went about handling this problem. the content provider itself never accesses the network in the main request thread. in stead, it,
looks for a cached result to the query
if there is a cached result, return that immediately
if there is no cached result, start a worker thread to find the result, and return empty results
when the worker thread from #3 is complete, it broadcasts an intent. this is received by the activity hosting the search, triggering it to re-submit the same search. this time when the search query gets to the content provider, the result is cached, and returned immediately.
the only complexity to this was how to force the activity to re-submit the search without user interaction. the solution was different depending on the version of android,
public void onReceive(Context context, Intent intent) {
String query = intent.getStringExtra(SearchManager.QUERY);
if (query == null) {
return;
}
// if user has typed something new, ignore
if (!query.equals(searchQuery)) {
return;
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQuery(query, false);
} else {
SearchManager sm = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
ComponentName cm = FolderActivity.this.getComponentName();
sm.startSearch(query, false, cm, null, false);
}
}
I have created a searchable activity. Now, i want to add search suggestions that are taken from web service. I want to get those suggestions asynchronously. According to Adding Custom Suggestions I need to override the query method, do my suggestion search, build my own MatrixCursor and return it. but this is the problem, my request for getting the suggestion is an asynchronically one. so when result is back from net it out side of query method's scope.
Here is an example of SearchView with suggestions coming from a network service (I used Retrofit):
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_search_activity, menu);
final SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.search));
final CursorAdapter suggestionAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1,
null,
new String[]{SearchManager.SUGGEST_COLUMN_TEXT_1},
new int[]{android.R.id.text1},
0);
final List<String> suggestions = new ArrayList<>();
searchView.setSuggestionsAdapter(suggestionAdapter);
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
#Override
public boolean onSuggestionSelect(int position) {
return false;
}
#Override
public boolean onSuggestionClick(int position) {
searchView.setQuery(suggestions.get(position), false);
searchView.clearFocus();
doSearch(suggestions.get(position));
return true;
}
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
return false;
}
#Override
public boolean onQueryTextChange(String newText) {
MyApp.autocompleteService.search(newText, new Callback<Autocomplete>() {
#Override
public void success(Autocomplete autocomplete, Response response) {
suggestions.clear();
suggestions.addAll(autocomplete.suggestions);
String[] columns = {
BaseColumns._ID,
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_INTENT_DATA
};
MatrixCursor cursor = new MatrixCursor(columns);
for (int i = 0; i < autocomplete.suggestions.size(); i++) {
String[] tmp = {Integer.toString(i), autocomplete.suggestions.get(i), autocomplete.suggestions.get(i)};
cursor.addRow(tmp);
}
suggestionAdapter.swapCursor(cursor);
}
#Override
public void failure(RetrofitError error) {
Toast.makeText(SearchFoodActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show();
Log.w("autocompleteService", error.getMessage());
}
});
return false;
}
});
return true;
}
It seems that the request to the suggestion content provider is not run on the UI thread, anyway, according to this answer: https://stackoverflow.com/a/12895381/621690 .
If you can change your http request you could simply call it blocking inside the query method. Might help to listen for interruptions or other signals (custom ones maybe) to stop unnecessary requests.
Another option - if you do not want to change any request classes that are already asynchronous (like if you are using Robospice) - should be to just return the MatrixCursor reference and populate it later on. The AbstractCursor class already implements the Observer pattern and sends out notifications in case of changes. If the search system is listening it should handle any changes in the data. I have yet to implement that myself so I cannot confirm that it will work out as nicely as I picture it. (Have a look at CursorLoader's source for more inspiration.)
And, anyway, isn't that the whole point of a cursor? Otherwise we could simply return a list with data.
UPDATE:
For me, using a MatrixCursor didn't work out. Instead, I have implemented two other solutions:
Using the AutoCompleteTextField in combination with a custom subclass of ArrayAdapter which itself uses a custom subclass of Filter. The method Filter#performFiltering() (which I override with the synchronous call to the remote service) is called asynchronously and the UI thread is not blocked.
Using the SearchWidget with a SearchableActivity and a custom ArrayAdapter (without custom Filter). When the search intent comes in, the remote request is started (Robospice) and when it comes back via callback, I call the following custom method on my ArrayAdapter<Tag> subclass:
public void addTags(List<Tag> items) {
if (items != null && items.size() > 0) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
super.setNotifyOnChange(false);
for (Tag tag : items) {
super.add(tag);
}
super.notifyDataSetChanged();
} else {
super.addAll(items);
}
}
}
This method takes care of triggering the notifications on the adapter and thus the search result list.
The closest thing I have found to solve this, is by using a ContentProvider and do the network request on the Query method of your provider (even when this goes against the best practices), with the result you can create a MatrixCursor as it's show here and here.
I'm still searching for other options like using a SyncAdapter, which seems overwhelming for the purpose of just showing suggestions that aren't used anywhere else.
Another option, that I took to do this asynchronously is to use an AutoCompleteTextView, that way you can create a custom adapter, where you can implement the getFilter function as it is shown in this answer.