Android Suspenders (Android Dev Summit '18)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[MUSIC PLAYING] ADAM POWELL: So I'm Adam. CHRIS BANES: I'm Chris ADAM POWELL: And this is Android Suspenders. Today we're going to talk a little bit about Kotlin coroutines. But to get started, a little bit of background-- many of you are probably familiar with this already-- is the idea of Android's main thread. And just like almost every other UI tool kit out there, Android exposes a single UI or main thread for doing updates to parts of your UI. So some of the things that you might find yourself doing on the main thread, inflating views. You could be doing measure and layout operations to change the shape of your view hierarchy. You could be drawing. Or many other things really, like processing input events, and so on and so forth. So today, you're probably familiar with this 16-millisecond time limit. The vast majority of devices out there have a refresh rate of 60 hertz, which means that you have just over 16 milliseconds to do all of the work necessary in order to deliver a smooth frame rate. So what happens when that display rate goes up? So 90 hertz means you have 12 milliseconds. 120 hertz means you have eight milliseconds. And you have less and less time to get some of this work done. If you take a look at a CIS trace, you'll usually find that the things that cause you to miss a frame and thereby have kind of jank in your application, is really app code. If you're binding items to a recycler view or reading from a data store, so on and so forth-- These are all bits of work that it would be really great if we could keep this off of that main thread and out of this critical path. So how do we fix it? Well, this talk isn't about achieving eight millisecond refresh boundaries. This is about making use of the resources we have available. And all the phones that we have multiple cores in them today. So how do we actually make use of that? Well, some of you probably remember this thing. It's been around for quite some time and has a lot of issues. It composes a little bit badly, has some issues around rotation. It's really easy to get wrong. There's executors, which most Java developers are familiar with. And this kind of lets you share some thread pools, which is nice. But it's otherwise kind of a really raw API. You can build things out of it but it's not really convenient to use on its own. Loaders-- loaders are a thing that were certainly out there that solved a few problems. But it was pretty narrowly scoped. It's really now deprecated. So there's not a whole lot there. We can use futures. There's listenable future, which has started showing up in more AndroidX APIs. But you may not want to pull in all of guava and all of the infrastructure that really helps you leverage some of the things that you can do with it. And unless you're working on min SDK 24, you can't use CompletableFuture either. And really there's a whole bunch of other reasons why you might not want to use that to begin with. So there are a lot of other libraries out there. I mentioned guava. Guava is certainly one that you can pull into your app. RxJava is super-popular. It's a great library. Does a lot of things well. But if you're sitting here in this talk today, you're probably here to talk about coroutines, which is a new feature that went fully stable in Kotlin 1.3 just this fall. So why should we use them? CHRIS BANES: Great. So when I think about this question, I'm thinking about, what does your typical mobile app do? And I think for the most part, you can describe them as kind of CRUD apps. And I don't mean the American slang. I mean the fact that they create, they read, they update, and they delete data. Usually its some kind of local data source like a database or whatever it be. And then going a bit further, a lot of apps will also have some kind of sync. So they'll upload data and pull data back from some kind of web service. Now, apps are actually pretty simple, computationally. You're not really taxing the CPU that much, really. Yes, the logic can be quite tricky to get right. But actually they're pretty simple. And of course, Android, we put a lot stuff in your way. Like, we make your life harder. But actually, your apps are actually pretty simple from a logic point of view. So why coroutines? Well how will they actually fix that? Well I think that great for I/O tasks, especially for a resource-constrained system, like the phones and tablets that you're using today. Now, you can't create a thread for every network request you ever use because threads take in the space of about a megabyte to two megabytes of RAM every time you create one. That's why thread pulls actually cache threads for you. Now, coroutines, on the other hand, take in the sort of realm of 10's of kilobytes in terms of coroutines. They use threads underneath, but they use them in a much smarter way. Also in my mind anyway, they have an easier development model. It's a lot easier for developers to come into a new code source and see an imperative line-by-line coroutine over something like an RxJava chain. To fail to understand what's happening there is in my mind anyway, easier. And same thing for callbacks. Everyone knows about callback hell. And I'm to go line-by-line and see where you've been called and stuff. And so coroutines help, hopefully anyway, fix that. So a lot of this talk was written in mind with a sample app that I've been writing for the last months on Tivi. It was originally-- I went all in on RxJava. It was RxJava from top to bottom. And I've kind of slowly brought coroutines into it. It's now about 50% coroutines, 50% RxJava. So it still uses both. And I think they both have a place in Android development. And so yeah. Go to use both. Now, as Android developers, we have to care about APK size and method counts. Now, in Kotlin, in my app anyway, I'm using free libraries from the Kotlin coroutines kind of, tree. The core, which contains the majority of what you'll actually use. Android, which adds support for the Android main thread, amongst other small things. And then Rx2, which allows you to interact with Rx Java. So you're not using RxJava, you won't need that one. It's pretty small anyway. Now, if you actually pull down those jar files just from Maven Central, or wherever, they would come to about 724 kilobytes. So that's quite big. And there should be-- alarm bells are ringing. But once you actually put that in your APK, it actually shrinks down because it's packed in. And it comes down to about 500 kilobytes. But that method references are still quite high. That's 5% of your 64k. As soon as you turn on Minify, and now this is just tree-shaking. So there's no optimization turned on here. Or ProGuard, they both have similar results. You're looking about 113K. So a lot less. And again, the method reference is dropped. But as soon as you turn on optimization, and then you go through all this trying to fix all the ProGuard rules. You're coming down that magic value which is less than 100 kilobytes. And again, your method reference is now 834. It's less than 1% or 1% of your references. Now, one thing to note when you're using ProGuard or R8 is that you need to use this rule. It's not currently bundled with the jars, but hopefully it'll be added soon. But it's a pretty simple rule to add. ADAM POWELL: All right. So hopefully by now you're thinking about how you might use coroutines in your app. So let's go ahead and talk a little bit about actually writing them. So anything that you can do with a one-shot callback or future, you can do with a suspend function. Everything with coroutines is kind of based around this idea of suspending function as a basis for creating APIs. Suspend functions can stop and yield their thread without blocking. And they can be resumed later like a callback. And they can only be called from another suspend function. So just to setup all the machinery involved in that. But the core thing here is that all of this kind of fits on one slide. There's less to remember. The rest of the language still works the same in the presence of suspend functions. And we'll spend the rest of the talk talking about that. So here's a suspending method from Chris's app. Actually, this is a data repository for TV shows. So you go ahead and you call this update show function with an ID. We get some shows. And we get a little bit more data from a remote source. And a little bit more data from a second source. Finally, we merge all that together and we save that data. So these three main tasks where we spend the bulk of the time, these are done sequentially. But none of these tasks have a dependency on one another. So wouldn't it be nice if we could do this concurrently? So with the async builder, we can do this. So we're going to start from the very top here. We start by opening coroutine scope. And this is important because this allows us to perform this parallel decomposition using the async builder. It brings a coroutine scope receiver in the scope for this lambda block that we have. So first we build the async operation. And the second. And the third. And then we await the result of each one, in turn. So the nice thing here is that we've launched all of these things, let them run kind of independently, and then we bring them back together again. So since all of these things can be now done in parallel, things should complete faster. So trying to do this with raw threads by hand would be a lot more code, which you would need to maintain along the way. So the async builder is for when we want to run something and then later await the result after giving it a chance to finish while we're doing something else. It's very similar to C# or promises in JavaScript. So what about when you just want to launch something and forget about it, though? Well, there's something for that too. It's just called launch. It works pretty much the same way. So in this case, it is a lot more similar to executors and just sort of submitting a task, submitting something to an Android Handler, for example. When we just want to fire and forget and deal with it later. So that's a little bit about the basics of running coroutines in isolation. How do you run them on Android, though? So in this case, you might have an arch components ViewModel. And wouldn't be nice if we had kind of an easy way to put all this stuff together automatically? We need it to be able to open a scope. So where do you get one of those things to begin with that you can launch things into? Well, in this case, our ViewModel has a show repository, which is our data layer. And we have a view state that the viewer activity or fragment can observe. We can refresh on construction. And when we actually go to refresh, we launch into this new ViewModel scope extension property. This is coming very soon to the AndroidX libraries. So those of you who like to watch commits in AOSP might have seen this going already. So the refresh function launches a coroutine, uses the launch builder, which then calls the update function on the repository. And our coroutine resumes back on the main thread after update show returns. So we can safely manipulate our view hierarchy with the results just on the next line of code. We have this nice, clean, sequential ordering of operations. So those of you who want to go check out this thing that's upcoming, you can go to this link. Take a look at the change so far in advance of the actual release. This is coming in an upcoming release of our KTX libraries. So let's ahead and demystify how some of this works a little bit. But before we can go too, too deep, we need to start talking about some of the other primitives that are at work under the hood here. CHRIS BANES: Great. And so we are going to talk about jobs a little bit. So what is a job? Here, when you actually look at this code snippet-- the one we just looked at from Adam. You're actually using the launch method. Now, when you actually run that launch method, it returns what we call a job. A job allows us to keep a reference of that ongoing piece of work. And it has really one method on it. And it's called cancel. Now obviously in this example, we wouldn't just call cancel straightaway after we've launched something. That would be ridiculous. But what it does allow us to do is handle something like double-refreshes. If you have something like pull-to-refresh in your app and an automatic refresh, you don't want them both to happen at same time. And then you have two things happen at the same time. So here this code snippet, you can keep a reference to the one that's currently running. And if it happens again-- like a refresh is called-- then you can cancel the first one. And so that's kind of how job works. It's a very simple object that allows you to keep a reference of the ongoing piece of work. So you may have seen that little scope thing and wondered what it is. Adam explained it earlier. And you can have a scope and it provides you with all the context you need to run a launch or an async. So let's take a look at how they actually work underneath. So coroutine scope is an interface that allows objects to provide a scope for coroutines. Think things like things with a lifecycle. So fragments, activities, view model even. And they can provide a lifecycle for the coroutine itself. And start and stop it as it needs. Async and launch used to be the sort of global methods. And now a recent refactor brought them as instant methods on the coroutine scope. What it means is, mentally anyway, is that instead of just launching something, you're launching a coroutine on x. So I'm launching a coroutine on the activity. You just change that mentally in your head in that it's tied to the lifecycle of something else. ADAM POWELL: Right. If you're used to working with the lifecycle owner in arch components, so far, you know the lifecycle owner has a lifecycle that you can then observe and attach things to. Similarly, a coroutine scope has a coroutine context that carries along everything necessary to know how to launch a coroutine. CHRIS BANES: Some of the stuff we're going to talk about in a bit. And similarly, coroutine scopes provide a default context. So you can provide a default context for all the coroutines that were ever run on it. So I think things like what kind of thread pull or dispatch it runs on, the job-- the parent job-- that allows you to cancel it later. And other things which you can add to it. It's basically a map for context. ADAM POWELL: And it's on again. CHRIS BANES: --again. It's a good day for slides. Oh, we're back? OK, cool. So let's take a look at another example. This time we're not going to use view model scope, the automated thing we've added to AndroidX. We're going to write it out ourselves. And so here you can see that we've created a job. And now it's just a simple instantiation. We're going to keep a reference to it. And then we're going to create a coroutine scope using our job. And that means that anything that runs on it allows us to track back using that job object. We're also going to give it a default dispatcher. We're going to talk more about that later. But basically what it means is that anything that is launched on that scope will be automatically run on the main thread-- the Android main thread in this example. So once we've done that, we have our refresh method again. And this time, instead of that view model scope-- the automated thing-- we're going to use our own created scope. And again, it's exactly the same code. But it's just using a different type of scope this time. But this time the launch will be scoped to the job object we created earlier, which means that in our uncleared, which is the callback we have in ViewModel, to actually not when it's been torn down. We can actually just call job got canceled. And that means that any coroutines which are currently running when the ViewModel goes down will be canceled at the same time. It reduces memory leaks or whatever it is. It just allows it to tidy up. So if you actually have a look at how things are run now, so we've launched our coroutine. And now we're going to go into our update sharing method. So here we are in our coroutine. So our launch, which is modeled by that blue thing going around. Now, here in the updateShow method, which is denoted by the yellow arrow-- so we're going go past that first piece of code, which is the async builder. At this point, we have a first coroutine running, which now racing. And that's doing the local show still stuff. So it's running nicely and it's doing its thing. Now the outer coroutine is going to go past that and go past the second async which is the remote. Here, again, so we now have two coroutines running. Well, we have three coroutines running. But two inner ones, two child coroutines. So once they launch and they are running and going along, we fall onto the first to wait. Now, at this point, we're waiting for that first async-- the local-- to actually finish itself off. And then return a result, which is what await will return. But at this point, because we're waiting on that first async, the outer coroutine is what we call suspended. It's just sat there doing nothing. But during that time, that view model has been torn down. I don't know, the activity has gone away. Or whatever it will be. And we called job.cancel. Now, this point, the outer coroutine, is canceled. And then the inner two are also canceled. And now some of that coroutine scope gives us-- and scoping in general gives us for free. And that child coroutines automatically inherit from its parent coroutine. So if the parent coroutine has been canceled, anything below it will also be. That some of the nice stuff that has been added recently. But what if you're not actually using ViewModels? You know, a lot of us might not be using it. And then we have other APIs which later do similar type of things. So it's part of the Android architecture components. We added a list of functionality for life cycles. So here's a very quick example of how you use them generally. You create a lifecycle observer. And in this example, we're using default. And when you create-- it has methods for each of the different lifecycle events-- so on create, destroyed, stop, start, whatever it be. And to actually use it on like an activity, or whatever it be, you create an instance and you add observer. Hopefully, you've seen this API before. So this builds a kind of scope where lifecycle observer which allows us to scope coroutines to an actual lifecycle instance. The primary API we'll use here is that we're passing the lambda. And that's the thing that we run once we've been started. Which is kind of what you want, usually. Most of the time. So we'll now look at implementation. So the first thing we want to do is on stop. So that's when we'll start running that piece of code. So we'll create a coroutine scope using a job. And then we're going to run it on the dispatches domain. You can choose what that will be. And then we'll call script.launch, and then just call our lambda. Pretty simple, really. And then finally, a nonstop, which seems like a good lifecycle to use, that we'll call in that code, but will eventually cancel the job. And that will mean that coroutine, if it's still running at that point will be canceled. So you can see that code isn't actually that complex. It's AndroidX, so it's not nice to use. But actually, if you look at the deep down of it, it's actually pretty simple. And then to actually finish it all off, we'll provide a nice build function. And you pass it a lambda, and it will automatically add the Observer for you. And what it allows us to do is stuff like this. So here, we have a DetailsFragment and onViewCreated. We will use our liveScope extension function and then just run something. And that will automatically start it when we get to-- well, it will be started when we go start a new fragment. And it will be closed or ended when we get on stop. All right, brings us on to cancellation. ADAM POWELL: Right. We've talked a lot about this idea of cancelling a coroutine. But what actually happens when this thing cancels? I mean, if you just have kind of a block of code that's running, what gets torn down? What do you need to do to clean up? So when a routine is canceled, if it's canceled while it's suspended-- so it's waiting on something else to happen. In callback terms, the callback just hasn't been invoked yet. It will throw a cancellation exception, or rather, the coroutine will resume from the point that it was suspended with a cancellation exception. So what does that actually look like? Here's our example from before. So what happens if we need to clean something up if this is canceled in the middle of that updateShow function? Well, because it throws a cancellation exception. This is something that we kind of know how to do already. Plain old finally blocks runs expected. We didn't have to add any new concepts beyond what we already know from the rest of Kotlin. But if blocking code is running, cancellation requires some explicit cooperation in order to stop what it's doing. So we can check for that cancellation explicitly in a couple of ways. Generally, this means checking to see if your coroutine is currently active in one way, shape, or form. And there are a couple of useful patterns for doing this. So one of those patterns is that if you know that your job is already canceled, you can call any one of the stock suspending methods, such as yield used is the example here, to force a cancellation exception be thrown, relying on that standards and implementation that I mentioned before, that if you're canceled when you're trying to suspend, then you'll resume with that cancellation exception. So we know that this will immediately throw if we happen to be canceled. But if you're in a tight loop, you can also just check this isActive that's available from any suspending scope. And you can just simply stop what you're doing. There's no reason to really involve an exception here if all you're doing is some sort of tight inner computation loop that you need to break out of. Which kind of leads nicely up into how exceptions are handled with coroutines in general. And there are a few things that are really worth pointing out, especially if you followed some of the kotlinx.coroutines development leading up to release because there were some really significant changes that happened. The first is that launch will rethrow unhandled exceptions as they happen. So more precisely, it fails the parent. It cancels its parent job. The parent sees a cancellation exception with the original exception as the cause in the exception object itself. So similar to a thread, they get dispatched to the default exception handler at the root of a job tree if nothing else manages to handle that. But your coroutine context also gets a chance to intercept. You can attach a special element to the coroutine context itself that you can use to handle unhandled exceptions. So let's go ahead and see how that works. Here's, again, our example from before. And say that saveShow throws a very domain-specific exception in this case. So in this case, this will treated like an uncaught exception at runtime just like anything else that throws an uncaught exception on your main thread. So async is a little bit different. If an exception's thrown while something that you launch with async is running, it'll hold onto that exception and only throw when the caller calls await. So coming back to our example from before again, let's go ahead and use what we know. We throw our exception from one of these async jobs. But that gets thrown from this call to await itself. So we know exactly where we need to try and catch that exception just kind of in the normal way and handle that error. But there's a gotcha here. And that's that async works the same way as launch in terms of how this nested job tree is handled. The deferred object that it returns is just another kind of job. And so it'll cancel the parent if it fails with an unhandled exception, just like launch does. And it'll do this even if we did it an await and called it. Now this is kind of important because if something throws an exception, it's really important that your app know about it. Like it shouldn't just disappear into the ether. But at the same time it kind of defeats the purpose of the code in this sample here. We caught it. What are we supposed to do here? Well, in this case, instead of using coroutine scope at the top to open up this parallel decomposition, we can use supervisorScope. It works exactly like coroutineScope, except with a supervisor job, which is a special kind of job that won't be canceled if a child fails with an unhandled exception. One more spot-- yep. It will only be thrown from this await method here. CHRIS BANES: Cool. So earlier, we mentioned that we can actually decide where coroutine run and what kind of thread they run on. Because actually on the JVM and on Android, we actually still run on threads. It still uses a thread pool underneath. So we still can decide where that actually gets dispatched on. Now let's have a look. So here we have our launch, a very simple launch, which is missing a scope. But just look at the example. Now, default actually is that the context will be used in what we call a Dispatchers.Default. That's a default dispatcher that's kind of given for you for free. And it's supposed to be sort of default for everything, really. ADAM POWELL: It's essentially a computation thread pool, if you're used to using that from our RxJava. CHRIS BANES: So what is a coroutine dispatcher? Which is what it basically is. Well, it's the thing which runs and schedules coroutine, as I mentioned earlier. It schedules coroutines to run on something, on a thread in our case. Now, Dispatchers.Default, which is the default which you get, is that it uses cpuCount threads. So your device has four CPUs in it. You will get a thread pool of four, which isn't so great for things like I/O. So most apps will be doing a lot of network or disk or whatever it be. So it's not so great for that. So yeah, it's mostly more like a computational type dispatcher. But it's also an elastic thread executor, which we'll talk about in a minute. But it's the default. There's also Dispatchers.IO, which was added fairly recently. And it was designed specifically for blocking I/O tasks, so things that we really care about-- network, image loading, reading disk, database, blah, blah, blah. It uses a minimum of 64 parallelism, which basically means that you can have up to 64 tasks running at a time, which is great for what we need. So yeah, you can launch it like that. But the really great thing about the I/O dispatcher is that it shares thread pools with the default dispatcher. And the point where it becomes great is this. So here have an example where we have an async, which is using the default dispatcher. And then we're going to load an image on the I/O dispatcher. So we can do some disk reading, some kind of load image, whatever it be. And then we're going to use that result and then process it somehow. It's a computational task. Now, because this is running on the default dispatcher, there is no actual thread switch in there because we're using those shared thread pools. I/O and default uses shared threads. Therefore there's no actual thread switch, which makes it a whole lot quicker. And then we also have Dispatchers.Main, which we've spoken about a little bit already. It allows running coroutines on the UI main thread. A nuance with that is that in later releases anyway, it uses service loader to load the dispatcher in your code, which is kind of tricky when we have things like ProGuard. So you have to be careful with this. But yeah, just be careful. And the thing you need to know is that you need to add the Android dependency, which we spoke about earlier anyway. And then to use it as we said. Just launch in the Dispatchers.Main. So that brings us a little bit onto reactivity. So how many blog posts, that you've seen recently anyway, can be summarized as this slide? [LAUGHTER] And I, myself, have been guilty of this. [LAUGHTER] Now I'm going to make the premise and the statement that most devs use RxJava just because it makes threading easy. So the fact that you have schedule on, and you can easily switch afraid on-- switch multiple threads. And that's why most RxJava-- its use is. Now, of course, there are going to be people who go tell, not top to bottom with chains and reactivity. But I think for the 80% of cases, it's just a switcher thread. And that's because the APIs that we have, and we spoke about them earlier, aren't so great to use. And because of that, most people end up using things like Single, Maybe, and Completable because that's specifically what they are. They're single, one-shot things. Single allows you to always have a type. Maybe it's nullable. And Completable doesn't have a return type. So they're all pretty similar. But in fact, they only actually exist on RxJava, RxScala, and RxGroovy. They don't actually exist in any other platform. So as I said earlier, maybe it's more of a reflection of the API that we have are at disposal rather than the fact that they're needed. So coroutines can actually quite easily replace Single, Maybe, and Completable. They do exactly what you think. We said earlier about replacing callbacks. They also replace these quite nicely. So here's an example. So we have a Retrofit interface, which has a GET. And it turns a Single, which it just returns a list of Shows. And the way you'd use that in RxJava is you do this chaining. So you'd switch the scheduler using scheduleOn. And here we're going to use the I/O scheduler, which is provided for you. And then we're going to do some calls, too, when it's finished. Now, the nice thing about the Rx2 interrupt library of coroutines is that you can actually use that exact Single as an await. So you can actually use it as a suspend and deferred. So it's really handy for when you're kind of slowly migrating towards coroutine and you do want to change everything from day one. You can actually keep those interfaces, all those server pools. And you can actually just call await on them. Receive inside can be using coroutines. So it's quite a handy way to slowly migrate. Wouldn't it be great if we can actually made that Retrofit call just a suspending function and just remove the whole Rx from the start? Well, we can. And that is actually going to Retrofit soon. Jake has a PR, which is pending review. But he tells me it's soon. So yeah, it's coming soon. And then, if you look at consuming code, it's pretty much the same. It's just we can now get rid of that await. And it's just a normal suspending function that we've called, and it's just a normal coroutine And that brings us on to our final section, which is kind of bringing it all back together and trying to think of two scenarios where we show you how to use coroutines to make your lives easier on Android. Both the examples are all about location. The first one is about actually getting the last known location, which you can kind of think is a one-shot callback. So here we're going to use the FusedLocationProviderClient, which is from the Google Play Services API. It's actually kind of cool because it combines all different providers we have, like Wi-Fi and GPS and mobile and whatever it is, Bluetooth. It provides all those providers for you into one single API. So it's actually a really nice API to use. And it returns a Task, which is a kind of future-y type thing that Play Services library has. And yeah, so you call client.getLastLocation. And it returns a Task. And from the task, you can add what we call a complete listener. And then you'll get the result back eventually. So it's completely async. So when you think about it, what we're doing is converting a callback API into a suspended function. That's pretty much what we want to do. And luckily, the coroutine library has two builders that do exactly what we want. The first is suspendCoroutine. And what you do-- you pass a lambda, which allows you to set up your callbacks. So basically what we'll do here is call Play Services and go, get me the last location. At that point, the coroutine immediately suspends waiting for the result to come back. And then the callback can wait you back up, basically. So we'll go through an example now. Yeah, you're giving a continuation to later resume. That's how we do. And that's how you pass the result on. Analyze the calls and cancel the call. So actually, that's different. Hold on. So there is a newer version-- sorry. Didn't mean to skip that. There's a newer version called suspendCancellableCoroutine. It's like a little add-on on top. And it allows you to cancel the call. So say your coroutine's canceled. You can then tell the underlying API, in this case Play Services, to cancel its call. So let's build a function. So here we're going to have a suspended function called getLastLocation. And it returns an actual location. It doesn't return a future or anything like that. It's just a straight location. So I'm going to use our suspendCancellableCoroutine builder. And then we're going to have a continuation. That's our kind of callback-y type thing. And then we're going to set up. So we're going to call location client, which is the Play Services API, last location. And then we're going to-- we get a Task back. And then we're going to add our onCompleteListener. At that point, we are going to wake up our coroutine. So that is how we pass back to the suspended coroutine the result. And it then wakes up. And the suspended function basically wakes back up, resumes. Now, because this is a cancellable-- I'm actually backing up a little bit. Because we are using Task, we're not using the success listener. We're using the complete listener, which means that the Task itself can throw an exception. It can fail for whatever reason. So you don't have location permission or whatever it be. Now it will raise an exception on you, which means that you can populate that backup to the call, which is done with a resume of exception method. And finally, because we're using suspendCancellableCoroutine, we need to tell the Play Servers API that we've been canceled. So therefore it should cancel. So we did that with a callback, which is invokeOnCompletion. At that point, assume you know that the coroutine's been canceled, which means that you need to start Play Services. Now, this API doesn't actually exist because Play Services doesn't have a cancel method. But imagine it just exists. Now Adam's going to talk about observing. ADAM POWELL: Sure. So what happens when you want to observe a sequence of events over time? Now this is the thing that Rx is really, really good at. If you're using Rx for anything, it should be for this. And people tend to compare RxJava and coroutine quite a bit in blogs and so on and so forth. So what does that actually look like if we try and emulate this using coroutine as a primitive? So Play Services' location API, in addition to letting you get just kind of a "one-shot, what's my current location," it lets you request location updates using a callback that's invoked multiple times. So normally this is a prime candidate to be an observable. You register a callback at subscribe time. And unregister when that subscription's disposed. We get this composable control over shutting down the updates really cleanly. So Rx is a great library. It offers a ton of functionality to build things like this. So how many similar benefits can we get if we base this off of suspending functions? So let's, again, start just writing a simple function, or at least it's going to start out simple. Suspending functions don't return until their work is all done. So there's no disposable or closeable return by it since the calling scope itself is cancellable. We just don't return until we're done. So our observer in this case can just be a simple lambda that accepts a location result. And we'll go ahead and call it whenever a new location is reported without actually returning from observed location. So if you take in this giant pile of code here, some of you might notice that it looks an awful lot like an observable.createCall from Rx. So let's go ahead and take it apart piece by piece. And we'll go ahead and start off. And we'll just create this done signal that we can await on later to know when to finish normally and clean up. So this is like our observable completion. In the case where you have a stream with a well-defined end signal, you can complete this to let the observer function clean up and return. And we'll see that in a little bit. So the next piece here where we're creating a location callback, this should really be no surprise. We need one to receive updates from the location provider. But what's interesting here is that we use launch to call the observer function on the same coroutine dispatcher that observe location was called on. Remember, the coroutine scope that we opened here carries along the dispatcher with it that it was called with. So we always know that we're going to call the observer in the same place that the caller wanted the information reported. So we cancel the old job from before and start a new one. We make sure that we call the observer while we're holding the suspending mutex. And this keeps things serialized to make sure that we don't have two calls to an observer active at once. We assume that onLocationResult won't have multiple calls active at once either. So this is kind of an example of some of the things that if you're building one of these things yourself, this is one of those comparison things of-- RxJava kind of does a lot of these things for you. But with coroutine, we have all the primitives to build it. We just need to do a little bit more of it by hand if we're putting one of these things together. So we register our callback. And then we await on our done signal. And since we never complete it, I mean, when does the location stream really complete anyway? This will go ahead and wait for the calling job to be canceled. So we remove the callback in the finally block. So it'll happen on cancellation. And then request location updates normally takes a looper instead of something like an executer, which is kind of unfortunate because this pattern really shines when you can avoid an extra hop. So if you use a direct executer that'll just go ahead and run the observer wherever the update happens on the incoming binder thread or whatever it is. It really kind of shines when you can avoid those extra hops. But you get the idea. So here's what it looks like in use. And if you take a look, this looks an awful lot like just a for-each call on a collection. And it behaves exactly the same way. So since we use launch to call the observer block in the first place, if the inner block throws an exception, that means that the observed location call itself will throw an exception into that outer scope. We can just wrap this whole thing in a try-catch. And that's all of our error handling that we need to do. So it's a child job. So it'll cancel the parent scope with the exception as well, the same scope that wraps the whole of the observed location function body. So it'll resume from the await with the cancellation exception, unregister the location callback from the finally block from above. And all of this just composes. So you, again, can lean on all of these constructs of the language that you already know by just adding some of these suspending primitives. CHRIS BANES: Cool. So let's wrap up a little bit. So what's next? What are some action items for you to do? Well, the first is that, as you saw in the key note earlier, we actually have a code lab specifically for coroutine. And that was released about three weeks ago, which is really good introduction. It's coroutines and how to actually use it in your app. And secondly, read the manual. I don't like to do that usually, but the docs are actually really, really good. They're on Github. You can look at them. You can even edit them if you want to. But they're all very use-case based. They're all, I need to do x. How do I do it? So make sure to go and check it out if you are using coroutine. And that is it. Thank you very much. [MUSIC PLAYING]
Info
Channel: Android Developers
Views: 29,333
Rating: undefined out of 5
Keywords: type: Conference Talk (Full production);, pr_pr: Android, purpose: Educate
Id: EOjq4OIWKqM
Channel Id: undefined
Length: 37min 4sec (2224 seconds)
Published: Wed Nov 07 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.