Асинхронный рабочий в Android WorkManager
Google недавно анонсировал новый компонент архитектуры WorkManager. Это позволяет легко планировать синхронную работу, внедряя doWork()
в Worker
класс, но что, если я хочу сделать некоторую асинхронную работу в фоновом режиме? Например, я хочу сделать вызов сетевой службы с помощью Retrofit. Я знаю, что могу сделать синхронный сетевой запрос, но он заблокирует поток и просто будет чувствовать себя не так. Есть какое-то решение или оно просто не поддерживается на данный момент?
9 ответов
По умолчанию 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();
}
}