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.
Related
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.
I am using an implementation of AutoCompleteTextView in which suggestions are fetched from webapi and displayed based on user input. I am using retrofit client for making network connection and observable for response, if two requests are sent from client one after another, how observable handle responses so that only last response is handled at client.
I could not post my code now, however in case of problem i will post it.
I am more concerned with response like assume user types a character and request is sent for suggestions after a specific time interval, before response can be received at client(delay may be due to any reason at server end) user types next character so the request will be sent for new list of suggestions. My requirement is that the response from first request should be invalidated and only latest response should be considered.
You should be using debounce as debounce will give the latest emitted observable and it will emit after the time specified and comboning it with publish subject will give you a +
PublishSubject<String> subject;
subject = PublishSubject.create();
subject.debounce(100, TimeUnit.MILLISECONDS)
.onBackpressureLatest()
.subscribe(new Action1<String>() {
#Override
public void call(String s) {
// Whatever processing on user String
}
});
autoCompleteTextView.addTextChangedListener(new TextWatcher() {
public void afterTextChanged(Editable s) { }
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
public void onTextChanged(CharSequence s, int start, int before, int count) {
subject.onNext(s.toString());
}
});
A way to handle this gracefully is to use debounce. I'll assume you already know how to make an observable from the AutoCompleteTextView.
Let's say you have this Observable autoCompleteTvObservable.
autoCompleteTvObservable.filter((it)-> it.length() >= 2)
.debounce(1000, TimeUnit.MILLISECONDS);
Filter above is just filtering the TextWatcher so that it will only trigger if length of inputted text is greater than or equal to 2, then debounce will wait 1 second and then just execute the observable afterwards with the last inputted text.
With this you can make sure that only the last version of the input would get queried.
I handled delay by implementing autoCompleteTextView class and overriding performFiltering by below code.
private val MESSAGE_TEXT_CHANGED = 100
private val DEFAULT_AUTOCOMPLETE_DELAY:Long = 3000
override fun performFiltering(text: CharSequence?, keyCode: Int) {
mLoadingIndicator.visibility = View.VISIBLE
super.performFiltering(text, keyCode)
mHandler.removeMessages(MESSAGE_TEXT_CHANGED)
mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_TEXT_CHANGED,
text), mAutoCompleteDelay)
}
mHandler is object which is created in implementation of autoCompleteTextView for handling request.
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'm building a 2 Player game on Android. The game works turnwise, so player 1 waits until player 2 made his input and vice versa. I have a webserver where I run an API with the Slim Framework. On the clients I use Retrofit. So on the clients I would like to poll my webserver (I know it's not the best approach) every X seconds to check whether there was an input from player 2 or not, if yes change UI (the gameboard).
Dealing with Retrofit I came across RxJava. My problem is to figure out whether I need to use RxJava or not? If yes, are there any really simple examples for polling with retrofit? (Since I send only a couple of key/value pairs) And if not how to do it with retrofit instead?
I found this thread here but it didn't help me too because I still don't know if I need Retrofit + RxJava at all, are there maybe easier ways?
Let's say the interface you defined for Retrofit contains a method like this:
public Observable<GameState> loadGameState(#Query("id") String gameId);
Retrofit methods can be defined in one of three ways:
1.) a simple synchronous one:
public GameState loadGameState(#Query("id") String gameId);
2.) one that take a Callback for asynchronous handling:
public void loadGameState(#Query("id") String gameId, Callback<GameState> callback);
3.) and the one that returns an rxjava Observable, see above. I think if you are going to use Retrofit in conjunction with rxjava it makes the most sense to use this version.
That way you could just use the Observable for a single request directly like this:
mApiService.loadGameState(mGameId)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<GameState>() {
#Override
public void onNext(GameState gameState) {
// use the current game state here
}
// onError and onCompleted are also here
});
If you want to repeatedly poll the server using you can provide the "pulse" using versions of timer() or interval():
Observable.timer(0, 2000, TimeUnit.MILLISECONDS)
.flatMap(mApiService.loadGameState(mGameId))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<GameState>() {
#Override
public void onNext(GameState gameState) {
// use the current game state here
}
// onError and onCompleted are also here
}).
It is important to note that I am using flatMap here instead of map - that's because the return value of loadGameState(mGameId) is itself an Observable.
But the version you are using in your update should work too:
Observable.interval(2, TimeUnit.SECONDS, Schedulers.io())
.map(tick -> Api.ReceiveGameTurn())
.doOnError(err -> Log.e("Polling", "Error retrieving messages" + err))
.retry()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(sub);
That is, if ReceiveGameTurn() is defined synchronously like my 1.) above, you would use map instead of flatMap.
In both cases the onNext of your Subscriber would be called every two seconds with the latest game state from the server. You can process them one after another of limit the emission to a single item by inserting take(1) before subscribe().
However, regarding the first version: A single network error would be first delivered to onError and then the Observable would stop emitting any more items, rendering your Subscriber useless and without input (remember, onError can only be called once). To work around this you could use any of the onError* methods of rxjava to "redirect" the failure to onNext.
For example:
Observable.timer(0, 2000, TimeUnit.MILLISECONDS)
.flatMap(new Func1<Long, Observable<GameState>>(){
#Override
public Observable<GameState> call(Long tick) {
return mApiService.loadGameState(mGameId)
.doOnError(err -> Log.e("Polling", "Error retrieving messages" + err))
.onErrorResumeNext(new Func1<Throwable, Observable<GameState>(){
#Override
public Observable<GameState> call(Throwable throwable) {
return Observable.emtpy());
}
});
}
})
.filter(/* check if it is a valid new game state */)
.take(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<GameState>() {
#Override
public void onNext(GameState gameState) {
// use the current game state here
}
// onError and onCompleted are also here
}).
This will every two seconds:
* use Retrofit to get the current game state from the server
* filter out invalid ones
* take the first valid one
* and the unsubscribe
In case of an error:
* it will print an error message in doOnNext
* and otherwise ignore the error: onErrorResumeNext will "consume" the onError-Event (i.e. your Subscriber's onError will not be called) and replaces it with nothing (Observable.empty()).
And, regarding the second version: In case of a network error retry would resubscribe to the interval immediately - and since interval emits the first Integer immediately upon subscription the next request would be sent immediately, too - and not after 3 seconds as you probably want...
Final note: Also, if your game state is quite large, you could also first just poll the server to ask whether a new state is available and only in case of a positive answer reload the new game state.
If you need more elaborate examples, please ask.
UPDATE: I've rewritten parts of this post and added more information in between.
UPDATE 2: I've added a full example of error handling with onErrorResumeNext.
Thank you, I finally made it in a similar way based the post I referred to in my question. Here's my code for now:
Subscriber sub = new Subscriber<Long>() {
#Override
public void onNext(Long _EmittedNumber)
{
GameTurn Turn = Api.ReceiveGameTurn(mGameInfo.GetGameID(), mGameInfo.GetPlayerOneID());
Log.d("Polling", "onNext: GameID - " + Turn.GetGameID());
}
#Override
public void onCompleted() {
Log.d("Polling", "Completed!");
}
#Override
public void onError(Throwable e) {
Log.d("Polling", "Error: " + e);
}
};
Observable.interval(3, TimeUnit.SECONDS, Schedulers.io())
// .map(tick -> Api.ReceiveGameTurn())
// .doOnError(err -> Log.e("Polling", "Error retrieving messages" + err))
.retry()
.subscribe(sub);
The problem now is that I need to terminate emitting when I get a positive answer (a GameTurn). I read about the takeUntil method where I would need to pass another Observable which would emit something once which would trigger the termination of my polling. But I'm not sure how to implement this.
According to your solution, your API method returns an Observable like it is shown on the Retrofit website. Maybe this is the solution? So how would it work?
UPDATE:
I considered #david.miholas advices and tried his suggestion with retry and filter. Below you can find the code for the game initialization. The polling should work identically: Player1 starts a new game -> polls for opponent, Player2 joins the game -> server sends to Player1 opponent's ID -> polling terminated.
Subscriber sub = new Subscriber<String>() {
#Override
public void onNext(String _SearchOpponentResult) {}
#Override
public void onCompleted() {
Log.d("Polling", "Completed!");
}
#Override
public void onError(Throwable e) {
Log.d("Polling", "Error: " + e);
}
};
Observable.interval(3, TimeUnit.SECONDS, Schedulers.io())
.map(tick -> mApiService.SearchForOpponent(mGameInfo.GetGameID()))
.doOnError(err -> Log.e("Polling", "Error retrieving messages: " + err))
.retry()
.filter(new Func1<String, Boolean>()
{
#Override
public Boolean call(String _SearchOpponentResult)
{
Boolean OpponentExists;
if (_SearchOpponentResult != "0")
{
Log.e("Polling", "Filter " + _SearchOpponentResult);
OpponentExists = true;
}
else
{
OpponentExists = false;
}
return OpponentExists;
}
})
.take(1)
.subscribe(sub);
The emission is correct, however I get this log message on every emit:
E/Polling﹕ Error retrieving messages: java.lang.NullPointerException
Apperently doOnError is triggered on every emit. Normally I would get some Retrofit debug logs on every emit which means that mApiService.SearchForOpponent won't get called. What do I do wrong?
I'm playing around with RXJava, retrofit in Android. I'm trying to accomplish the following:
I need to poll periodically a call that give me a Observable> (From here I could did it)
Once I get this list I want to iterate in each Delivery and call another methods that will give me the ETA (so just more info) I want to attach this new info into the delivery and give back the full list with the extra information attached to each item.
I know how to do that without rxjava once I get the list, but I would like to practice.
This is my code so far:
pollDeliveries = Observable.interval(POLLING_INTERVAL, TimeUnit.SECONDS, Schedulers.from(AsyncTask.THREAD_POOL_EXECUTOR))
.map(tick -> RestClient.getInstance().getApiService().getDeliveries())
.doOnError(err -> Log.e("MPB", "Error retrieving messages" + err))
.retry()
.subscribe(deliveries -> {
MainApp.getEventBus().postSticky(deliveries);
});
This is giving me a list of deliveries. Now I would like to accomplish the second part.
Hope I been enough clear.
Thanks
Finally I found a nice way to do it.
private void startPolling() {
pollDeliveries = Observable.interval(POLLING_INTERVAL, TimeUnit.SECONDS, Schedulers.from(AsyncTask.THREAD_POOL_EXECUTOR))
.flatMap(tick -> getDeliveriesObs())
.doOnError(err -> Log.e("MPB", "Error retrieving messages" + err))
.retry()
.subscribe(this::parseDeliveries, Throwable::printStackTrace);
}
private Observable<List<Delivery>> getDeliveriesObs() {
return RestClient.getInstance().getApiService().getDeliveries()
.flatMap(Observable::from)
.flatMap(this::getETAForDelivery)
.toSortedList((d1, d2) -> {
if (d1.getEta() == null) {
return -1;
}
if (d2.getEta() == null) {
return 1;
}
return d1.getEta().getDuration().getValue() > d2.getEta().getDuration().getValue() ? 1 : -1;
});
}
Let's go step by step.
First we create an Observable that triggers every POLLING_INTERVAL time the method getDeliveriesObs() that will return the final list
We use retrofit to get an Observable of the call
We use flatMap to flattern the resut list and get in the next flatmap a Delivery item, one by one.
Then we get the estimated time of arrival set inside the Delivery object and return it
We sort the list to order by estimated time of arrival.
In case of error we print and retry so the interval does not stop
We subscribe finally to get the list sorted and with ETA inside, then we just return it or whatever you need to do with it.
It's working properly and it's quite nice, I'm starting to like rxjava :)
I haven't spent a lot of time with Java 8 lambdas, but here's an example of mapping each object to a different object, then getting a List<...> out at the other end in plain ol' Java 7:
List<Delivery> deliveries = ...;
Observable.from(deliveries).flatMap(new Func1<Delivery, Observable<ETA>>() {
#Override
public Observable<ETA> call(Delivery delivery) {
// Convert delivery to ETA...
return someEta;
}
})
.toList().subscribe(new Action1<List<ETA>>() {
#Override
public void call(List<ETA> etas) {
}
});
Of course, it'd be nice to take the Retrofit response (presumably an Observable<List<Delivery>>?) and just observe each of those. For that we ideally use something like flatten(), which doesn't appear to be coming to RxJava anytime soon.
To do that, you can instead do something like this (much nicer with lambdas). You'd replace Observable.from(deliveries) in the above example with the following:
apiService.getDeliveries().flatMap(new Func1<List<Delivery>, Observable<Delivery>>() {
#Override
public Observable<Delivery> call(List<Delivery> deliveries) {
return Observable.from(deliveries);
}
}).flatMap(...)