- WorkManager basics
- Starting with an example
- Adding the WorkManager dependency
- Define what your work does
- Define how your work should run
- Running work
- Behind the Scenes — How work runs
- Using Chains for dependent work
- Observing your WorkRequest status
- Conclusion
- Полный список
- Задача
- Статус задачи
- Результат
- Отмена задачи
- setInitialDelay
- Периодическая задача
- Context
- Перезагрузка
WorkManager basics
Welcome to the second post of our WorkManager series. WorkManager is an Android Jetpack library that runs deferrable, guaranteed background work when the work’s constraints are satisfied. WorkManager is the current best practice for many types of background work. In the first blog post, we talked about what WorkManager is and when to use WorkManager.
In this blog post, I’ll cover:
- Defining your background task as work
- Defining how specific work should run
- Running your work
- Using Chains for dependent work
- Observing your work’s status
I’ll also explain what’s going on behind the scenes with WorkManager, so that you can make informed decisions about how to use it.
Starting with an example
Let’s say you have an image editing app that lets you put filters on images and upload them to the web for the world to see. You want to create a series of background tasks that applies the filters, compresses the images, and then uploads them. In each phase, there is a constraint that needs to be checked — that there is sufficient battery when you are filtering the images, that you have enough storage space when compressing the images, and that you have a network connection when uploading the images.
This is an example of a task that is:
- Deferrable, because you don’t need it to happen immediately, and in fact might want to wait for some constraints to be met (such as waiting for a network connection).
- Needs to be guaranteed to run, regardless of if the app exits, because your users would be pretty unhappy if their filtered images are never shared with the world!
These characteristics make our image filter and uploading tasks a perfect use case for WorkManager.
Adding the WorkManager dependency
The code snippets in this blog post are in Kotlin, using the KTX library (KoTlin eXtensions). The KTX version of the library provides extension functions for more concise and idiomatic Kotlin. You can use the KTX version of WorkManager using this dependency:
You can find the latest version of the library here. If you want to use the Java dependency, just remove the “-ktx”.
Define what your work does
Let’s focus on how you execute one piece of work, before we get to chaining multiple tasks together. I’ll zoom in on the upload task. First, you’ll need to create your own implementation of the Worker class. I’ll call our class UploadWorker , and override the doWork() method.
- Define what your work actually does.
- Accept inputs and produce outputs. Both inputs and outputs are represented as key, value pairs.
- Always return a value representing success, failure, or retry.
Here’s an example showing how to implement a Worker that uploads an image:
Two things to note:
- The input and output are passed as Data , which is essentially a map of primitive types and arrays. Data objects are intended to be fairly small — there’s actually a limit on the total size that can be input/output. This is set by the MAX_DATA_BYTES . If you need to pass more data in and out of your Worker , you should put your data elsewhere, such as a Room database. As an example, I’m passing in the URI of the image above, and not the image itself.
- In the code I show two return examples, Result.success() and Result.failure() . There’s also a Result.retry() option which will retry your work again at a later time.
Define how your work should run
While a Worker defines what the work does, a WorkRequest defines how and when work should be run.
Here’s an example of creating a OneTimeWorkRequest for your UploadWorker . It is also possible to have a repeating PeriodicWorkRequest :
This WorkRequest takes in the imageData: Data object as input and runs as soon as possible.
Let’s say the UploadWork shouldn’t always just run immediately — it should only run if the device has a network connection. You can do this by adding a Constraints object. You can create a constraint like this:
Here’s an example of other supported constraints:
Finally, remember Result.retry() ? I said earlier that if a Worker returns Result.retry() , WorkManager will reschedule the work. You can customize the backoff criteria when you make a new WorkRequest . This allows you to define when the work should be retried.
The backoff criteria is defined by two properties:
- BackoffPolicy, which by default is exponential, but can be set to linear.
- Duration, which defaults to 30 seconds.
The combined code for enqueuing your upload work, with constraints, input and a custom back-off policy, is:
Running work
This is all well and good, but you haven’t actually scheduled your work to run yet. Here’s the one line of code you need to tell WorkManager to schedule your work:
You first need to get the instance of WorkManager , which is a singleton responsible for executing your work. Calling enqueue is what starts the whole process of WorkManager tracking and scheduling work.
Behind the Scenes — How work runs
So what can you expect WorkManager to do for you? By default, WorkManager will:
- Run your work off of the main thread (this assumes you are extending the Worker class, as shown above in UploadWorker ).
- Guarantee your work will execute (it won’t forget to run your work, even if you restart the device or the app exits).
- Run according to best practices for the user’s API level (as described in the previous article).
Let’s explore how WorkManager ensures your work is run off of the main thread and is guaranteed to execute. Behind the scenes, WorkManager includes the following parts:
- Internal TaskExecutor: A single threaded Executor that handles all the requests to enqueue work. If you’re not familiar with Executors you can read more about them here.
- WorkManager database: A local database that tracks all of the information and statuses of all of your work. This includes things like the current state of the work, the inputs and outputs to and from the work and any constraints on the work. This database is what enables WorkManager to guarantee your work will finish — if your user’s device restarts and work gets interrupted, all of the details of the work can be pulled from the database and the work can be restarted when the device boots up again.
- WorkerFactory**: A default factory that creates instances of your Worker s. We’ll cover why and how to configure this in a future blog post.
- Default Executor**: A default executor that runs your work unless you specify otherwise. This ensures that by default, your work runs synchronously and off of the main thread.
** These are parts that can be overridden to have different behaviors.
When you enqueue your WorkRequest :
- The Internal TaskExecutor immediately saves your WorkRequest info to the WorkManager database.
- Later, when the Constraints for the WorkRequest are met (which could be immediately), the Internal TaskExecutor tells the WorkerFactory to create a Worker .
- Then the default Executor calls your Worker ’s doWork() method off of the main thread.
In this way, your work, by default, is both guaranteed to execute and to run off of the main thread.
Now if you want to use some other mechanism besides the default Executor to run your work, you can do so! There’s out of the box support for coroutines ( CoroutineWorker ) and RxJava ( RxWorker ) as means of doing work.
Or you can specify exactly how work is executed by using ListenableWorker . Worker is actually an implementation of ListenableWorker that defaults to running your work on the default Executor and thus synchronously. So if you want full control over your work’s threading strategy or to run work asynchronously, you can subclass ListenableWorker (the details of this will be discussed in a later post).
The fact that WorkManager goes to the trouble of saving all of your work’s information into a database is what makes it perfect for tasks that need to be guaranteed to execute. This is also what makes WorkManager overkill for tasks that don’t need that guarantee and just need to be executed on a background thread. For example, let’s say you’ve downloaded an image and you want to change the color of parts of your UI based off of that image. This is work that should be run off of the main thread, but, because it’s directly related to the UI, does not need to continue if you close the app. So in a case like this, don’t use WorkManager — stick with something lighter weight like Kotlin coroutines or creating your own Executor .
Using Chains for dependent work
Our filter example included more than just one task — we wanted to filter multiple images, then compress, then upload. If you want to run a series of WorkRequest s, one after the other or in parallel, you can use a chain. The example diagram shows a chain where you have three filter tasks run in parallel, followed by a compress task and an upload task, run in sequence:
This is super easy with WorkManager. Assuming you have created all your WorkRequests with the appropriate constraints, the code looks like:
The three filter-image WorkRequest s execute in parallel. Once all three filter WorkRequests are finished (and only if all three finish), the compressWorkRequest happens, followed by the uploadWorkRequest .
Another neat feature of chains is that the output of one WorkRequest is given as input to the next WorkRequest . So assuming you set your input and output data correctly, like I did above with my UploadWorker example, these values will get passed along automatically.
For handling output from the three filter work requests run parallel, you can use an InputMerger , specifically the ArrayCreatingInputMerger . This looks like:
Notice that the InputMerger is added to the compressWorkRequest , not the three filter requests that are run in parallel.
Let’s assume that the output of each of the filter work requests is the key “KEY_IMAGE_URI” mapped to an image URI. What adding the ArrayCreatingInputMerger does is it takes the outputs from requests run in parallel and when those outputs have matching keys, it creates an array with all of the output values, mapped to the single key. Visualized this looks like:
So the input to compressWorkRequest will end up being the pair of “KEY_IMAGE_URI” mapped to an array of filtered image URIs.
Observing your WorkRequest status
The easiest way to observe work is by using the LiveData class. If you’re not familiar with LiveData , it’s a lifecycle-aware observable data holder — and it’s described in more detail here.
Calling getWorkInfoByIdLiveData returns a LiveData of WorkInfo . WorkInfo includes the output data and an enum representing the state of the work. When the work finishes successfully, its’ State is SUCCEEDED . So, for example, you could automatically display that image when the work is done by writing some observation code like:
A few things to note:
- Each WorkRequest has a unique id and that unique id is one way to look up the associated WorkInfo .
- The ability to observe and be notified when the WorkInfo changes is a feature provided by LiveData .
Work has a lifecycle, represented by different State s. When observing the LiveData you’ll see those states; for example you might see:
The “happy path” of states that work goes through are:
- BLOCKED : This state occurs only if the work is in a chain and is not the next work in the chain.
- ENQUEUED : Work enters this state as soon as the work is next in the chain of work and eligible to run. This work may still be waiting on Constraint s to be met.
- RUNNING : In this state, the work is actively executing. For Worker s, this means the doWork() method has been called.
- SUCCEEDED : Work enters this terminal state when doWork() returns Result.success() .
Now when the work is RUNNING , you might call Result.retry() . This will cause the work to go back to ENQUEUED . The work can also be CANCELLED at any point.
If the work result is a Result.failure() instead of a success, its state will end in FAILED . The full flowchart of states therefore looks like this:
For an excellent video explanation, check out the WorkManager Android Developer Summit talk.
Conclusion
That’s the basics of the WorkManager API. Using the snippets we just covered you can now:
- Create Worker s with input and output.
- Configure how your Worker s will run, using WorkRequest s, Constraint s, starting input and back off policies.
- Enqueue WorkRequest s.
- Understand what WorkManager does under the hood, by default, in respect to threading and guaranteed execution.
- Create complex chains of interdependent work, running both sequentially and in parallel.
- Observe your WorkRequest s status using WorkInfo .
Want to try WorkManager yourself? Check out the codelab, which is in both Kotlin and Java.
Stay tuned for more blog posts about WorkManager topics as we continue this series. Have a question or something you’d like us to cover? Let us know in the comment section!
Источник
Полный список
В этом уроке знакомимся с WorkManager.
Полный список уроков курса:
Немаловажная часть работы приложения — это фоновая работа. Это может быть загрузка или аплоад, сжатие или распаковка, синхронизация и т.п. Когда-то давно для фоновой работы были предназначены сервисы. Но в Android 8 их очень сильно ограничили: если приложение не активно, то и сервис будет остановлен через какое-то время. Да и еще задолго до Android 8 разработчики начали использовать такие инструменты как JobScheduler или Firebase JobDispatcher для запуска фоновых задач.
WorkManager — новый инструмент. Он позволяет запускать фоновые задачи последовательно или параллельно, передавать в них данные, получать из них результат, отслеживать статус выполнения и запускать только при соблюдении заданных условий.
При этом он очень простой в использовании. Я рассчитываю, что мне хватит 4 небольших урока, чтобы рассмотреть все его возможности.
Задача
Давайте создадим и запустим фоновую задачу.
Добавьте в dependencies
Создаем класс, наследующий класс Worker:
В метод doWork нам предлагается поместить код, который будет выполнен. Я здесь просто ставлю паузу в 10 секунд и возвращаю результат SUCCESS, означающий, что все прошло успешно. Нам не надо заморачиваться с потоками, т.к. код будет выполнен не в UI потоке.
Задача готова. Теперь нам нужно MyWorker обернуть в WorkRequest:
WorkRequest позволяет нам задать условия запуска и входные параметры к задаче. Пока что мы ничего не задаем, а просто создаем OneTimeWorkRequest, которому говорим, что запускать надо будет задачу MyWorker.
OneTimeWorkRequest не зря имеет такое название. Эта задача будет выполнена один раз. Есть еще PeriodicWorkRequest, но о нем чуть позже.
Теперь можно запускать задачу:
Берем WorkManager и в его метод enqueue передаем WorkRequest. После этого задача будет запущена.
20:37:36.567 5369-5444 doWork: start
20:37:46.568 5369-5444 doWork: end
Видно, что задача выполнялась 10 секунд, и код выполнялся не в UI потоке.
Статус задачи
WorkManager предоставляет возможность отслеживать статус выполнения задачи. Например в Activity пишем:
В метод getStatusById необходимо передать ID задачи, который может быть получен методом WorkRequest.getId. В результате мы получаем LiveData, подписываемся на него и в метод onChanged нам будут приходить все изменения статуса нашей задачи. Методом WorkStatus.getState будем получать текущее состояние.
20:52:54.189 6060-6060 onChanged: ENQUEUED
20:52:54.199 6060-6087 doWork: start
20:52:54.203 6060-6060 onChanged: RUNNING
20:53:04.200 6060-6087 doWork: end
20:53:04.211 6060-6060 onChanged: SUCCEEDED
Сразу после вызова метода enqueue задача находится в статусе ENQUEUED. Затем WorkManager определяет, что задачу можно запускать и выполняет наш код. В этот момент статус меняется на RUNNING. После выполнения статус будет SUCCEEDED, т.к. мы вернули такой статус в методе doWork.
Статус нам приходит в UI потоке.
Теперь еще раз запустим задачу и закроем приложение:
20:58:19.402 doWork: start
20:58:19.424 onChanged: ENQUEUED
20:58:19.462 onChanged: RUNNING
20:58:29.403 doWork: end
Обратите внимание, задача завершилась, а статус SUCCEEDED не пришел. Почему? Потому что, закрыв Activity мы всего лишь отписались от LiveData, который передавал нам статусы задачи. Но сама задача никуда не делась. Она никак не зависит от приложения и будет выполняться, даже если приложение закрыто.
Результат
Мы в нашей задаче возвращали статус WorkerResult.SUCCESS, тем самым сообщая, что все ок. Есть еще два варианта:
FAILURE — в этом случае после завершения задачи workStatus.getState вернет FAILED. Для нас это сигнал, что задача не была выполнена.
RETRY — а этот результат является сигналом для WorkManager, что задачу надо повторить. В этом случае workStatus.getState вернет нам статус ENQUEUED — т.е. задача снова запланирована.
Я протестировал на эмуляторе поведение при RETRY: первый раз задача была перезапущена примерно через одну минуту после предыдущего завершения. С каждым последующим перезапуском интервал увеличивался:
21:10:22.637 doWork: start
21:10:32.638 doWork: end
21:11:32.655 doWork: start
21:11:42.657 doWork: end
21:14:07.538 doWork: start
21:14:17.543 doWork: end
21:18:17.561 doWork: start
21:18:27.602 doWork: end
21:26:27.618 doWork: start
21:26:37.653 doWork: end
Отмена задачи
Мы можем отменить задачу методом cancelWorkById, передав ID задачи
При этом в классе MyWorker будет вызван метод onStopped (если вы его реализовали). Также в классе MyWorker мы всегда можем использовать boolean метод isStopped для проверки того, что задача была отменена.
Если отслеживаем статус задачи, то WorkStatus.getState вернет Cancelled.
Также есть метод cancelAllWork, который отменит все ваши задачи. Но хелп предупреждает, что он крайне нежелателен к использованию, т.к. может зацепить работу библиотек, которые вы используете.
Задаче можно присвоить тег методом addTag:
Одной задаче можно добавлять несколько тегов.
У WorkStatus есть метод getTags, который вернет все теги, которые присвоены этой задаче.
Присвоив один тег нескольким задачам, мы можем всех их отменить методом cancelAllWorkByTag:
setInitialDelay
Выполнение задачи можно отложить на указанное время
В методе setInitialDelay мы указали, что задачу следует запустить через 10 секунд после передачи ее в WorkManager.enqueue
Периодическая задача
Рассмотренный нами OneTimeWorkRequest — это разовая задача. А если нужно многократное выполнение через определенный период времени, то можно использовать PeriodicWorkRequest:
В билдере задаем интервал в 30 минут. Теперь задача будет выполняться с этим интервалом.
Минимально доступный интервал — 15 минут. Если поставите меньше, WorkManager сам повысит до 15 минут.
WorkManager гарантирует, что задача будет запущена один раз в течение указанного интервала. И это может случиться в любой момент интервала — через 1 минуту, через 10 или через 29.
С помощью параметра flex можно ограничить разрешенный диапазон времени запуска.
Кроме интервала в 30 минут дополнительно передаем в билдер flex параметр 25 минут. Теперь задача будет запущена не в любой момент 30-минутного интервала, а только после 25-й минуты. Т.е. между 25 и 30 минутами.
Context
Чтобы получить Context в Worker классе, используйте метод getApplicationContext.
Перезагрузка
Что происходит с запланированными задачами при перезагрузке устройства? Я протестил этот кейс на эмуляторе и выяснил, что все задачи сохраняются. Т.е. OneTimeWorkRequest c отложенным запуском, OneTimeWorkRequest с результатом RETRY, PeriodicWorkRequest — все эти задачи будут снова запущены после перезагрузки устройства.
Поэтому действуйте обдуманно и храните где-то у себя ID или тэг задачи, чтобы вы могли ее отменить, если она вам больше не нужна.
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Источник