Asynchronous Worker in Android WorkManager
Solution 1
Per WorkManager docs:
By default, WorkManager runs its operations on a background thread. If you are already running on a background thread and have need for synchronous (blocking) calls to WorkManager, use synchronous() to access such methods.
Therefore, if you don't use synchronous()
, you can safely perform sync network calls from doWork()
. This is also a better approach from design perspective because callbacks are messy.
That said, if you really want to fire async jobs from doWork()
, you'll need to pause the execution thread and resume it upon async job completion using wait/notify
mechanism (or some other thread management mechanism e.g. Semaphore
). Not something I would recommend in most cases.
As a side note, WorkManager is in very early alpha.
Solution 2
I used a countdownlatch and waited for this to reach 0, which will only happen once the asynchronous callback has updated it. See this code:
public WorkerResult doWork() {
final WorkerResult[] result = {WorkerResult.RETRY};
CountDownLatch countDownLatch = new CountDownLatch(1);
FirebaseFirestore db = FirebaseFirestore.getInstance();
db.collection("collection").whereEqualTo("this","that").get().addOnCompleteListener(task -> {
if(task.isSuccessful()) {
task.getResult().getDocuments().get(0).getReference().update("field", "value")
.addOnCompleteListener(task2 -> {
if (task2.isSuccessful()) {
result[0] = WorkerResult.SUCCESS;
} else {
result[0] = WorkerResult.RETRY;
}
countDownLatch.countDown();
});
} else {
result[0] = WorkerResult.RETRY;
countDownLatch.countDown();
}
});
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return result[0];
}
Solution 3
FYI there is now ListenableWorker, which is designed to be asynchronous.
Edit: Here are some snippets of example usage. I cut out big chunks of code that I think aren't illustrative, so there's a good chance there's a minor error or two here.
This is for a task that takes a String photoKey, retrieves metadata from a server, does some compression work, and then uploads the compressed photo. This happens off the main thread. Here's how we send the work request:
private void compressAndUploadFile(final String photoKey) {
Data inputData = new Data.Builder()
.putString(UploadWorker.ARG_PHOTO_KEY, photoKey)
.build();
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(UploadWorker.class)
.setInputData(inputData)
.setConstraints(constraints)
.build();
WorkManager.getInstance().enqueue(request);
}
And in UploadWorker:
public class UploadWorker extends ListenableWorker {
private static final String TAG = "UploadWorker";
public static final String ARG_PHOTO_KEY = "photo-key";
private String mPhotoKey;
/**
* @param appContext The application {@link Context}
* @param workerParams Parameters to setup the internal state of this worker
*/
public UploadWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
mPhotoKey = workerParams.getInputData().getString(ARG_PHOTO_KEY);
}
@NonNull
@Override
public ListenableFuture<Payload> onStartWork() {
SettableFuture<Payload> future = SettableFuture.create();
Photo photo = getPhotoMetadataFromServer(mPhotoKey).addOnCompleteListener(task -> {
if (!task.isSuccessful()) {
Log.e(TAG, "Failed to retrieve photo metadata", task.getException());
future.setException(task.getException());
return;
}
MyPhotoType photo = task.getResult();
File file = photo.getFile();
Log.d(TAG, "Compressing " + photo);
MyImageUtil.compressImage(file, MyConstants.photoUploadConfig).addOnCompleteListener(compressionTask -> {
if (!compressionTask.isSuccessful()) {
Log.e(TAG, "Could not parse " + photo + " as an image.", compressionTask.getException());
future.set(new Payload(Result.FAILURE));
return;
}
byte[] imageData = compressionTask.getResult();
Log.d(TAG, "Done compressing " + photo);
UploadUtil.uploadToServer(photo, imageData);
future.set(new Payload(Result.SUCCESS));
});
});
return future;
}
}
EDIT
Depending on the things you are using in your application, you can also extends RxWorker (if you are using RxJava) or CoroutineWorker (if you're using Coroutines). They both extend from ListenableWorker.
Solution 4
If you are talking about asynchronus job you can move your work into RxJava Observables / Singles.
There is a set of operators like .blockingGet()
or .blockingFirst()
which transforms Observable<T>
into blocking T
Worker
performs on background thread so do not worry about NetworkOnMainThreadException
.
Solution 5
I have used BlockingQueue
, that simplifies threads synchronization and passing result between threads, you will need only one object
private var disposable = Disposables.disposed()
private val completable = Completable.fromAction {
//do some heavy computation
}.subscribeOn(Schedulers.computation()) // you will do the work on background thread
override fun doWork(): Result {
val result = LinkedBlockingQueue<Result>()
disposable = completable.subscribe(
{ result.put(Result.SUCCESS) },
{ result.put(Result.RETRY) }
)
return try {
result.take() //need to block this thread untill completable has finished
} catch (e: InterruptedException) {
Result.RETRY
}
}
Also don't forget to release resources if your Worker has been stopped, this is the main advantage over .blockingGet()
as now you can properly free cancel your Rx task.
override fun onStopped(cancelled: Boolean) {
disposable.dispose()
}
Anton Tananaev
I am an author of Traccar open source GPS tracking server and Traccar Client phone apps.
Updated on July 30, 2022Comments
-
Anton Tananaev almost 2 years
Google recently announced new
WorkManager
architecture component. It makes it easy to schedule synchronous work by implementingdoWork()
inWorker
class, but what if I want to do some asynchronous work in the background? For example, I want to make a network service call using Retrofit. I know I can make a synchronous network request, but it would block the thread and just feels wrong. Is there any solution for this or it is just not supported at the moment? -
Usman Rana about 6 yearscan you answer this:stackoverflow.com/questions/50580106/…
-
Nitish over 5 yearsWhat happens when Constraint fails. Means Constraint For ideal state then work manager triggers . and after some time Phone out of Ideal state.
-
idish over 5 yearsCan you please add an example on how to use this class?
-
Bartholomew Furrow over 5 years@idish I've added an example.
-
David Vávra over 5 yearsI can't use SettableFuture.create() in alpha-13, the class is restricted only to the same library group.
-
idish over 5 yearsIndeed
SettableFuture.create();
module is private only to the WorkManager library group. Cannot be used. -
idish over 5 yearsUsing synchronous over async API when running from a background thread is not always good enough. For example, certain async APIs have some
onProgress
callbacks will be called on the main thread, heads up with that. -
A.Sanchez.SD over 5 yearsIt's not actually private. stackoverflow.com/questions/43656617/… Just suppress the warning "@SuppressLint("RestrictedApi")"
-
Abhay Pai over 5 yearsThe task is executed on the main thread developer.android.com/reference/androidx/work/ListenableWorker. They say that
The startWork() method is called on the main thread.
Also I am unable to see anyonStartWork
in the class. Can you explain this? -
Usman Rana about 5 yearscan ffmpeg command be executed using RxJava? as it is already async method having a callback
-
Admin about 4 yearsI tried your way, but my doWork gets called multiple times,
-
Vinayak almost 4 yearsCan you please add more code for same.This is quite abstract
-
Duong.Nguyen over 3 yearsWe can use ListenableWorker, you can see document here: developer.android.com/topic/libraries/architecture/workmanager/…
-
Maher Abuthraa over 2 yearsVery elegant solution 👍🏼
-
Webfreak over 2 yearsThanks @MaherAbuthraa, I've also updated the answer to use the built in suspend worker.