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.
Related
I'm trying to get my WorkerInfo with the getWorkInfoByLiveData method that exists in the WorkManager instance.
val workInfo = workManager.getWorkInfoByIdLiveData(uuid).value
The WorkInfo is always null. Also, I'm calling this method from the main thread.
The scenario of how I'm checking this method. I try to enqueue my worker when a user sends a network request and if the internet is not connected I simply register a work with the WorkManager. After some time if I try to get the WorkerInfo with the UUID, it'll always give me null.
Note: When calling getWorkInfoByLiveData the Worker is not executed at that time.
Don't I'm expecting from WorkManager to give me WorkInfo with ENQUEUED State.
Edit 1:
So, another scenario would be like this, the app on which I'm working is like a social app. Now after registering a first worker, let's say the user don't want to see the posts from a specific user so this where I need to register my second worker because the user internet is not available at this time. Now what I need to do is to cancel the previously registered worker and then create a Chain of workers with not to show post of a user to beginWith and then the fetch all posts. Now in order to cancel the worker, I check that if the previous worker is still in Enqueued State then cancel it and create a new chain or workers.
Here is the code.
fun Context.isWorkerRegistered(uuid: UUID?): Boolean {
val id = uuid ?: return false
val workerInfo = workManager.getWorkInfoByIdLiveData(id).value
return workerInfo?.state == WorkInfo.State.ENQUEUED
}
The workInfo instance is always null.
Note: Livedata won't calculate the value until an active observer is added.
getWorkInfoByIdLiveData() returns a LiveData<WorkInfo> that you need to observe to get the workInfo value:
val status = workManager.getWorkInfoByIdLiveData(uuid)
.observe(this, Observer{ workInfo ->
if (workInfo!=null){
// ...
}
}
you can take a look at the WorkManager's codelab to see how it can be used.
I'm using LiveData+Room+ViewModel to build a simple kotlin app. The main activity (which presents a list) is getting the required data from a ViewModel which is getting the info from a database (the data is transformed before being consumed by the activity). Now, I need to allow the user to refresh the data through a swipe. When that happens, the app should check if the current connection can be used for that and if it can't, then the app should schedule a job.
Currently, I'm delegating this work (check the current connection and the eventual job scheduling) to my ViewModel. It looks like this:
fun tryToRefreshDataFromService(){
//first, check if there's network
//If there is, call web service and then update db
//if no network, schedule a job and try to refresh from the database
if(canGetDataFromNetwork()){
Timber.d("With network access, getting data from web services")
WebServiceAsyncTask(newsManager).execute()
}
else{
//schedule job for refreshing
//no network access, setting up job
Timber.d("No network access, setting up job")
scheduleJob()
}
}
The activity will then be able to call the method from within a helper method (which handles the swiper refresh event):
private fun recoverDataForTabs(swiper: SwipeRefreshLayout? = null){
_swiper = swiper //for clearing
_viewModel.tryToRefreshDataFromService()
}
However, it seems like this is really a bad idea because it seems like ViewModels shouldn't know anything about Android framework classes (and that ends up being required for this case). So, does this mean that I should update my code so that the network checking + job scheduling is done from the activity?
Thanks
You can inject framework-related objects into your ViewModels. For example:
class MyViewModel(val networkChecker: IMyNetworkChecker, val jobSetter: IMyJobSetter, ...) {
fun tryToRefreshDataFromService(){
if(networkChecker.canGetDataFromNetwork()){
Timber.d("With network access, getting data from web services")
WebServiceAsyncTask(newsManager).execute()
}
else{
Timber.d("No network access, setting up job")
jobSetter.scheduleJob()
}
}
}
I am working with WorkManager Alpha 05.
I'm developing a Service that enqueues task on demand of other applications.
It has two methods:
createTask (Create a new task, given a name and a set of data, it returns and ID)
checkTaskStatus (The application asks the services given a ID, the status of the task)
The communication is done via bound services using messages. That means both client and services has the correct implementations to communicate information.
Method 1 is working fine.
I have problems with method 2.
WorkManager.getInstance().getStatusById(taskID)
.observe(LifecycleOwner, Observer {
status -> if (status !=null){
val myResult = status.state.toString()
statusString = myResult
Log.d("Task Status",myResult)
}
})
The observer is logging the status correctly, but I can't send back that message to the client. Is there a way to check the status in a sync way?
I don't really need to have the task attached to a LiveData.
Seems like SynchronousWorkManager was removed on October 11:
Removed WorkManager.synchronous() and WorkContinuation.synchronous() and all related methods. Added ListenableFuture as the return type of many methods in the API. This is a breaking API change.
How to use ListenableFuture:
You can now synchronously get and observe by using ListenableFutures. For example, WorkManager.enqueue() used to return void; it now returns a ListenableFuture. You can call ListenableFuture.addListener(Runnable, Executor) or ListenableFuture.get() to run code once the operation is complete.
More info can be found here.
The WorkManager instance has a synchronous method which returns the SynchronousWorkManager, This will give you a set of methods to perform synchronous operations. Take into account that this is meant to be used in a background thread.
I have an app that authenticates using OAuth2 and fetches data from a RESTful service using Retrofit. Now, I have the token retrieval and refreshing up and running. The token is refreshed like so (schedulers omitted):
// Each Retrofit call observable is "wrapper" using this method
protected <T> Observable<T> wrap(#NonNull final Observable<T> page) {
return authenticate()
.concatMap(token -> page)
.onErrorResumeNext(throwable -> {
Log.w(TAG, "wrap: ErrorResumeNext", throwable);
return refreshAccessToken()
.flatMap(accessToken -> page);
}));
}
// Retrieves the access token if necessary
Observable<AccessToken> authenticate() {
// Already have token
if(accessToken != null) return Observable.just(accessToken);
// No token yet, fetch it
return api.getAccessToken(...);
}
// Refreshes the token
Observable<AccessToken> refreshAccessToken() {
return api.refreshToken(...);
}
This works, but in some cases, multiple requests are sent at once and they both invoke the refreshing process - basically my app ends up refreshing the token as many times as there were requests at the moment.
So, the question is: How do I ensure that when the token needs to be refreshed, it is done only once, no matter how many ongoing requests require the token to be refreshed? Can I somehow make the other requests "wait" until the first request sucessfully invoked and retrieved the new token?
We have accomplished this behavior using a hot observable for refreshing the token and providing access to its instance for all requests that failed to authenticate.
Use share operator to turn your basic cold observable for refreshing a token into a hot one, so every other subscriber shares its result. Once the request comes back, all awaiting observers gets notifies and in that moment (in the operator chain it comes right before share() into a callback for doOnUnsubscribe) destroy the refreshing observable instance so the next subscriber will create new one. All this can be easily achieved by a singleton pattern, where you wrap the refreshing observable into a singleton wrapper class and just ask for it through getInstance(). If there is no request going on -- the instance is null -- getInstance should create a new one.
There are some other things you need to take care of, error during refreshing and invalidating the token all together for example, but these are the basics.
I don't have much time right now to elaborate more on this, but if you will encounter some trouble implementing this by your own, leave a comment and I will post some code examples by tomorrow. They wouldn't make much sense without a context.
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.