Asynchronous Worker in Android WorkManager

纵饮孤独 提交于 2019-11-29 02:58:48

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.

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];

    }

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;
    }
}

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.

I have used BlockingQueue, that simplifies synchronizing threads and passing result, you will need only one object

private var disposable = Disposables.disposed()


override fun doWork(): Result {
    val result = LinkedBlockingQueue<Result>()

    disposable = completable.subscribe(
            { result.put(Result.SUCCESS) },
            { result.put(Result.RETRY) }
    )

    return try {
        result.take()
    } 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()
}

With the power of coroutines, you can 'synchronise' the doWork() like this:

Suspend method for getting the location (asynchronously):

private suspend fun getLocation(): Location = suspendCoroutine { continuation ->
    val mFusedLocationClient = LocationServices.getFusedLocationProviderClient(appContext)
    mFusedLocationClient.lastLocation.addOnSuccessListener {
        continuation.resume(it)
    }.addOnFailureListener {
        continuation.resumeWithException(it)
    }
}

Call example in doWork():

override fun doWork(): Result {
    val loc = runBlocking {
        getLocation()
    }
    val latitude = loc.latitude
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!