[MUSIC PLAYING]
SUMIR KATARIA: Hi, everyone.
Welcome to WorkManager: Beyond the Basics.
My name is Sumir Kataria.
RAHUL RAVIKUMAR: Hi.
I'm Rahul Ravikumar.
SUMIR KATARIA: And we're going to be talking a little bit
about WorkManager, which is Jetpack's
deferrable constraint-based background processing library.
And we'll be covering a lot of intermediate and advanced
topics, so we're kind of counting on you
to know a little bit about WorkManager.
So let's talk a little bit about Lights,
or the release highlights of WorkManager
over the last year or so.
So we released WorkManager last year, 1.0 and 2.0.
So 1.0 is a support library version, the old world.
2.0, which is what we're currently developing,
is the AndroidX version.
2.1 brought you on-demand initialization.
We'll be talking a little bit about initialization
later today, because that's an important topic.
2.2, which is our latest stable version,
is introducing support for GcmNetworkManager,
and its optional API 22 and below.
So that adds a lot of reliability
to your WorkManager.
And then 2.3, which is what we're currently developing--
and we hope will be stable later in the year--
brings you progress and setForeground APIs.
So that should very important.
We won't really be talking about that today.
So let's dive into some topics.
And we're going to be covering a broad swath of things.
But they'll all be sort of focusing
on things that make you understand how WorkManager
works under the hood.
And this is the first topic, which is how does WorkManager
interact with the OS?
So you all probably have a simple worker like this.
This is very simplified.
It just-- it's a worker that uploads images.
So this is the basic unit of work.
And you'll probably create a work request using this.
You might add some constraints.
In this case, since it's uploading,
it'll probably be network constraints of some sort.
You'll build it, and you'll enqueue it.
So this is the basic WorkManager workflow.
And what happens at this point?
This is very important to understand,
because it will have repercussions
throughout the rest of your code.
So first is, how do we persist a request?
And the first thing we do is we store it
in the Work Manager database.
This is our source of truth for everything.
Nothing else that we talk about is actually controlling
the source of truth.
So if you want to know if your work is running,
has finished, has failed, has retried five times-- whatever
it is, all of this is stored here.
After that, if you're on a newer API--
API 23 and up-- we'll store it in JobScheduler.
Sorry.
We'll send this request to JobScheduler.
For API 22 and below, we'll check if you have Google Play
services on your device and you're using the [? work-gcm ?]
dependency.
We'll send it to GcmNetworkManager.
Otherwise, we have a custom Alarm Manager and broadcast
receiver implementation.
So after all of this is done, how,
does your work request run?
And we talked about JobScheduler,
GcmNetworkManager, and Alarm Manager.
So these are all things that are outside of your app.
So it's either the OS or it's Play Services.
Something else now knows about your work.
And that thing can wake up your app, if needed,
and ask WorkManager to run your work.
So for example, if you have a network connectivity constraint
on a newer device, JobScheduler will wake up your app,
and tell it, hey, you have a network.
You can do this work.
And then WorkManager will run it.
We also have this thing called a GreedyScheduler.
GreedyScheduler only exists within your app process.
And it does the same stuff.
It'll keep track of some of the constraints.
And it will opportunistically ask
WorkManager to run the work.
But since it's in your process, it's
not going to wake up your app.
It's just an optimized way to eagerly run your work.
And this is very useful because it doesn't
depend on the rest of the OS.
So it's just opportunistically and quickly running your work.
So let's take a brief detour.
And this will tie right back into this concept right here.
So somebody came to me--
an internal team at Google came to me and asked, hey,
we want to run WorkManager experimentally.
We're introducing it in our app, and we'd
like to make sure that we can experiment
and do A/B experiments with it.
So the first idea they came up with
was, what if I stopped calling WorkManager
for an experimental population, so I
can do, like, this A/B test?
What happens here?
So let's go back to that first part of that flowchart.
Your old work will actually still run,
because you've enqueued this work in the past.
Then you say, OK, I'm stopping using
WorkManager for this user.
Well, all of this work is still remembered by the OS,
and it'll start the app process.
And it'll ask WorkManager to run the work.
So that's not what they wanted.
So the second idea was, what if I
don't initialize WorkManager for that experimental population?
OK.
So your old work will still run if you
use on-demand initialization, because that'll initialize
WorkManager on demand.
And we'll talk about initialization a little bit
more, but that's one thing.
And with auto initialization, it'll just throw an exception.
Because then it will say, hey, wait.
You wanted to run this work.
I tried initializing WorkManager,
but it's not initialized.
OK, well, that's an exception.
So they came back with a third idea.
What if I remove WorkManager for that experimental population?
Well, now, things aren't looking right.
So your old work will get ignored,
because your components that are supposed to be in your app
are no longer found by JobScheduler,
GcmNetworkManager, and Alarm Manager.
So they may print something out in [? log-cap ?] saying, oh,
yeah, I couldn't find this thing.
And it'll ignore the work.
But there's a problem here, and it's subtle--
it'll still use system resources.
So I said, for example, that there
is a connectivity constraint.
So JobScheduler is still tracking.
When do I have network?
Oh, I have network.
I'd better go tell this app to run.
And this app-- in this specific case, our app--
doesn't have WorkManager anymore.
So you've used a bunch of system resources to do nothing.
And that's not good citizenship either.
So the answer here is you should cancel all your work requests
to clean up after yourself.
That's what I gave to that team.
And the reason I'm bringing this up
is because you should always realize that WorkManager
is something that exists within your app,
but there are repercussions outside of your app.
The rest of the OS also knows about it.
And it's important to know that.
So a common question people come to me and ask
is why is my work not running?
And there's a wide variety of reasons for this,
and I want to remind you about a few of them.
One is unsatisfied constraints.
And that's very simple, right?
Like, that's the most obvious answer.
If your constraints aren't met, your work won't run.
If you're on a newer device, you can always
dumpsys JobScheduler.
Because we do store our jobs in JobScheduler
and it will tell you what the constraints are that
aren't met.
Another answer might be you're in doze mode.
Remember that WorkManager is for deferrable processing,
so your jobs can be delayed in doze mode to preserve battery.
Similarly, Battery Saver mode, which is on,
like, Pixel devices by default when
your battery gets below 15%.
Background jobs don't run in this case.
So same thing.
It could be that your OS or your app
is just doing too much work.
So JobScheduler only allows a certain number of active jobs
at a time.
So you might be exceeding that.
And that's across all apps.
WorkManager is also limited by the thread pool
you give it at the beginning, when you configure it.
And by default, that's between two and four jobs.
So if you're trying to do 10 things at once,
you know some of them will be in the queue for later.
And you could also have failed or incomplete prerequisites.
So you should always check-- are your prerequisites finished?
Have they all succeeded?
If they haven't succeeded, a failed parent job
will fail all its descendant jobs.
So that's very important.
If you haven't realized that yet,
if you haven't hit this yet, you should always
make sure that happens.
That's the expectation in WorkManager.
And if you're using a unique work with the append policy,
make sure that the parent job hasn't failed.
Because if it fails, everything you keep appending
will keep failing.
And we'll be giving APIs to address this soon.
Because we know a lot of people sort of struggle with this one.
And then the final reason is your app
could be force-stopped.
And a force stop is something that you usually
go from your Settings, and you say force stop this app.
It wipes out all jobs and alarms.
So WorkManager handles this fairly gracefully.
The next time it's initialized, it'll just
reschedule everything for you.
But since this is considered a destructive action,
we can't really control it when it's force-stopped.
We can't wake up your app for you at that point.
That's the intent.
And some devices we know force-stop your app
very aggressively.
If you're running into this, come talk to us.
We have some ideas.
And the corollary to the last point
is why is my work running too often?
This is one thing that also people ask me.
Let's take a look at an example.
So here's an activity on Create, and you're enqueue-ing
a periodic work request.
And this is something we run into fairly often.
This is wrong.
The reason this is wrong is because every time you
enqueue that work request beyond create,
it creates a new periodic work request.
And you just keep having more and more
periodic work happening.
This is the right way to do it.
So you enqueue a unique periodic work request.
And a unique periodic work request,
you can specify what happens if that work request of that name
already exists before.
So we're saying keep the old work request in this case.
So if we've enqueue this before, don't create it again.
Just keep using the one we already have.
And this is the right way to do it.
And over to Rahul.
RAHUL RAVIKUMAR: Yes, thank you, Sumir.
So Sumir touched upon how to experimentally use
work manager.
And he talked about initialization before.
So let's look at initialization in more detail.
There's two types of initialization, the first one
being automatic initialization.
And the second one is on demand.
So let's look at automatic first.
Automatic initialization is useful
when you just want WorkManager to be initialized automatically
with the default configuration.
That is, you don't want to customize anything.
You just want the defaults.
The way this works is we have a content provider
called WorkManager Initializer.
And that gets manifest merged into your app.
And the way content providers work
is content providers get initialized first,
before Application onCreate happens.
So WorkManager initializes onCreate method.
We will call WorkManager.initialize
and specify the default configuration.
And so when Application onCreate happens,
WorkManager is already initialized.
And so when you call WorkManager.getInstance,
at this point, it will always give you a non-null WorkManager
instance.
And that's good.
However, if you notice this flowchart,
we are actually getting your Application onCreate.
So we are adding the cost of initializing WorkManager
to your application create, which is not great.
So to fix that, we give you on-demand initialization.
On-demand initialization is useful
when you want WorkManager to be initialized lazily.
So you don't want it to be initialized the moment
Application onCreate happens.
And you want to also possibly customize
the default configuration.
So let's look at that in more detail.
So the first thing you need to do
is you want to disable the default initializer,
because if you don't, then we'll still use the content provider
to initialize WorkManager.
And that's bad.
So now when you call WorkManager.getInstance
and give it the context, we'll check if it's already
been initialized.
And if it has, then you give that--
we'll give you back that instance.
If it hasn't been already initialized,
then we'll synchronously initialize WorkManager
at that point in time and give you that instance.
And going forward, that will be the instance
that everybody gets.
So that's how on-demand initialization works.
So how do we discover the configuration, though,
because WorkManager needs the configuration?
So the way this works is your application class--
the subtype implements a Configuration.Provider.
And that has a single method called
get WorkManager configuration.
And that gives us the configuration
we need to use to initialize WorkManager.
So first thing we do is get the configuration
from the application.
And then we use that to initialize WorkManager.
If your application class does not actually
implement that interface, then we'll
just use the default configuration,
but still initialize WorkManager.
So let's talk about some pros and cons.
So the pros are obvious for on-demand initialization.
It can lead to a more performant app startup.
That's because we don't actually block your Application onCreate
for initializing WorkManager.
We'll initialize it lazily.
It can also work around a lot of device-specific problems.
And so we have noticed that OEMs play fast and loose
with content provider initialization protocol.
So sometimes you do encounter cases
where WorkManager has not been initialized correctly
or you have too many jobs.
And this is normally because the content provider
does not-- the initialization protocol is not
correctly followed.
So for that, you can use on-demand initialization.
One of the cons is sometimes your application
does get force stopped.
And remember, when a force stop event happens,
all your jobs and alarms get wiped out.
And so usually this is not a problem, because remember,
as Sumir said, WorkManager has a source
of truth, which is the internal database.
So we know what workers exist.
And we know how they need to be scheduled.
So we'll just reschedule everything.
However, that rescheduling only happens when
WorkManager is initialized.
So if you're delaying WorkManager initialization,
the rescheduling is also getting delayed.
This is usually not a problem, because you can just
lazily call WorkManager.getInstance
after the Application on Create.
And we have the-- and you have the APIs to do that.
So switching gears, let's talk a little bit about testing,
because unlike [INAUDIBLE] talk, we actually
have time to talk about testing.
[LAUGHTER]
So when WorkManager was a 1.0 stable and a 2.0 stable,
it had very limited support for testing.
And we know unit tests are very important.
So in 2.1, we revamped our testing story a lot.
So I want to talk about that.
So let's take a worker, for example.
So here I have a test worker, which
extends a coroutine worker and has a suspending do work.
All it does is delay for a second
and return a Result.success.
Now in an ideal world, if I was testing this worker--
forget the fact that this is a worker--
I would just create an instance of this class.
And I would just call doWork and assert that it
returned a result of success.
It would be really that simple.
However, you know that you can't actually
create instances of workers really
easily, because you have to use WorkerFactory.
And you have to provide WorkManager the configuration.
And you have to go through the full worker initialization.
So we made that easy by giving you a new class
called Test Listenable Worker Builder.
So in a test, if I want to test the test worker that I defined,
the first thing I do is use TestListenableWorkerBuilder,
give it the test worker, and supply the context that I want.
And then I set some input data.
And I set some other additional arguments as needed.
And when I call .build, it really gives you a real
instance of Listenable Worker that you can use for testing.
So at this point, I can just call
startWork, which is a method on Listenable Worker.
And I call the suspending await.
Await is suspending.
It will block-- it will suspend until you get a result
from Listenable Worker.
And then once you get that, you can always
assert on the result.
Because await is suspending, I just wrapped my test
in a runBlocking block.
So now my test became so much simpler,
because I don't have to initialize WorkManager.
I don't have to initialize all of the [? infra ?] needed
to initialize a worker.
So let's look at what testing a synchronous worker looks like.
So just like the previous example, unlike--
instead of extending a coroutine worker, I'm extending a worker.
And I have the same do work [INAUDIBLE] return success.
Now, the key difference in coroutine worker and a test
worker here, that extends to the synchronous worker,
is that this runs in a preconfigured executor,
because this is always executed on a background thread.
And so for that, when I am testing this test worker,
the first thing I need to do is to define the executor
that it needs to run in.
So here, I am defining a single-threaded executor.
And then instead of using Test Listenable Worker Builder,
I'm using Test Worker Builder.
And I give it the executor.
And the rest of the code remains the same.
I set some input data.
I set some additional arguments.
And when I call .build, I just get an instance of Listenable
Worker like I did before.
And I call startWork and await.
And the whole thing is surrounded
in a runBlocking block, because await is suspending.
So my test became, again, very elegant and very easy to write.
Sometimes, however, you want to test delays and constraints.
And you want to test how your worker is reacting
to changes in constraints.
And in that case, you want to initialize
WorkManager in test mode.
So that is still the one use case
where you want to initialize WorkManager for testing.
So let's look at an example to that.
So in my test, I have a setup.
So I define the configuration that I
want to initialize WorkManager in test mode with.
And I call WorkManagerTestInitHelper.
And I call initialize WorkManager,
giving it the context and the configuration.
At this point, WorkManager is initialized for testing.
So I can now use WorkManager.getInstance
like I would in a normal application.
So this is what my test would look like.
So I would first create the constraints
that I want to test my worker with.
Then I would create a real instance of One Time Work
Request.
So I would use the OneTimeWorkRequestBuilder.
And then I supply the constraints.
And I would call .build.
At this point, I have a real One Time Work Request.
And I call WorkManager.enqueue.
Because this is a special instance of WorkManager
in test mode, this worker is actually not
going to run until the constraints are met.
And by default, the test WorkManager instance
will treat all constraints as unmet.
So we have to mark those constraints as met.
And for that, you need an instance
of Test Driver, which WorkManager Test Init
Helper will give you.
At that point, you can use the Test Driver.
And you can mark all constraints for the worker
that you just defined as met.
And now you can just use all the rest of the APIs
like you would normally.
So you can just call getWorkInfoById,
give it the request.id.
And then you could get the work info
and then assert that the state of the work request
was success.
And that's it.
SUMIR KATARIA: And yeah, just to finish up,
these are some resources for you.
So if you need to know any documentation,
we have a lot of it at developer.androi
d.com/workmanager.
And these are your artifacts.
So the work-runtime, rxjava2, testing, and GCM artifacts
are all there.
And thank you.
RAHUL RAVIKUMAR: Thank you.
[APPLAUSE]
[MUSIC PLAYING]