I am using the following observable to call retrofit api then save the response into cache file:
#Override public Observable<StoryCollectionEntity> storyEntityList(final int page) {
return this.restApi.storyCollection(id, page)
.doOnNext(saveStoryCollectionToCacheAction)
.onErrorResumeNext(CloudNewsDataStore.this.mNewsCache.getStories(page));
}
This works as expected. my question is: how can i make this observer returns api response periodically?
let's say, user wants to refresh the data every 5 minutes
The interval() operator will emit an item at a given time interval.
You can use this to trigger periodic events like so:
Observable.interval(5, TimeUnit.MINUTES)
.flatMap(count -> this.restApi.storeCollection(id, page))
// etc.
Related
I've had a look at the codelab for WorkManager plus some examples on here, but everything in code I have seen is either related to doing work locally on the device or work uploading to the server, not downloading data and responding to the data received. In the developer guidelines it even says, "For example, an app might need to download new resources from the network from time to time," so I thought it would be perfect for this task. My question is if WorkManager can handle the following scenario and if not, what is the proper tool for handling it:
Schedule a job that runs once a day in background
The job is to do a data fetch from the REST API (and post it to a LiveData object if possible).
When the data returns, check that it is newer than local data.
Notify the user that new data is available.
My worker class looks something like this:
public class MyWorker extends Worker {
#NonNull
#Override
public WorkerResult doWork() {
lookForNewData();
return WorkerResult.SUCCESS;
}
public void lookForNewData() {
MutableLiveData<MyObject> liveData = new MutableLiveData<>();
liveData.observe(lifeCycleOwner, results -> {
notifyOnNewData(results);
})
APILayer.getInstance().fetchData(searchParams, liveData)
}
My issue is of course that the LiveData object can't observe because there is no activity or fragment that can be its LifecycleOwner. But even if I used a callback from the API to respond to the data arriving, my worker would already have posted that it was successful and it probably would not proceed with the callback, right? So I kind of know this approach is totally wrong, but I can't see any code for getting data with WorkManager
Please help with a proper solution and some example code or some links, either with WorkManager if it can handle this kind of work or something else if it is more appropriate.
Schedule a job that runs once a day in background
You can schedule a PeriodicWorkRequest for that, which should be queued with enqueueUniquePeriodicWork. This makes sure only one PeriodicWorkRequest of a particular name can be active at a time.
Constraints constraint = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(MyWorker.class, 1, TimeUnit.DAYS)
.setConstraints(constraint)
.build();
WorkManager workManager = WorkManager.getInstance();
workManager.enqueueUniquePeriodicWork("my_unique_worker", ExistingPeriodicWorkPolicy.KEEP, workRequest);
The job is to do a data fetch from the REST API (and post it to a LiveData object if possible).
This can by done by sending your request synchronously within doWork() of your worker. I wouldn't use LiveData within your Worker class. We come to that later. The API call would look with Retrofit for example like that:
#Override
public WorkerResult doWork() {
Call<MyData> call = APILayer.getInstance().fetchData();
Response<MyData> response = call.execute();
if (response.code() == 200) {
MyData data = response.body();
// ...
} else {
return Result.RETRY;
}
// ...
return Result.SUCCESS;
}
When the data returns, check that it is newer than local data.
You fetched your API data in a synchronous way. Fetch your local data also synchronously and do whatever you need to do to compare them.
Notify the user that new data is available.
If you schedule a task with WorkManager it is guaranteed to run, even if your app is force-quit or the device is rebooted. So your task might complete while your app is not running. If you want to notify the user in any case you can send a notification. If you want to notify the user within a certain screen you can subscribe on your tasks status. For example like this (taken from the official guide):
WorkManager.getInstance().getStatusById(compressionWork.getId())
.observe(lifecycleOwner, workStatus -> {
// Do something with the status
if (workStatus != null && workStatus.getState().isFinished()) {
// ...
}
});
There's also getStatusesForUniqueWork(String uniqueWorkName) for our example.
The official guide is also explaining how to return data from you Task with which you can call setValue() on your MutableLiveData for example.
I would propose to update your local data within your Worker, subscribe on your workers status and once it succeeds update your UI with the local data (if you are not subscribed on your local data anyways, i.e. with Room and LiveData).
Edit: In reference to point 4, reading status of periodic work requests works a little different. They are only switching between ENQUEUED and RUNNING until CANCELLED. But will never have the state SUCCEEDED or FAILED. So listening for isFinished() might not be what you are expecting.
This is initial thought. Somebody please correct me if i'm wrong.
my worker would already have posted that it was successful and it probably would not proceed with the callback, right?
we can use the callback from API reponse, to construct output Data of the worker and set it using worker.setOutputData()
Then listen to the LiveData<WorkStatus> from workManager. From this workstatus we can get outputData using, workStatus.getOutputdata(). This data can give us the API response we want.
We can pass this response to next worker in the worker chain to carry out tasks like updating local DB.
I need to poll endpoint every second, currently I do it with
Observable.interval(0, 1, TimeUnit.SECONDS, ioScheduler)
.switchMap { return pollWithRetrofit() }
It works fine except the situation when the calls start taking more than 1 second to process, so the retrofit subscription is cancelled by swithMap before I get any response. It can happen multiple times in a row, effectively leaving the client without any response from the poll calls for long duration. In this case I would like to not cancel the retrofit call until I get a response from the next call.
I know that switchMap cancels the previous subscription when the base subscription produces onNext call, currently it happens every second by the Observable.interval, so my idea is to cancel previous call only when the retrofit calls it's onNext, i.e moving the switching one step forward the reactive chain.
How do I do that? Or is there some other solution?
You could use onBackpressureDrop and flatMap with maxConcurrency of 1 to make sure a longer call is still allowed to succeed:
Flowable.interval(0, 1, TimeUnit.SECONDS, ioScheduler)
.onBackpressureDrop()
.flatMap(v -> pollWithRetrofit(), 1);
fwiw I'm using code like following for doing something similar...am not fully convinced that this is cleanest approach either (though has been working successfully for a while now)
someRetrofitInterface.apiCall()
.repeatWhen { completed -> completed.delay(30, TimeUnit.SECONDS) }
.retry(3)
(from https://github.com/joreilly/galway-bus-android/blob/master/base/src/main/java/com/surrus/galwaybus/domain/interactor/GetNearestBusStopsUseCase.kt)
My ViewModel is returning nullfields for current User how to fix?
Say I had the following Disposable. getUserProfile calls a Retrofit 2 API that returns Observable<Response<GetUserProfileResponseBody> when the Activity is started in onStart. Is this a one-time call to the Retrofit 2 API? Say the user profile has a bunch of posts and user details which is retrievable in the Response<GetUserProfileResponseBody>. I want the user profile to automatically update when stuff changes or gets added, and if it doesn't, I would just swipe down and call onRefresh. For example, say in onStart I call the following code:
public void load(String userId) {
GetUserProfileRequest request = new GetUserProfileRequest(userId);
Disposable disposable = mViewModel
.getUserProfile(request)
.subscribe((Response<GetUserProfileResponseBody> response) -> {
if (response.body() != null) {
mViewModel.setCurrentUser(response.body().getCurrentUser());
}
}, (Throwable ex) -> {
});
mCompositeDisposable.addDisposable(disposable);
}
and then I go ahead and edit my profile (in the same activity so mCompositeDisposable.clear() is not called). Afterwards, I expect the changes to reflect immediately in the user profile. Do I have to call load(userId) one more time, which in turns adds another disposable to composite disposable, or will Observable automatically detect the changes from the Retrofit 2 API to update the UI? I want the Observable that I'm subscribing to to get updated so I don't have to resubscribe.
Similarly, everytime I swipe refresh, I don't want to call all of load which add another disposable either, I just want the thing I'm subscribing to to call the Retrofit 2 API again so I can have updated data in the SAME disposable.
Update
In subscribe I'm setting current user in viewmodel. At the same time, in a Fragment of this activity, I am accessing the viewmodel field in the layout.xml, but that field is null, because mViewModel.setCurrentUser has not yet been called when the layout.xml is rendered.
<variable
name="viewModel"
type="com.skoolar.viewmodels.GetUserProfileViewModel" />
<ImageView
android:id="#+id/image_user_avatar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
How do I delay the rendering of the layout.xml until mViewModel.currentUser is rednerred?
You can use background polling to automatically refresh the user posts on the profile every X seconds. Lets say you have a postService to retrieve user wall feed.
Observable.interval(30, TimeUnit.SECONDS, Schedulers.io())
.map(tick -> postService.getRecentPost())
.subscribe(posts -> {
// the subscriber is called with List<Message> every time
// the remote service is polled
for (Post post : posts) {
log.info(post.toString())
}
})
In my Android app I am using domain level Repository interface, which is backed with local DB implemented using SqlBrite and network api with Retrofit observables. So I have method getDomains(): Observable<List<Domain>> in Repository and two corresponding methods in my Retrofit and SqlBrite.
I don't want to concatenate or merge, or amb these two observables. I want my Repository to take data only from SqlBrite and since SqlBrite returns QueryObservable, which triggers onNext() every time underlying data changed, I can run my network request independently and store results to SqlBrite and have my Observable updated with fetched from network and stored to DB data.
So I tried to implement my Repository's getDomains() method as follow:
fun getDomains(): Observable<List<Domain>> {
return db.getDomains()
.doOnSubscribe {
networkClient.getDomains()
.doOnNext { db.putDomains(it) }
.onErrorReturn{ emptyList() }
.subscribe()
}
}
But in this case every time the client should subscribe, every time it would make network requests, that is not so good. I thought about other do... operators to move requests there, but doOnCompleted() in case of QueryObservable would never be called, until I call toBlocking() somewhere, which I won't, doOnEach() also not good as it makes requests every time item from db extracted.
I also tried to use replay() operator, but though the Observable cached in this case, the subscription happens and results in network requests.
So, how can combine these two Observables in the desired way?
Ok, it depends on the concrete use case you have: i.e. assuming you want to display the latest data from your local database and from time to time update the database by doing a network request in the background.
Maybe there is a better way, but maybe you could do something like this
fun <T> createDataAwareObservable(databaseQuery: Observable<T>): Observable<T> =
stateDeterminer.getState().flatMap {
when (it) {
State.UP_TO_DATE -> databaseQuery // Nothing to do, data is up to date so observable can be returned directly
State.NO_DATA ->
networkClient.getDomains() // no data so first do the network call
.flatMap { db.save(it) } // save network call result in database
.flatMap { databaseQuery } // continue with original observable
State.SYNC_IN_BACKGROUND -> {
// Execute sync in background
networkClient.getDomains()
.flatMap { db.save(it) }
.observeOn(backgroundSyncScheduler)
.subscribeOn(backgroundSyncScheduler)
.subscribe({}, { Timber.e(it, "Error when starting background sync") }, {})
// Continue with original observable in parallel, network call will then update database and thanks to sqlbrite databaseQuery will be update automatically
databaseQuery
}
}
}
So at the end you create your SQLBrite Observable (QueryObservable) and pass it into the createDataAwareObservable() function. Than it will ensure that it loads the data from network if no data is here, otherwise it will check if the data should be updated in background (will save it into database, which then will update the SQLBrite QueryObservable automatically) or if the data is up to date.
Basically you can use it like this:
createDataAwareObservable( db.getAllDomains() ).subscribe(...)
So for you as user of this createDataAwareObservable() you always get the same type Observable<T> back as you pass in as parameter. So essentially it seems that you were always subscribing to db.getAllDomains() ...
if your problem is that you have to subscribe your observer every time that you want to get data you can use relay, which never unsubscribe the observers because does not implement onComplete
/**
* Relay is just an observable which subscribe an observer, but it wont unsubscribe once emit the items. So the pipeline keep open
* It should return 1,2,3,4,5 for first observer and just 3, 4, 5 fot the second observer since default relay emit last emitted item,
* and all the next items passed to the pipeline.
*/
#Test
public void testRelay() throws InterruptedException {
BehaviorRelay<String> relay = BehaviorRelay.create("default");
relay.subscribe(result -> System.out.println("Observer1:" + result));
relay.call("1");
relay.call("2");
relay.call("3");
relay.subscribe(result -> System.out.println("Observer2:" + result));
relay.call("4");
relay.call("5");
}
Another examples here https://github.com/politrons/reactive/blob/master/src/test/java/rx/relay/Relay.java
I'm refactoring some code that would call a web service with a formatted date parameter ("2016-3-10" for example), and if that returned null, would fire off another method with a date one day earlier (like "2016-3-9"). This would happen for at least 3 retries.
I'm refactoring this into RxJava and am not sure how to implement the backoff strategy or which .retry() or .retryWhen() operator to use in this situation.
I have the web service returning the usual Observable using Retrofit.
This is what I have currently:
PictureService pictureService = retrofit.create(PictureService.class);
pictureService.getPhotos(date)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retry() //Some retry
...
.subscribe()
So which operator would be best used here for retrying the requests, and how should I pass in a new date to the getPhotos() request when the current date returns null?
Just for the sake of completion here is the Retrofit service that was mentioned above:
#GET("/someurl/photos?api_key=xxxxxxx")
Observable<PictureAPI> getPhotos(#Query("earth_date") String earthDate);
OK, now that I can answer this question again, here we go:
Observable.just("2016-3-10", "2016-3-9", "2016-3-8")
.concatMap(date -> pictureService.getPhotos(date))
.filter(response -> response != null)
.take(1)...
For a detailed explanation take a look at this blog post by Dan Lew: http://blog.danlew.net/2015/06/22/loading-data-from-multiple-sources-with-rxjava/
In short: concat (and concatMap) will subscribe to each new Observable only after the previous one has emitted onCompleted AND only if more items are needed downstream. The take(1) will unsubscribe after the first non-null response and therefore concatMap will just not subscribe to the next Observable.
EDIT: To check that really only one request is sent you could
1.) Enable logging in Retrofit (if you are recent version by adding an Interceptor to the OkHttpClient). This lets you directly observe the requests that are being sent.
2.) Add a doOnSubscribe() like this:
.concatMap(date -> pictureService.getPhotos(date)
.doOnSubscribe(new Action0() {
#Override
public void call() {
Log.d(TAG, "sending request for " + date);
}
});
)
Since Retrofit requests are only sent upon subscription the log message will appear if and only if a request is then sent.