[MUSIC PLAYING]
BEN WEISS: This video will introduce you
to more of WorkManager's advanced features.
You'll learn how to customize initialization
and how WorkManager supports apps
that span multiple processes.
I'll also show you how to test the worker,
and I'll introduce you to some debugging techniques.
Let's go.
We are at Android Studio with a WorkManager sample
app, which we'll use to guide through this video.
Let's start where it all begins, at initialization.
Under the hood, WorkManager uses the WorkManagerInitializer
provider, which is declared in the work runtime module
and then merged into the app's AndroidManifest.
In most cases, you don't need to configure WorkManager,
as it provides a reasonable default configuration.
But depending on the complexity of your app,
you might need to modify the standard WorkManager behavior,
and this is how you do it.
First, the default provider needs to be disabled.
This is done by removing it from the AndroidManifest,
using the "remove" attribute for this node.
Now you can add your own configuration
in the Application class by implementing
Configuration.Provider and overriding
getWorkManagerConfiguration.
Using this builder, you can make changes
to all defaults of your WorkManager instance.
Since these have been created to work for many apps
on a variety of devices, it makes sense
to only adjust what you need to change for your specific use
case.
The threading behavior can be changed
by providing a separate executor.
This is super useful for testing,
and we'll go over that later.
There's two different executors--
the task executor is used internally
within WorkManager and the executor that
takes care of your workers.
We also cover log levels later in this video.
And you can even change the WorkerFactory.
It's responsible for health and WorkManager
find your worker classes when they have to be instantiated.
The WorkerFactory enables you to change worker names
and redirect and queue jobs correctly.
When a worker is instantiated by WorkManager
once all constraints are met, it does
so using the Worker's fully qualified class name.
Imagine work being enqueued, but before all constraints are met
and the worker would get to run, the app gets updated.
The update contains the usual bug fixes and stability
improvements and also renames a worker.
Without a WorkerFactory pointing to the new class name,
WorkManager could not find the worker anymore.
This would result in WorkManager silently
dropping jobs for this worker.
And that's not something that your users are likely
going to be happy about.
And to avoid this problem, you'll
create a custom WorkerFactory that
can handle renamed workers.
This is our RenameWorkerFactory, which
extends from WorkerFactory and implements createWorker.
When workerClassName content matches the OldWorker,
we return an instance of NewWorker instead.
It's important to return "null" for non-handled cases,
as this lets the default WorkerFactory take over.
And then in the app's configuration,
I set the created WorkerFactory to RenameWorkerFactory.
In the configuration, you can also
set the default process name.
This is useful when working with an app that
runs across multiple processes.
With its 2.5 release, WorkManager
introduced an artifact to improve support
for these applications.
Let's make ImageOperations remote friendly.
All that's needed here is calling RemoteWorkManager
and using the RemoteWorkContinuation.
These classes provide core functionality of WorkManager
and are more performant when it comes
to interprocess communication and more memory
efficient for multi-process apps,
since they don't need to spin up a WorkManager
instance for each process.
Now let's talk about testing.
The sample's lib module contain several workers, but no tests.
So let's write some, starting off with a CleanupWorker,
the class that takes care of removing temporary files.
To test workers, we recommend using WorkManager's own test
dependency, work-testing.
With this dependency in place, we can use TestWorkerBuilders.
If you want to change your threading behavior
for your worker under a test, use the TestWorkBuilder.
For coroutine workers, this is necessary,
and the right builder is TestListenableWorkerBuilder,
which is what we'll use when testing CleanupWorker.
Before each test run, the CleanupWorker gets initialized.
Here you can create a rich configuration
for your worker's needs.
This is a basic worker that doesn't take input or produce
output, so there's no need to configure it further.
With that worker setup, we now can focus
on writing the first test.
CleanupWorker removes all PNGs from a directory.
To test this, we create test files ending in PNG,
and then assert that there are no more PNGs
left after the work is done.
With this scaffold, we can now add
the code we want to test by calling worker.doWork.
But doWork uses coroutines and is a suspend function.
One doesn't simply add the "suspend" keyword to the test,
as this is not what a test framework expects and, as such,
would fail to compile.
This is where coroutine's runBlocking comes in handy.
Any code written in this run will
be executed synchronously on the current thread, blocking it.
And that's exactly what we want for a test like this one.
Now the test passes.
Let's move on to test a worker that
doesn't use coroutines to get its work off the main thread.
The BaseFilterWorker extends from Worker
and provides scaffolding for all filter workers in the sample.
Also, since BaseFilterWorker is an abstract class that
can't be instantiated directly, we
create a concrete test implementation
that overrides applyFilter and echoes back
to Bitmap that was passed in.
Now that it can be instantiated, let's
write a test to verify the worker behaves correctly.
Instead of the TestListenableWorkerBuilder,
we use a TestWorkerBuilder.
The difference between the two is that TestWorkerBuilder also
takes in an executor, which allows test
to run with different threading policy.
And we let this worker run single threaded by passing
in newSingleThreadExecutor.
The worker in the test takes input data,
so that also needs to be passed in as a workDataOf
before calling build.
Then a call to a doWork will execute the worker's job.
Now we can check whether the result is successful.
And starting with WorkManager 2.6,
we also can verify if a result contains the data
that we are looking for by using worker's getOutputData.
Here we're checking whether the KEY_IMAGE_URI
is present at the data object with a value of type String.
You can also test constraints and periodic work.
Let's make the sampler remove stale temporary files roughly
once a day.
This will be taken care of by the CleanupWorker
when some constraints and battery state are met.
Unconstrained workers or unique jobs
can be tested without the need to spin up WorkManager,
but since a worker doesn't know anything about scheduling,
we now need a WorkManager instance.
But instead of waiting for constraints to be fulfilled,
WorkManager provides a testing-specific API.
The work-testing artifact provides the test init helper,
which creates a WorkManager that executes work synchronously,
and a testDriver, which allows changing
of constraints and delays.
The workManager we now get is configured for testing.
Our requests should be executed once a day.
So a PeriodicWorkRequestBuilder of CleanupWorker
with an interval of 24 hours is what
we're going to request from WorkManager.
And we're also requiring the battery
to be charging and not low.
This means the device has been plugged in for a while.
This request now can be enqueued with WorkManager.
By calling "get" on the result, we're
blocking the thread until the request has been enqueued.
This is a big code smell in production,
but in a test environment, that kind of synchronous behavior
is exactly what we want.
Now the work queue can be moved forward
with the testDriver, where we set
the period delay and all constraints
to be met for our given request ID.
And then we check in on the WorkInfo
to see whether the work is actually running.
Now that we have testing under control,
let's investigate how to debug when things
don't work as you intended.
First things first, WorkManager is logging at info level
by default. So in the WorkManager configuration,
let's turn logging up all the way to VERBOSE.
When running the app and executing work,
we now can see what's happening by filtering Logcat
for the WorkManager prefix.
There's a lot going on here.
We want to see that CleanupWorker removes
any temporary files, then filters
should be applied before the result gets saved.
Now let's see if the workers are doing their jobs as intended.
Firstly, there's a CleanupWorker starting its work.
Then it's returning a Success.
This is expected and intended.
Then the next job is enqueued and started.
This is the water color filter, and it is returned successfully
as well.
Same accounts for the grayscale filter.
And finally, the filtered image is saved successfully.
We can even request more information through the Android
debug bridge.
We're using the activity manager to broadcast
the action, request diagnostics for the current app's package.
This will yield even more detailed debugging
logs for jobs that either have completed over the past 24
hours, are currently running, or scheduled.
When work run is deferred, we can take a look
at system logs for JobScheduler as well.
This will provide rough system logs
for any app using JobScheduler.
We're only interested in logs for a single application,
and this can be specified by adding the package
name to that command.
Here we can see when jobs were started and stopped,
as well as whether all the constraints were met.
There are more features scheduled
to improve debugging for WorkManager, so stay tuned.
Now that we have a nicely tested and hopefully bug-free app,
it's a good time to take a look at the sample and Codelabs
linked below.
Next up in this series, we'll show you
how to migrate GCM Network Manager and FCM Job Dispatcher
code to WorkManager.
Subscribe to the Android Developers channel for more,
and hit the Like button if you enjoyed this video.
Thanks for watching.
[MUSIC PLAYING]