Working with WorkManager (Android Dev Summit '18)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[MUSIC PLAYING] SUMIR KATARIA: Thanks for coming. Welcome to Working with WorkManager session. My name is Sumir Kataria. I'm a software engineer at Google. RAHUL RAVIKUMAR: Hi. My name is Rahul Ravikumar, and I'm also a software engineer at Google. SUMIR KATARIA: And we both work on Android architecture components, particularly WorkManager. Let me grab my-- All right. So what are we going to talk about today? We're going to give you a State of the Union about WorkManager. It was released at Google I/O. I want to give you an abbreviated guide to WorkManager. So for those of you who haven't used WorkManager before, we want to go over some of the basic APIs. And we're going to spend most of the time talking about WorkManager, the questions you've asked, the things that have been frequently misunderstood, and also, the new changes that we've made since I/O. So let's start with the State of the Union. There have been 11 releases of WorkManager since Google I/O, and these are alpha releases. Today was the 11th one. And beta's coming soon. So those of you who watched the keynote yesterday may have heard that beta's coming this month. This was news to us, too. But it's coming this month. [LAUGHTER] We were very close, but I guess we're doing it this month now. [LAUGHTER] So we want to give you an abbreviated guide to WorkManager. And what is it, for those of you who are just completely new to this? It's a library for managing deferrable background work. It wraps JobScheduler, and AlarmManager, BroadcastReceivers. It's backwards compatible to API 14. And those of you who've used JobScheduler will find many of the concepts very familiar. So let's talk about work. You've got a unit of background work-- and we'll talk a little bit more about how we create it and how we enqueue it later-- but you've got it, and how does WorkManager execute this work? This is a graph that might help you. While your process is up and running, we'll feed it to this executor that we have that does the work. It could be a thread pull. You can customize this thing. But your process may be killed. And WorkManager is guaranteed to do work. It may defer it, but it will do that work when the constraints for that work are met. So we also enqueue it in the background. So if you're API 23+, we use JobScheduler. And before that, we use an AlarmManager and BroadcastReceiver implementation. So whenever the signals are met-- let's say that you've put in some work for a two-hour time delay when you have network-- When all of those conditions are met, it'll still go to the same executor. All right. RAHUL RAVIKUMAR: Let's do a quick API walkthrough for those who have not seen the API. So the fundamental unit of work in WorkManager is a Worker. Here I'm defining a calculation Worker. It extends to Worker type. So now, the only thing that you need to do when you define a new Worker is to extend doWork. And doWork returns a result. Here I'm returning a success. You can also return a retry or a fail. I'm doing some expensive calculation on background threads. So you don't have to worry about threading here, because WorkManager is guaranteed to schedule your work on a background thread. And here I am returning a result of success synchronously. So that's it. I'm pretty much good to go. So now, you've defined a Worker. How do you actually make that work run? So for that you need to enqueue a work request. So there are two types of work requests-- one is a OneTime, and the other one is a PeriodicWork request. Here I am using a OneTime work request builder. And I'm building it with the calculation Worker that I just defined in the previous slide. So I'm setting an initial delay. And this is a timing-based constraint. And this tells WorkManager to only run the work after two hours have passed since the point of enqueue. And I'm also setting another constraint, which is the charging constraint. This tells WorkManager that this work is only eligible to run when the device is actually connected to-- when the device is actually charging. I add a tag. I'll talk about tags in more detail. And I'm finally calling dot build. Now, all I need to do is to call getInstance and enqueue the work. And that's it. Now the work is scheduled. So now you've schedule your work. You've defined a bunch of constraints. And it's hard to keep track. And if you want to keep track of what state your work is in, then you want to call getWorkInfo by real live data. And this returns live data of workInfo. And so workInfo is the type that determines the state of your work. And live data here is a life cycle of our observable. So once you attach it to a life cycle owner and you define an observer, you can observe the state transitions of the work request. So it'll go something like-- once you enqueue the work, it'll go into enqueue. Once the constraints are met, it'll go into a running. And then finally, because you return success, it is going to succeed at state. So remember that tag that we added when we built the OneTime work request? You can also get work infos by tag, live data. And tag is something that you associate with a work request. And there can be one to many. So we can associate the same tag to multiple work requests. And here I'm calling it workInfos by tag live data, which runs a live data again. Notice how it returns a list of workInfo and not a single workInfo. And again, I can do the same things that I did in the previous slide. So one of the coolest features of WorkManager is the ability to chain work. So that helps you define an acyclic graph of work. And here I'm asking WorkManager to begin with a, b, and c. And here, a, b, and c are work requests. And I'm seeing that d and e are only eligible to run once all a, b, and c are done. And when I used to begin with API, I'm asking WorkManager to run a, b, and c potentially in parallel. Whether they actually run in parallel is determined by the capabilities of the device and the size of your thread pull. And finally, I'm calling [INAUDIBLE] on f, g, and h again. Now, f, g, and h will only run once all the preceding works are done. Finally, don't forget to call enqueue. That's when all the magic happens. So now you called enqueue. Now, you might be wondering what begin with returns. So begin with actually returns an instance of work continuation. And a work continuation is a node in your [INAUDIBLE] graph. And this lends to a very fluent API. So every time you call begin with, it returns a new continuation. And every time you call dot den, it returns a new instance of another continuation. Finally, don't forget to call enqueue. The one important thing that you need to remember is, when you chain work, the outputs of the parent work request become inputs to your descending work request, or your children. This helps you manage state and send states from parent work to the descendant work. Now, finally, WorkManager also exposes the APIs to cancel work. So for some reason if you want to do that, you can cancel work by ID. And every work request has a unique ID. So here we are canceling work by that ID. And we can also cancel all work by tag. So those are the two APIs. And that's it. SUMIR KATARIA: And I want to point out that all the APIs as we've been showing are for alpha 11, so you may notice some slight changes, for those of you who have been using this API before. But everything is live today. So let's talk a little bit about how you can get the most out of WorkManager. Manager And these also include questions like, how do I do a certain type of task? Why does this work a certain way? And the biggest one that we get a question about is threading. How does threading work in WorkManager? So we talked about a work request. Rahul just mentioned that. And you enqueue it. What happens to it then? So we have an internal task executor. You can think of it as a single-threaded executor. And the enqueue goes to that, which stores it in a local database. So every app that uses WorkManager has a WorkManager database. This is the source of truth. This is where we keep track of the state of your work-- your inputs and outputs, everything with the dependency chains. Everything goes in this database. So after it's been enqueued, sometime later your constraints are met. And the OS tells you that. If you have no constraints, it's eligible to run right away. If there are constraints, the OS will tell you, oh. You have network. You are charging right now. Whatever it is. Same task executor-- uses a WorkerFactory to create a Worker. WorkerFactory is exactly what it is. It's a factory for Workers. And you can make your own. You can customize and do things with that. We'll talk more about that later. After the Worker's been created, we execute it on an executor. And this is also a thing that you can actually customize. We'll talk more about that later as well. But what if you don't want to execute something on that executor? We give you a default one. You can specify your own, but what if you're using our Ex.java? What if you're using code routines? What if you have your own bespoke solution that you want to use to run things in the background? This was a request that came up quite a lot when we first released WorkManager. So to do this, we want to provide you an API to let you do work on your own and just tell us when it's done. So you want to signal async work completion to us. And for that, we use a class called ListenableFuture. Those of you who use Guava will be very familiar with this. But we split it out-- And the Guava team has helped us with this, so everybody's collaboratively worked on splitting this out to its own lightweight artifacts. So you don't need a full Guava dependency for this. It's literally one or two classes now. And it's a future that can have one or more listeners, and those listeners can be invoked on a specified executor. That's all it is. It's very simple. So using this ListenableFuture, we made a class called ListenableWorker, which only has one method that you need to override. It's called startWork. We'll call startWork on the main thread for you. You give us back a ListenableFuture. You do whatever work you want on whatever thread you want, and when you're done, just set the result on the future. And we'll be able to listen for it, and we'll be able to react to it. So the actual threading model in WorkManager is, after your constraints are met, it goes to the task executor, which uses the WorkerFactory to create a ListenableWorker. We call startWork on it and we also attach a listener. So we can listen to whenever you're done. What this means is that the Worker class, which is still around, is a simple ListenableWorker. So we've got the doWork method that we talked about. We override the startWork for you. We create a future. On that background executor that I talked about-- the one we provide by default-- we execute the work. And we, of course, return the future. So now, we have two classes-- Worker and ListenableWorker. What's the differences between them? Workers, we consider those a simple class. For most use cases, we think that's sufficient, is a class that runs synchronously and on a pre-specified background thread. ListenableWorker runs asynchronously on an unspecified background thread. So Worker dot doWork is a synchronous API. You're expected to finish what you're doing there. If you're trying to create a ListenableWorker, you may need to return a ListenableFuture, which is an interface. If you have access to Guava, you have access to many kinds of ListenableFutures. If you don't have access to Guava, or don't want to add the dependency, ResolvableFuture is a lightweight implementation that we provide in Android X concurrent concurrent futures. So you can use that. So let's look at an example. One of the things that a lot of people are trying and doing incorrectly with Workers is that they're trying to get locations. And we're going to specifically use the Fused Location provider client. So if you listen to the Kotlin Suspenders talk yesterday, they also used this. It's a JMS core API to asynchronously get your location. Remember that a Worker class is synchronous. So if you attach a callback and later get informed about it, but return success, your work is already completed. It's not going to work the way you think it is. So first thing we do here-- we are using a ListenableWorker-- is we create a ResolvableFuture. So this is the future that we'll return and do all our bookkeeping on. In the startWork method we'll check to see if we have permissions. If we don't, we'll set a failure on the future. Otherwise, what we'll do is, we'll get that Fused Location provider's last location. This is kind of like a future in JMS core-- or Google Play services world. We'll call this getLocation method, which I'll go into. And then we return to future. So that's the high-level startWork. In the getLocation method, we'll use that task and we'll add a listener to it. If the task is successful, we'll pass that location back with a success status. Otherwise, we'll set an exception on the future. So that's it. We've basically addressed the three cases where we want to have a successful or an unsuccessful task, or if we don't have permissions. And WorkManager will attach that listener. It'll listen to the success or failure of your task and do the bookkeeping as necessary. All right. RAHUL RAVIKUMAR: Let's talk about operations. Now, remember, Sumir mentioned that WorkManager uses the database as a source of truth. So any time you enqueue or you cancel work, we have to do some bookkeeping, and we have to make sure we keep track of what your intentions were. So these involve rights to a database, and because there are rights to a database, we have to do them on a background thread. So as a result, they're asynchronous. What if you actually wanted to do something after the enqueue happened, or the cancel happened? So you want to make sure that those operations completed before you want to do some more stuff. So for that, you've introduced a new API. So now, enqueue and cancel-- actually return a new type called operation. Operation is a very simple interface. It has two methods-- so it has a getState API, which returns a live data of an operation dot state. If you attach an observer to this live data, you will see that the operation transitions from an in progress to a successful or a failure. You can also call getResult, and this returns the familiar ListenableFuture type. And remember that this API will only return the terminal state of the operation. It won't give you the intermediate state. So if you're attaching a listener, you'll only ever get a success or a failure. And with the exception-- telling you why the failure happened. SUMIR KATARIA: Another question a lot of people have is, when is work stopped? What happens when you stop work on behalf of WorkManager? All right. So there's three cases when work is stopped. The first is very simple-- your constraints are no longer met. So you said, for example, that I need a network to do this upload task, but your network got lost. So we both stop your work at that point. A second case is that they always preempted your work for some reason-- for example, you exceeded the 10-minute time limit that the OS gives you to do your work. Or the device decided to go into doze mode for battery savings, or something like that. And the third reason is that you just canceled your work somewhere else in your app. How do we stop work? There is a method ListenableWorkeronStopped. We call this method when we stop. So just override this. Then you get your stop signal right there. We also cancel [INAUDIBLE] future that we talked about. So you can also just add your own listener and look for that. So this is when one of these two things happens for you, whichever one you're looking at. This is your signal to be a good citizen and clean up. Because after this is called, the process may be killed by the OS. So if the OS woke up your app's process just to run this work, it could actually kill it when it decides that the work should stop. And if you happen to return something after this signal-- say you return a success-- we ignore it, because as far as we're concerned, your work has been stopped. Whatever you are doing is no longer necessary to be done. You can also pull for stoppages in your Worker. So you can also call the isStopped method. And that will tell you whether you've been signaled for stopping. So let's look at how you can be a good citizen and clean up. So let's say in this example we're parsing a file asynchronously. Let's say it's a huge file, so you're doing it in a Worker. And you've got that familiar ResolvableFuture thing that I showed you earlier. You've also got this input stream, so you're reading the file, right? So in startWork you say, parseFile. This is doing something asynchronously, and you return to future. Here's parseFile. So you've got some executor or whatever-- a coroutine, it doesn't matter-- You're asynchronously doing that runable that follows. So the first thing you might be doing is, you're opening the file. You're reading each bite out of the file, doing something with that bite-- and then when you're done, you set a success because you're done. And then you have the necessary try catch, finally, after that, so that you can clean up after yourself. So how do we handle when your work gets stopped while this is executing? So like I said, we override the onStopped method. Let's say that we want to just finish what we're doing. We could easily just close that input's file stream. OK. So what happens now you've done that? Let's go back to the code. Well, if you're in the middle of that read loop and you close a file, it throws an exception, right? Next time you try to read something-- well, fortunately, you're already handling that exception right there. So it looks like you're good, right? You're done. Or are you? There's one more case that you forgot about here, which is that, what if the onStopped happens before you even open the file? So now you're no longer looking for that stopped thing, because you never got that signal. You'll read that file because it opened after you tried to close it, which it didn't do anything for. So you'll do all of this work. And how you fix it is, use that isStopped method. So basically, while you're in that loop, you can always just make sure that you're looking for stoppages. So this is a good example of how you would honor the OS's signal to you, or WorkManager's signal to you, that you should stop and be a good citizen. All right. RAHUL RAVIKUMAR: So every time you enqueue a method-- or every time you enqueue a work request-- the work request goes through several state transitions. And I'll talk about how they look. So let's look at the life of a OneTime work request. So when you enqueue a new work request, it can end up in one of two states-- it can be blocked, if it's blocked on another work request, given it's a chain work-- or it can be enqueued. Once the constraints are met, it goes into running. And this is the point at which the Worker's being actively executed. Depending on the result that you return on how you signal work completion, we'll take it to one of the terminal states. So if the Worker returns success, then we'll terminate it with a succeeded state. If the Worker returns a failure, then we'll mark it as a failure. And that also is a terminal state. At any point in time while the Worker was in a non-terminating state if you call cancel, then-- sorry. If you actually retry, then we'll apply the back-off policy, and then we'll retry. So the Worker will go back to the enqueued state. At any point in time if you have a non-terminating state and you call cancel, it'll end up in canceled. So this is what a life of a OneTime work request looks like. Let's look at PeriodicWork. PeriodicWork is almost the same. Because periodic work can't be chained, there is no blocked state. So it all ends up in the enqueued state. Once the constraints are met, it will go into running. So whether you succeed or you retry, it will go back to the enqueued state. Now, this might seem confusing. If you succeed, then the work really isn't done, because it's a PeriodicWork. We'll just wait for the next interval. If you fail and you ask us to retry, then we'll apply the appropriate back-off policy, and then we'll increment the run attempt count. So we'll tell you that this is the second time that you're trying to run it for that last period. If you mark your work as failed, then we'll transition into the failed state. At that point, your PeriodicWork won't run again. And again, any time your work is in a non-terminal state, and if you ask us to cancel it, we'll mark it canceled. So let's apply those rules to how life of a chain of work looks like. So here is the parent of all Workers. So when you enqueue this chain of work, only a is in the enqueued state, and all descendants of a are actually blocked now. Right? So let's assume a's constraints are met and it goes into [INAUDIBLE] running state. Once it's done-- let's assume it succeeded-- So it unblocks b and c now. So b and c, now, become unqueued and then they go into running. And for the sake of the argument, let's assume that b succeeds and c fails. What happens now? Because b succeeded, it unblocks d. So d goes into the enqueued state. But notice what happened to e, f, and g-- They all failed. So the key takeaway is that, if a unit of work fails, then all descendant is also marked as failed. If a unit of work is canceled, then all of its descendants are also marked canceled. SUMIR KATARIA: So I want to talk about something called unique work. And let's start with a little question here-- what's wrong with this code? So you've got an application object in the onCreate. You're enqueuing some PeriodicWork. The problem here-- and I've seen this in a few bugs-- is that this enqueues PeriodicWork every time your app process starts. And that's probably not what you're trying to do. You're trying to set up a sync here. Let's say it's syncing your data every day. If you call this code every time the app starts-- every time you've got another thing that's syncing your code-- you really only want one of them. So unique work, basically, is something that lets you specify conflict policies for a database insert. Think of it like that. If you insert the same key into a database again, what do you want to happen? Do you want to overwrite what's already in there? You want to ignore what you're trying to do? That's what unique work does. It basically is a conflict policy for WorkManager. And here's the syntax for it. It's pretty simple. UniqueName is that key. It's that something that uniquely identifies that chain of work. Policy is the existing work policy, or what we call the conflict policies. And then you obviously have your requests. So the existing work policies are the interesting things. There's three of them. The first one is keep, which is, it keeps the existing unfinished work. So if you have things that are in blocked, running, or enqueued, it will keep them. And if the work is already finished or it's not there to begin with, it will enqueue whatever you just sent along with that call. The next one is replace. It always replaces the work requests in the database. If your work is running, it'll get stopped, as I described a few minutes earlier. It cancels the old work and it stops it. That's what it does. Append is a special one. It basically appends to that chain of work. So for example, this is useful if you are trying to do something in order-- for example, you're trying to build a messaging app, and you're sending messages in order. So you may have a unique chain of work for sending messages, and you want to append new messages to the end of it. So it's basically creating a tree for you. All right. RAHUL RAVIKUMAR: Remember, Sumir mentioned that in one of his previous slides on [INAUDIBLE] how one can customize WorkManager. So let's look at all the things that you can do. So you can actually specify a WorkerFactory that can be used to instantiate your Workers and ListenableWorkers. And this is especially useful in the context of dependency injection. So if you're using Dagger and you want to inject something into your Worker before the Worker starts, this is a good place to do that. You can also specify the default executor that you want all Workers to use. You can specify the logging verbosity if you want to distinguish between a build and a release build, and you want to make sure you log more information to diagnose your problems. And then you also specify various other JobScheduler parameters, like number of jobs that you want us to send to JobScheduler, the ideas of jobs that you want us to use in case you're already using JobScheduler before. So if you want to customize WorkManager, then you have to disable the default WorkManager Initializer first. So for that, you have to add this entry to your Manifest. And note the tools node remove. That means you are removing this. You don't want this entry to get merged. So now that you have disabled the default WorkManager Initializer. The next step is to, actually, create a new instance of configuration. So here I am using the configuration builder, and I'm overriding it to specify my own custom executor. So now that I've done that, I can just call a WorkManager.Initialize. I specified the application context and the configuration, and I'm done. So make sure you do this in your Application#onCreate or ContentProvider#onCreate. Because the operating system can actually invoke job services on your behalf. And when job services are being invoked, WorkManager needs to be initialized. SUMIR KATARIA: Finally, the last thing we want to talk about before we wrap up is some tips for all the library developers out there. If you're using WorkManager in your library, you have some special use cases that we want to think about. So the general advice we give for library developers is, because WorkManager is a singleton and the application initializes it, as Rahul just showed you, you are not really in control of what's there. Rely on the defaults. So use a default WorkerFactory. The default WorkerFactory that we provide creates Workers and ListenableWorkers using reflection. Rely on that, because you don't know what else is happening. If you need some particular dependency injection or anything else of that sort, you'll have to have a contract with the app. Silo your work with tags. So Rahul also showed you how to tag your work. If you silo all your work-- meaning you put your prefix, or your package name, or your library name in your tags-- you can easily identify all the work that's yours. You don't have to worry about other people's work. You don't have to deal with any of that. You can just get your work and operate on just that, if you don't know the IDs. Finally, we do provide the ability for apps to wipe away all work. And this is generally for privacy reasons. This is not something we expect to be called, but it's for that critical use case where you have to wipe user data for some reason. So as a library developer, how do you find out if your work's been wiped or gone from under you? You should look at getLastCancelAllTimeMillis. It's a very confusing name. OK. Next steps. So get WorkManager if you haven't already. And for those of you who have, thank you very much. Your feedback's been invaluable. We're up to alpha 11, so there's three general categories of artifacts here. There's the runtime. There's the KTX stuff, which includes a quarantines Worker that we just put in. So you don't have to write your own. And then there's testing support as well. These are some helpful links. Schedule tasks with WorkManager is the developer.android.com section for WorkManager. On YouTube, there's the Google I/O 2018 talk, which talks about all the basics in much greater length. Some of those APIs have changed a little bit, but it's still broadly a good thing to read or to listen to. Also, please file your feedback at our public issue tracker. Beta is coming. We were told that this morning-- or last morning as well-- So we're kind of-- [LAUGHTER] We have to get back to work, but-- [LAUGHTER] Thanks a lot. Please visit all the Jetpack libraries on the web, and we'll be outside for any questions that you might have. Thanks. RAHUL RAVIKUMAR: Thanks. [MUSIC PLAYING]
Info
Channel: Android Developers
Views: 32,563
Rating: 4.9083967 out of 5
Keywords: type: Conference Talk (Full production);, pr_pr: Android, purpose: Educate
Id: 83a4rYXsDs0
Channel Id: undefined
Length: 29min 28sec (1768 seconds)
Published: Thu Nov 08 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.