Асинхронный рабочий в Android WorkManager

Google недавно анонсировал новый компонент архитектуры WorkManager. Это позволяет легко планировать синхронную работу, внедряя doWork() в Worker класс, но что, если я хочу сделать некоторую асинхронную работу в фоновом режиме? Например, я хочу сделать вызов сетевой службы с помощью Retrofit. Я знаю, что могу сделать синхронный сетевой запрос, но он заблокирует поток и просто будет чувствовать себя не так. Есть какое-то решение или оно просто не поддерживается на данный момент?

9 ответов

Решение

По документам WorkManager:

По умолчанию WorkManager выполняет свои операции в фоновом потоке. Если вы уже работаете в фоновом потоке и нуждаетесь в синхронных (блокирующих) вызовах WorkManager, используйте synchronous() для доступа к таким методам.

Поэтому, если вы не используете synchronous()вы можете безопасно выполнять синхронизацию сетевых звонков с doWork(), Это также лучший подход с точки зрения дизайна, потому что обратные вызовы грязны.

Тем не менее, если вы действительно хотите запускать асинхронные задания из doWork()вам нужно будет приостановить выполнение потока и возобновить его после завершения асинхронного задания, используя wait/notify механизм (или некоторый другой механизм управления потоками, например Semaphore). Не то, что я бы порекомендовал в большинстве случаев.

Как примечание, WorkManager находится в очень ранней альфа.

Я использовал обратный отсчет и ждал, пока это достигнет 0, что произойдет только после того, как асинхронный обратный вызов обновит его. Смотрите этот код:

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

    }

К вашему сведению, теперь есть ListenableWorker, который спроектирован как асинхронный.

Если вы говорите об асинхронной работе, вы можете перенести свою работу в RxJava Observables / Singles.

Существует множество операторов, таких как .blockingGet() или же .blockingFirst()который превращает Observable<T> в блокировку T

Worker выполняет в фоновом потоке, так что не беспокойтесь о NetworkOnMainThreadException,

С помощью сопрограмм вы можете "синхронизировать" doWork() как это:

Приостановить метод получения местоположения (асинхронно):

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

Пример вызова в doWork():

override fun doWork(): Result {
    val loc = runBlocking {
        getLocation()
    }
    val latitude = loc.latitude
}

Я использовал BlockingQueue, что упрощает синхронизацию потоков и передачу результата, вам понадобится только один объект

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

Также не забудьте освободить ресурсы, если ваш работник был остановлен, это главное преимущество перед .blockingGet() как теперь вы можете правильно отменить свою задачу Rx.

override fun onStopped(cancelled: Boolean) {
    disposable.dispose()
}

Это поздно, но это может помочь другим людям,

Вы можете использовать CoroutineWorker а внутри doWork() используйте что-то под названием suspendCancellableCoroutine, он разработан специально для этой цели.

Ниже приведен фрагмент кода:

class FileDownloader(private val appContext: Context, params: WorkerParameters) :
CoroutineWorker(appContext, params) {

   override suspend fun doWork(): Result {

       try {

          suspendCancellableCoroutine<Int> { cancellableContinuation ->

              // Here you can call your asynchronous callback based network

                override fun onComplete() {
                        cancellableContinuation.resumeWith(
                            kotlin.Result.success(100))
                }

                override fun onError(error: Error?) {

                        cancellableContinuation.resumeWithException(
                            error?.connectionException ?: Throwable()
                        )
                   
               }
               
     }

     }catch (e: Exception) {
           return Result.failure()
      }

  return Result.success()
}
}

Здесь Coroutine будет остановлен, пока вы не вызовете cancellableContinuation.resumeWith.

Этот пример может быть полезен тем, кто ищет Firebase и Work Manager. оно используетandroidx.coccurrentпоэтому вам нужно будет [установить] [1] его в своем проекте Android.

      import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.ListenableWorker;
import androidx.work.WorkerParameters;
import androidx.concurrent.futures.CallbackToFutureAdapter;

import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.firebase.firestore.FirebaseFirestore;

public class MessageWorker extends ListenableWorker
{
    // Define the parameter keys:
    public static final String MESSAGE_ID = "messageId";
    public static final String MESSAGE_STATUS = "messageStatus";
    public MessageWorker(@NonNull Context context, @NonNull WorkerParameters 
      workerParams) {
        super(context, workerParams);
         }

    @NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        return CallbackToFutureAdapter.getFuture(completer -> {
            String messageId = getInputData().getString(MESSAGE_ID);
            String messageStatus = getInputData().getString(MESSAGE_STATUS);
            FirebaseFirestore.getInstance()
                    .collection("messages")
                    .document(messageId)
                    .update("status", messageStatus)
                    .addOnSuccessListener(new OnSuccessListener<Void>() {
                        @Override
                        public void onSuccess(Void unused) {
                            completer.set(Result.success());

                        }
                    })
                    .addOnFailureListener(new OnFailureListener() {
                        @Override
                        public void onFailure(@NonNull Exception e) {
                            completer.set(Result.retry());
                        }
                    });

            // This value is used only for debug purposes: it will be used
            // in toString() of returned future or error cases.
            return "startSomeAsyncStuff";
        });

    }


}


  [1]: https://developer.android.com/jetpack/androidx/releases/concurrent#1.0.0

Я также предпочел бы подход, рекомендованный @TomH. Однако я использовал его с Firebase Storage. Использование WorkManager вместе с CountDownlatch помогло мне. Вот фрагмент кода. Бревна сделаны из древесины.

Он возвращает downloadUrl из Firebase в виде строки после завершения задачи, но до того, как рабочий вернет успех.

@NonNull
@Override
public Result doWork() {
    mFirebaseStorage = mFirebaseStorage.getInstance();
    mTriviaImageStorageReference = mFirebaseStorage.getReference().child("images");

    CountDownLatch countDown = new CountDownLatch(2);
    Uri imageUri = Uri.parse(getInputData().getString(KEY_IMAGE_URI));

    try {

    // get the image reference
    final StorageReference imageRef = mTriviaImageStorageReference.child(imageUri.getLastPathSegment());

    // upload the image to Firebase
    imageRef.putFile(imageUri).continueWithTask(new Continuation<UploadTask.TaskSnapshot, Task<Uri>>() {
        @Override
        public Task<Uri> then(@NonNull Task<UploadTask.TaskSnapshot> task) throws Exception {
            if (!task.isSuccessful()) {
                throw task.getException();
            }
            countDown.countDown();
            return imageRef.getDownloadUrl();
        }
    }).addOnCompleteListener(new OnCompleteListener<Uri>() {
        @Override
        public void onComplete(@NonNull Task<Uri> task) {
            if (task.isSuccessful()) {
                Timber.d("Image was successfully uploaded to Firebase");
                Uri downloadUri = task.getResult();
                String imageUrl = downloadUri.toString();

                Timber.d(("URl of the image is: " + imageUrl));

                mOutputData = new Data.Builder()
                        .putString(KEY_FIREBASE_IMAGE_URL, imageUrl)
                        .build();
                countDown.countDown();
            } else {
                Toast.makeText(getApplicationContext(), "upload failed", Toast.LENGTH_SHORT).show();
                countDown.countDown();
            }
        }
    });
    countDown.await();
    return Result.success(mOutputData);

    } catch (Throwable throwable) {
        Timber.e(throwable, "Error uploading image");
        return Result.failure();
    }
}