I'm using Retrofit 2 with RxAndroid, and I want to keep a request going during a config change. I thought I could do it with Observable.cache() as described in this blog post and others I've seen, but the following flow causes an InterruptedException.
Observable<Result<List<Post>>> request =
postService.index(page).cache();
Subscription subscribeOne = request.subscribe();
subscribeOne.unsubscribe();
Subscription subscribeTwo = request.subscribe();
I'm pretty sure the following code in the Retrofit source is responsible for cancelling the request when unsubscribe is called.
// Attempt to cancel the call if it is still in-flight on unsubscription.
subscriber.add(Subscriptions.create(new Action0() {
#Override public void call() {
call.cancel();
}
}));
Not unsubscribing makes everything work, but this could cause leaks. Has anyone managed to handle config changes with Retrofit 2? Is there a different approach I can use?
Thanks to a hint from /u/insane-cabbage, I managed to implement this with a BehaviourSubject (safely encapsulated in a presenter). Here's an example of the flow.
BehaviorSubject<String> subject = BehaviorSubject.create();
/** User loads view and network request begins */
Observable.just("value")
.delay(200, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.newThread())
.subscribe(subject::onNext);
Subscription portraitSub = subject.subscribe(
s -> System.out.println("Portrait: " + s));
/** onDestroy() */
portraitSub.unsubscribe();
/** Rotating... */
Thread.sleep(300);
/** onRestoreInstanceState() **/
Subscription landscapeSub = subject.subscribe(
s -> System.out.println("Landscape: " + s));
/** Output */
> Landscape: value
I have a working example RxApp that uses AsyncSubject to implement cache for network request and the code shows how to subscribe to a pending request. I'm a bit confused with Rx subjects as on the other they seem pretty handy but on the other hand it's recommended that they are to be used only in very seldom cases e.g. To Use Subject Or Not To Use Subject?. Would be great if some one could explain what really is the problem if they're used like in my example.
Related
I am doing something that I thought should be straightforward: A WearOS tile shall display the data fetched via https request. I took the goals tile example from the documentation and added a callback for fetching the data in my Tile Service:
private val repoRetriever = EnergyRetriever()
private val callback = object : Callback<EnergyResult> {
override fun onFailure(call: Call<EnergyResult>?, t:Throwable?) {
Log.e("MainActivity", "Problem calling Energy API {${t?.message}}")
}
override fun onResponse(call: Call<EnergyResult>?, response: Response<EnergyResult>?) {
response?.isSuccessful.let {
val energyResult = response?.body()
Log.i("MainActivity", "SUCCESS! " + energyResult?.time + " - "+energyResult?.consumption + "kW")
EnergyRepository.setEnergy(energyResult);
getUpdater(getApplicationContext()).requestUpdate(GoalsTileService::class.java)
}
}
}
On the top of onTileRequest I initiate the request. I know that the tile will be rendered with the initial/old dataset, which is ok. I just want the tile to update once the data has been fetched:
override fun onTileRequest(requestParams: TileRequest) = serviceScope.future {
if (isNetworkConnected()) {
repoRetriever.getEnergyUpdate(callback)
}
// Retrieves data to populate the Tile.
val energyResult = EnergyRepository.getEnergy()
// Retrieves device parameters to later retrieve font styles for any text in the Tile.
val deviceParams = requestParams.deviceParameters!!
// Creates Tile.
Tile.builder()
// If there are any graphics/images defined in the Tile's layout, the system will
// retrieve them via onResourcesRequest() and match them with this version number.
.setResourcesVersion(RESOURCES_VERSION)
.setFreshnessIntervalMillis(5 * 60 * 1000)
// Creates a timeline to hold one or more tile entries for a specific time periods.
.setTimeline(
Timeline.builder().addTimelineEntry(
TimelineEntry.builder().setLayout(
Layout.builder().setRoot(
// Creates the root [Box] [LayoutElement]
layout(energyResult!!, deviceParams)
)
)
)
).build()
}
This is obviously not working right because the onTileRequest will finish before the HTTP request is done. I also understand that one shouldn't block this function to wait. RequestUpdate() causes problems because the tile won't update again within a 20 second period due to limitations imposed by Google. I've read that one can use Futures in onTileRequest to defer the actual update until the http request returns - however I haven't been able to figure out just how and for the life of me I can't find an understandable example that would apply to what I'm trying to do. I don't even know if using callbacks for the http request is advisable when using futures here.
Anyone got suggestions?
Starting this work from an incoming tile request is problematic for two reasons.
The first tile request, likely stops you sending updates within the next twenty seconds. Ideally you should separate the data refresh from when you happen to get called for a tile. It also looks like your logic is effectively an infinite loop.
Your app might be started temporarily for the tile request, and then return immediately, causing your tileservice to be unbound and your app to shutdown before your update completes.
Instead consider scheduling some background task with WorkManager, load your data, store is in Androidx DataStore or Room as a cache. Trigger the Tile update from Work Manager, and then load your data from the tile request out of that cache.
I don't have a clear example, but I do this in my app and it works well.
https://github.com/yschimke/rememberwear/blob/main/wear/src/main/kotlin/com/google/wear/soyted/app/work/DataRefreshWorker.kt
https://github.com/yschimke/rememberwear/blob/main/wear/src/main/kotlin/com/google/wear/soyted/tile/RememberWearTileProviderService.kt
I'm new to Firebase and I'm having a lot of problems with the fact that all the tasks are called asynchronously.
For example, I am trying to use fetchProvidersForEmail to know if I should direct the user to sign up or log in. However, by the time the task finishes, it's too late.
I am not sure if it's clear but here is my current code (which works) and below is the method I would want to create. How can I get that done?
public static void printProviders(String email) {
FirebaseAuth auth = FirebaseAuth.getInstance();
auth.fetchProvidersForEmail(email).addOnCompleteListener(new OnCompleteListener<ProviderQueryResult>() {
#Override
public void onComplete(#NonNull Task<ProviderQueryResult> task) {
Log.d(TAG, "We have " + task.getResult().getProviders().size() + " results.");
for (int i = 0; i < task.getResult().getProviders().size(); i++) {
Log.d(TAG, "Provider " + (i+1) + ": " + task.getResult().getProviders().get(i));
}
}
}
);
}
Here is the pseudo-code of the method I would want to create (of course, this doesn't work)...
public static boolean emailIsRegistered(String email) {
FirebaseAuth auth = FirebaseAuth.getInstance();
auth.fetchProvidersForEmail(email).addOnCompleteListener(new OnCompleteListener<ProviderQueryResult>() {
#Override
public void onComplete(#NonNull Task<ProviderQueryResult> task) {
if (task.getResult().getProviders().size() > 0) {
return true;
}
return false;
}
});
}
However, this does not work because the return statement is void for onComplete() and because the task is executed asynchronously...
I am new to this. I tried to search through StackOverflow but couldn't find anything that helped me. Hopefully someone can help.
Thank you!
When you call fetchProvidersForEmail that information is not available in the APK of your app. The Firebase client has to call out to the servers to get this information.
Given the nature of the internet, this means that it will take an undetermined amount of time before the result comes back from those servers.
The client has a few options on what to do in the meantime:
wait until the data is available
continue executing and calling you back when the data is available
Waiting for the data would mean that your code stays simple. But it also means that your app is blocked while the data is being looked up. So: no spinner animation would run, the user can't do anything else (which may be fine for your app, but not for others), etc. This is considered a bad user experience. So bad in fact, that Android will show an Application Not Responding dialog if your app is in this state for 5 seconds.
So instead, the Firebase SDKs choose the other option: they let your code continue, while they're retrieveing the data from the servers. Then when the data is retrieved, they call back into a code block you provided. Most modern web APIs are built this way, so the sooner you come to grips with it, the sooner you can efficiently use those APIs.
The easiest way I found to grasps asynchronous programming is by reframing your problems. Right now you're trying to "first determine if the email is already used, then sign the user up or in".
if (emailIsRegistered(email)) {
signInUser(email);
}
else {
signUpUser(email);
}
This approach leads to a emailIsRegistered method that returns a boolean, something that is impossible with asynchronous methods.
Now let's reframe the problem to "determine if the email is already used. When we know this, sign the user up or in".
This leads to a different piece of code:
public static boolean emailIsRegistered(String email) {
FirebaseAuth auth = FirebaseAuth.getInstance();
auth.fetchProvidersForEmail(email).addOnCompleteListener(new OnCompleteListener<ProviderQueryResult>() {
#Override
public void onComplete(#NonNull Task<ProviderQueryResult> task) {
if (task.getResult().getProviders().size() > 0) {
signUserIn(email);
}
signUserUp(email);
}
});
We've moved the calls to sign the user up or in into the emailIsRegistered method and invoke then when the result becomes available.
Now this of course hard-coded the follow up action into the emailIsRegistered method, which makes it harder to re-use. That's why you quite often see a callback being passed into these functions. A great example of that is the OnCompleteListener that you're already using. Once the Firebase client gets the result from the servers, it calls the onComplete method that you passed in.
Learning to deal with asynchronous calls is both hard and important. I'm not sure if this is my best explanation of the concepts ever. So I'll include some previous explanations (from both me and others):
Setting Singleton property value in Firebase Listener
Firebase Android: How to read from different references sequentially
Is it possible to synchronously load data from Firebase?
Knowing when Firebase has completed API call?
Gathering data from Firebase asynchronously: when is the data-set complete?
What is callback in Android?
I have a below scenario when setView is called then Presenter fetches some data over the network on a new thread. Test fails by giving this reason - Actually, there were zero interactions with this mock. But it should pass if interaction gets verified.
Testcase
#Test
public void checkUnoRate() {
ratePresenter.setView(rateView,Constants.UNO);
verify(rateView,times(1)).showRate(new Rate());
}
Inside "ratePresenter.setView"
Call<UnoRate> call1 = ratesAPI.getUnoRate();
call1.enqueue(new Callback<UnoRate>() {
#Override
public void onResponse(Call<UnoRate> call,Response<UnoRate> response) {
UnoRate unoRate = response.body();
Rate rate = new Rate();
rate.setBuyRate(unoRate.getBuy());
rate.setSellRate(unoRate.getSell());
rate.setFee(0);
rateView.showRate(rate);
}
});
One very simple solution is to use Mockito's verification with timeout feature. This will retry the verification repeatedly up until the timeout, looking for the condition to pass at some point or another.
#Test
public void checkUnoRate() {
ratePresenter.setView(rateView,Constants.UNO);
verify(rateView, timeout(100).times(1)).showRate(new Rate());
}
The docs, however, warn against it: "This feature should be used rarely - figure out a better way of testing your multi-threaded system." This is probably because you're introducing a new aspect--time--as a proxy for the thing you really want to check, which is that all of the queues have been processed. You could even imagine a busy enough VM where a conservative timeout could cause the test to flake in automated testing systems but that works fine on development machines.
If feasible, you could switch your ratesAPI to use a synchronous executor, or instead you could add methods needed to your API accessor to block the test thread until all calls have returned asynchronously:
#Test
public void checkUnoRate() {
ratePresenter.setView(rateView,Constants.UNO);
ratesAPI.flush(); // Implement this to perform a Thread.join on the callback thread,
// or otherwise wait until all callbacks have been called.
verify(rateView,times(1)).showRate(new Rate());
}
Or, to remove multithreading and external API interactions from your test, simulate the callback synchronously:
#Mock RatesAPI ratesApiMock;
#Mock Call<UnoRate> unoRateCallMock;
#Captor Callback<UnoRate> unoRateCallbackCaptor;
#Test
public void checkUnoRate() {
// Set up mock.
when(ratesApiMock.getUnoRate()).thenReturn(unoRateCallMock);
// Perform the action.
ratePresenter.setView(rateView,Constants.UNO);
// Verify nothing happens yet.
verify(rateView, never()).showRate(any());
// Capture and trigger the callback.
verify(unoRateCallMock).enqueue(unoRateCallbackCaptor.capture());
unoRateCallbackCaptor.getValue().onResponse(yourCall, yourResponse);
// Verify the asynchronous action.
verify(rateView,times(1)).showRate(new Rate());
}
As a side note, eventually you'll probably want to verify against a different parameter than new Rate(). Mockito compares via equals methods when not using Mockito matchers.
Today I was just doing some research on Retrofit by our very own Jake Wharton, so I did something like this
RetroClass.getClient().getSomeData(param, new Callback<Model>(){
#Override
public void failure(...){/*blah*/}
#Override
public void success(Model response, Response notUsed)
{
try
{
Thread.sleep(10000);
}
catch(Exception e){e.pST();}
}});
I expected a ANR, but the flow is executing fine, Jake Wharton mentioned in this post
Does Retrofit make network calls on main thread?
"As it states in the answer, if you use the second pattern (last argument as a Callback) the request is done asynchronously but the callback is invoked on the main thread. By default Retrofit uses a thread pool for these requests."
that the call back is executed on the main thread, whats happening here, any insights ? Why isnt Thread.sleep() not causing an ANR here...? Im baffled....
Yes by default for the Android platform the Callback Executor used is MainThreadExecutor.
Make sure you're not overriding the default implementation when creating your RestAdapter by doing something like this
RestAdapter restAdapter = new RestAdapter.Builder()
.setExecutors(new MyHttpExecuter(), new MyCallbackExecutor()) { // watch out for this
.build()
If you override the default Callback Executor by setting your own then you won't get the default behaviour.
I use the android.os.Handler class to perform tasks on the background. When unit testing these, I call Looper.loop() to make the test thread wait for the background task thread to do its thing. Later, I call Looper.myLooper().quit() (also in the test thread), to allow the test thread to quit the loop and resume the testing logic.
It's all fine and dandy until I want to write more than one test method.
The problem is that Looper doesn't seem to be designed to allow quitting and restarting on the same thread, so I am forced to do all of my testing inside a single test method.
I looked into the source code of Looper, and couldn't find a way around it.
Is there any other way to test my Hander/Looper code? Or maybe some more test friendly way to write my background task class?
The source code for Looper reveals that Looper.myLooper().quit() enqueues a null message in the Message queue, which tells Looper that it is done processing messages FOREVER. Essentially, the thread becomes a dead thread at that point, and there is no way to revive it that I know of. You may have seen error messages when attempting to post messages to the
Handler after quit() is called to the effect "attempting to send message to dead thread". That is what that means.
This can actually be tested easily if you aren't using AsyncTask by introducing a second looper thread (other than the main one created for you implicitly by Android). The basic strategy then is to block the main looper thread using a CountDownLatch while delegating all your callbacks to the second looper thread.
The caveat here is that your code under test must be able to support using a looper other than the default main one. I would argue that this should be the case regardless to support a more robust and flexible design, and it is also fortunately very easy. In general, all that must be done is to modify your code to accept an optional Looper parameter and use that to construct your Handler (as new Handler(myLooper)). For AsyncTask, this requirement makes it impossible to test it with this approach. A problem that I think should be remedied with AsyncTask itself.
Some sample code to get you started:
public void testThreadedDesign() {
final CountDownLatch latch = new CountDownLatch(1);
/* Just some class to store your result. */
final TestResult result = new TestResult();
HandlerThread testThread = new HandlerThread("testThreadedDesign thread");
testThread.start();
/* This begins a background task, say, doing some intensive I/O.
* The listener methods are called back when the job completes or
* fails. */
new ThingThatOperatesInTheBackground().doYourWorst(testThread.getLooper(),
new SomeListenerThatTotallyShouldExist() {
public void onComplete() {
result.success = true;
finished();
}
public void onFizzBarError() {
result.success = false;
finished();
}
private void finished() {
latch.countDown();
}
});
latch.await();
testThread.getLooper().quit();
assertTrue(result.success);
}
I've stumbled in the same issue as yours. I also wanted to make a test case for a class that use a Handler.
Same as what you did, I use the Looper.loop() to have the test thread starts handling the queued messages in the handler.
To stop it, I used the implementation of MessageQueue.IdleHandler to notify me when the looper is blocking to wait the next message to come. When it happen, I call the quit() method. But again, same as you I got a problem when I make more than one test case.
I wonder if you already solved this problem and perhaps care to share it with me (and possibly others) :)
PS: I also would like to know how you call your Looper.myLooper().quit().
Thanks!
Inspired by #Josh Guilfoyle's answer, I decided to try to use reflection to get access to what I needed in order to make my own non-blocking and non-quitting Looper.loop().
/**
* Using reflection, steal non-visible "message.next"
* #param message
* #return
* #throws Exception
*/
private Message _next(Message message) throws Exception {
Field f = Message.class.getDeclaredField("next");
f.setAccessible(true);
return (Message)f.get(message);
}
/**
* Get and remove next message in local thread-pool. Thread must be associated with a Looper.
* #return next Message, or 'null' if no messages available in queue.
* #throws Exception
*/
private Message _pullNextMessage() throws Exception {
final Field _messages = MessageQueue.class.getDeclaredField("mMessages");
final Method _next = MessageQueue.class.getDeclaredMethod("next");
_messages.setAccessible(true);
_next.setAccessible(true);
final Message root = (Message)_messages.get(Looper.myQueue());
final boolean wouldBlock = (_next(root) == null);
if(wouldBlock)
return null;
else
return (Message)_next.invoke(Looper.myQueue());
}
/**
* Process all pending Messages (Handler.post (...)).
*
* A very simplified version of Looper.loop() except it won't
* block (returns if no messages available).
* #throws Exception
*/
private void _doMessageQueue() throws Exception {
Message msg;
while((msg = _pullNextMessage()) != null) {
msg.getTarget().dispatchMessage(msg);
}
}
Now in my tests (which need to run on the UI thread), I can now do:
#UiThreadTest
public void testCallbacks() throws Throwable {
adapter = new UpnpDeviceArrayAdapter(getInstrumentation().getContext(), upnpService);
assertEquals(0, adapter.getCount());
upnpService.getRegistry().addDevice(createRemoteDevice());
// the adapter posts a Runnable which adds the new device.
// it has to because it must be run on the UI thread. So we
// so we need to process this (and all other) handlers before
// checking up on the adapter again.
_doMessageQueue();
assertEquals(2, adapter.getCount());
// remove device, _doMessageQueue()
}
I'm not saying this is a good idea, but so far it's been working for me. Might be worth trying out! What I like about this is that Exceptions that are thrown inside some hander.post(...) will break the tests, which is not the case otherwise.