Understand Kotlin Coroutines on Android (Google I/O'19)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
YIGIT BOYAR: Hello. Welcome, everybody. My name is Yigit Boyar. I'm an engineer in the Android team. SERGEY VASILINETC: Hey, my name is Sergey. And I work in the same team. SEAN MCQUILLAN: Hi, I'm Sean. I'm from Android Developer. YIGIT BOYAR: Today we are going to talk about coroutines on Androids, but before we talk about that, let's try to figure out why do we even need some coroutines. And to understand that, let's forget how you write AI code on Androids. So this is the dream code we want to write. We're like, calling function makes a network request, whatever. You get the user, set it on the text field. This is what you want to write, but if you write that, you get an exception, because you cannot make a network request on the main thread. Easy. We just put it inside a thread and run the code. Now you're going to complain is the textView, which is like, you cannot touch the UI thread from a background thread. And OK, we'll write this kind of code where you make it asynchronous. You provide a callback, it draws on the background thread and calls your callback on the UI thread. And this code works fine, except, if you're writing code like this, you're going to receive out-of-memory exceptions, because you're going to be leaking those callbacks left and right. There is a solution to this as well, where we can have an understanding of a subscription that keeps this chain. And whenever you are a stop, we can just cancel the subscription. That one works, but then you end up with something like this. And I'm not making this up, by the way. Three years ago, when we started Architecture Components, I was looking at the Google App codes. And I found one application that had 26 lines of [INAUDIBLE] on register. So if it happens at Google, it happens everywhere. We don't always write the best code. But there's a solution to this as well, which, if you're using something like RxJava and this function returns an observable, you could just use the AutoDispose library. Associated subscription with your [INAUDIBLE] lifecycle and safely subscribe. And this works perfectly. Similarly, you could be using LiveData, which enforces you to have a lifecycle to observe, and this also works. So it's this old problem, right? Why are we even talking here about this if there is already good solutions? Every year, we run these developer benchmark surveys. We ask developers how are they doing, what are their problems. And the surveys we ran last year, one of the top complaints was threading and concurrency. Developers say this is hard on Android. And one of the top requests was this, what we call, LiveData++. People want us to extend LiveData, make it more like RxJava. And we're like, why? Why do you want this? We have good solutions. Just use one of them. So we did what we do best when we don't know. We did UX Research-- this is User Experience Research-- on concurrency. So we did in-depth interviews with nine developers. What they do is they do their regular work for a couple of weeks in their own company. And every time they see a problem about concurrency, like this observable, they just write it down. This was the problem. This is how I solved it. And this is how I feel about it. And in this study, we focused on three main things. Focused on LiveData, which is our observable data holder. We focused on RxJava. We saw the Reactive Extensions library. And coroutines, which provides suspendable computations. And in the result of that study, this was the conclusion. For LiveData, people say, we love it, but we want the complete solution. In fact, it's funny. LiveData doesn't even support anything but the main thread, but we talk about it in a concurrency study. For RxJava, it is amazing. People love and hate it. They love how powerful it is, but the common complaint we always heard was it's always misused. It feels like an overkill. And for coroutines, this was like, it really looks like the best solution, but I'm not sure. It's very new. It's not mature. So this was the overall conclusion. We said, we need a solution that is simple. It shouldn't be hard to learn that solution. It should be comprehensive. So it should be able to scale to different use cases. And it should be robust. There should be a built-in testing story. So we made two decisions. We said, OK, we are going to have first-class quality support in Jetpack. And we are going to have more support for RxJava in our documentation. But, today, it is all about coroutines. So Sean, why don't you tell us a little bit more about what coroutines are? SEAN MCQUILLAN: Thanks, Yigit. So I want to just take five minutes and talk a little bit about what problem coroutines are great at solving. So, in a sentence, the main problem that coroutines solve is simplifying async programming by replacing callbacks, which is quite abstract. So let's look at some samples and see what that looks like. I'm going to make an imaginary network request three ways. The first style is what's called blocking style. This is where the result is returned directly from the function. Let's see how that executes. And, for fun, I'm going to run that on the main thread. When called, a blocking network call will block the thread that called it. So the entire time that network request is running, the main thread will be blocked. And that's the thread that has to update the UI and handle user touches, so the user will see your app is frozen, or it might even crash. Now, I do want to pause and say there's nothing wrong with a blocking style of network APIs, but it's not what we want to do on Android. So, to fix that, as Yigit already talked about, we commonly introduced callbacks. So let's see how that executes. We're still going to call this from the main thread, but now, when fetchUser is called, the main thread is free to perform other work. It can handle onDraw or respond to user touches. And the networking library is responsible for finding another thread to actually run the request. When the result is ready, the network library can then use another callback I gave it to call back into my code and let me know that it's ready. Let's rewrite that exact same code with coroutines. It looks just like the blocking style. The result of fetchUser is available immediately. And I don't have to introduce a callback. To tell Kotlin I want to execute this with coroutines, it has a suspend modifier on the function. And when we run it, still on the main thread. The main thread is unblocked just like with callbacks. And this is a key concept of coroutines. The networking request still runs on another thread. When the result is ready, it resumes the coroutine where it left off. This code is much simpler than the callback style while still ensuring that I can write my Android app and make it never freeze for the user. This is the key mechanism here of coroutines-- suspend and resume. When a coroutine is suspended, it's not running. It's paused. And when it resumes, it picks up from where it left off. You can think of suspending a coroutine as taking a callback from the rest of the function. So you've put it together-- suspend and resume replace callbacks. We can even visualize that. The callback version and the coroutine version execute almost exactly the same way. Let's switch back and take a look at fetchUser. How can we call a function that makes a network request from the main thread? To start, we'll need to make fetchUser another suspending function. This tells Kotlin that it works with coroutines. And inside, we'll call another suspending function called withContext. We'll pass it dispatchers.io. Zooming in on those dispatchers, Kotlin gives us three dispatchers-- default, IO, and main. And they're used for different things. Default should be used for CPU-intensive work-- things like transforming a list of 100 elements, calling DiffUtil, or precomputing text. Anything that takes too long to run in the main thread should run on the default dispatcher. IO is a dispatcher that's optimized for blocking network in disk IO. You should use it anytime you need to write code that blocks an API, like writing a file or reading from a socket. And main-- this is the main thread on Android. And surprisingly, it's our recommendation as the right place to start coroutines in response to UI events. Since you're usually starting coroutines from the main thread, staying there will avoid extra work for simple operations. Then, when you need to transform a list or write a file, coroutines let you switch to one of the other dispatchers by using withContext. withContext will run the block that you pass it on the dispatcher you tell it to. So this block here is going to run on dispatchers.io, and I'm free to make blocking network calls. This allows us to provide main-safe APIs. You can just make a function that reads and writes from the network like this and call it from the main thread. This is a huge benefit on Android. Now, I don't have to worry about what every single function-- what thread it needs to run on. Instead, I can just call it. And the function itself can ensure that it's safe to be called from the main thread. To finish up introducing coroutines, let's take a look at how Kotlin implements them. Every thread has a call stack. It's what you see in the debugger or a stack trace. It's how Kotlin keeps track of which function is running and its local variables. When you call a suspend function, Kotlin needs to keep track of the fact it's running a coroutine instead of a regular function. I'm going to represent this as a suspend marker. Everything above the suspend marker will be a coroutine and everything below will be a regular function. Then Kotlin calls loadUser just like a normal function. It's going to put a stack entry onto the call stack. And this is where any local variables for loadUser would be stored. And then it just executes until it finds another suspend function call. Now Kotlin has to implement suspend. How does it do that? It's kind of simple once you figure it out. All Kotlin has to do is copy the state of the function from the stack to a place where it can save it for later. It'll put all suspended coroutines out here. And it's not structured like a stack. Then Kotlin will actually call fetchUser, create another stack entry, and when it calls withContext, suspends that as well. So at this point, all of the coroutines on the main thread are suspended. And this means the main thread is free to do other work like handle onDraw or respond to user touches. And this is really, really important. When all of the coroutines on a thread are suspended, the thread is free to do other work. If we fast forward a few seconds, the network result will be ready. And Kotlin will have to call resume. In order to do that, it just takes the save state and copies it back over, puts it on the stack, and resumes the function. When it resumes loadUser, it'll just go ahead and continue executing just like normal. If loadUser had errored, it would have thrown an exception right there. The suspend and resume mechanism is the magic behind coroutines. And we wanted to show it to you so you could understand how they work as you start using them in your code. That wraps up the coroutines intro. Coroutines on Android offer us the ability to simplify our code by replacing callbacks and allow us the ability to create main safety to ensure we never block the main thread. Now I'm going to hand over to Sergey who will talk a bit about libraries you can use today with coroutines. SERGEY VASILINETC: Thanks, Sean. Yeah, those threads [INAUDIBLE],, but we really want to benefit in our real application for that. And despite the very young age of coroutines, there are libraries that already support them in their stable or better artifacts. And I want to start with WorkManager that is brand new for us in AndroidX, because it already supports coroutines in its stable release. And you can use with coroutine work. But let's make a step back and try to figure out why we do that, we use it. So this is a typical flow of workers. So if you are not familiar of WorkManager, you can think of worker just as something that does long background job. It may have some constraints, but that's very simple-- just some work. And typical use case for that, you need to synchronize some local data with your web server. And this flow would look like you query your new nodes from your database, then upload it to the server. Lastly, you just mark those nodes as successfully synced. Well, you see, no need for coroutines. Well, actually, we didn't start to talk about cancellation, because cancellation may happen due to a variety of reasons. For example, constraints for this worker aren't met anymore or user explicitly canceled this job if you provided with UI. So how you would support cancellation? Well, you can try to do something like that. You try to put every other line with ifCheck, and it starts to look silly. And even more, it doesn't actually work, because this call, which is probably most expensive call because it goes to a network and does some work there, doesn't have any cancellation signal propagation, because if it was started, it will run to its end no matter what. And this actually will cause the worker to help us with that. We didn't talk about that yet, but coroutines don't only grab callbacks nicely. It also provides nice cancellation property. So every suspend function can be canceled. It can react on this cancellation. And, also, it propagates the all inner calls-- this cancellation signal. You may say, our code inside those calls are still blocking. We don't benefit from that anyhow. This is true. However, if you use Room as your database solution, you can mark your queries as suspend functions. And then Room will take care of cancellation for you. As well as threading, as Sean mentioned that multiple times, this thing will remain safe. Room takes care of threading. It will run the query on a background thread. Then, well, nice-- our database calls are cancelable now, but as we discussed before, the main call is this one. And, actually, if you use Retrofit, you can make it suspend as well, because Retrofit already supports suspend identifier for its network calls. And I want to highlight that Retrofit isn't part of AndroidX. It's just Java designed by Android. Next time you use Android, Android [INAUDIBLE] embraces coroutines, and we like it. At the end of the day, it's less work for us. Nice. Now this code supports cancellation. And it looks as easy as it looked before. So we got cancellation for free. So this was a quick look on the things that were available today. And Yigit will present you a lot of new guys that we just made. YIGIT BOYAR: Thanks, Sergey. So, so far, we talked about what you could do with coroutines. And for the rest part of this talk, we are going to talk about new stuff. So first one is LiveData and coroutines. Now, just to be very clear, LiveData is not designed for concurrency. It's an observable value holder. And you are expected to be able to access the value from the main thread. That's intentional. But that doesn't mean it should not be interoperable. So this is what we're going to provide you today. There will be an easy way to use LiveData with coroutines. So the most common use case is you have some value. You want to compute in a coroutine, but you want to serve the result as a LiveData. So starting today, with the Lifecycle 2.2 alpha01 artifact, you get this new one, new API, called LiveData. So it's a builder function very similar to the sequence builders in Kotlin. Inside that, you pass a coroutines block. And inside, you can do whatever you want and call this emit function to dispatch values. So if you look at this database load function, it is HLS [INAUDIBLE] function. Because you are calling the emit with the user in this case, we can infer that type for you, so you don't even need to specify this. So this really simple LiveData API bridges the gap between your LiveData elements and your coroutines. So let's get that API a little bit more in detail. So this is three parameters. And the first one is a context. So why do we need a context? Well, if this data is loadUser function, wasn't the-- [INAUDIBLE] function was a regular function, and you write this code, you are going to receive an IO on main thread exception, because this block, by default, draws on this picture's main. But we can change that. We can give it a context, as this picture's IO, and now this code will work perfectly. I want you to notice that I didn't change any contents of the code, because you can emit from whatever dispatcher you want. You don't need to be on the main dispatcher to change the value. Now, the second way is a really awkward parameter called timeout. To understand why we needed a timeout parameter, let's look at the infamous rotation problem on Android. So on the left, I have a ViewModel that serves LiveData. And on the right, I have an activity disk observing it. So while my activity goes to a started state, the LiveData will become active, which means OK, you're an observer visible to the user. You are better off creating some values. But, during that time, what if our activity rotates? So it's going to be stopped. LiveData will become inactive, be destroyed, and a new activity will come. So right now, there is no one observing LiveData, so there's no reason to produce results, except after the new one goes start it again, it becomes active again. So the problem we are trying to solve here is this gap while LiveData quickly becomes inactive and active in a very quick succession, like usually less than one second. So how do we fix that? Let's look at the detail how we run that code block. And to understand it better, we're just going to write a timer function. It basically creates a timer for LiveData. It gets the current time, returns a LiveData builder, and in an infinite loop, it just emits the time, delays one second, emits the time, delays one second, and never ends. And this code I'm showing is 100% OK to write. How does it actually work? When the LiveData returned by this block becomes active, we check, OK, did we run this block. And if we did not run that block, now we start executing it. While we're executing it, if that block becomes inactive, if LiveData becomes inactive, we check, OK, is this block still running. And if it is still running, we give it some time to finish. But even after the timeout, if it is still running and we are inactive, this basically is unnecessary computation. There is no one observing the LiveData, but the board keeps running. So we just cancel the continuation-- the coroutine. So if the LiveData becomes active again, we're just going to restart it. You only do it once. So if it finished to completion, there is no reason to restart it. Now, you can also emit more than one value. So let's put some structure around the sample we had before where we have a repository that has a getUser function, a loadUser function, and the loads from the database, and emits that value. Now, most of the time, this is not the code you write. You need to go to the web service, fetch an updated user, update the database, and emit that value again. So you can call emit as many times as you want as long as you are inside that quarantine block. But you might say, well, most of the time the database doesn't return your user, it returns you LiveData for user, because you want to be notified about the changes. Well, all you can say is you could just call emit source. If you ever use MediatorLiveData, this is very similar to emitSource where it says whatever value comes from the LiveData, just make it my value. You can run things like transformations here. Oh, also, we don't need this extra emit, because we're already observing the database, so you can get rid of it. So this LiveData API basically provides us a very nice way to make LiveData work with coroutines. But how about ViewModels? SEAN MCQUILLAN: Thanks, Yigit. So let's talk a little bit about how to integrate coroutines into your ViewModel. But, first, I want to talk a little bit about leaks-- specifically, coroutines leaks. And these are a very serious problem. They're kind of like a memory leak that we're all familiar with, but way worse. A coroutine can resume itself. And in addition to using memory, it can use CPU. It could write a file. It could make a network request that doesn't need to happen. To help us deal with coroutines leaks, Kotlin introduced this idea of coroutine scopes. So what is a scope? Well, it's really just a way of keeping track of your coroutines. All coroutines must run in a scope. And a scope gets the ability to cancel all of the coroutines inside of it. In addition, they're also the place that uncaught exceptions from a coroutine get shuffled off to. You put that all together, and you can use scopes help ensure that you never leak a coroutine. WorkManager that Sergey talked about provides a scope. So does the LiveData Builder that Yigit just talked about. viewModelScope is a scope. It's an extinction property on ViewModel from the KTX library. I'm going to do another one of those scary infinite loop things that Yigit showed, but this time in a coroutine that I start myself in a ViewModel. It uses viewModelScope to launch a coroutine in the scope. And by default, this launches on main. Then it starts an infinite loop that doesn't know how to stop itself. And every second it's going to go ahead and write a file. Now, that's pretty expensive. Coroutines don't make writing files faster or cheaper, and we definitely don't want to leak this work. ViewModelScope lets us write code like this safely. When the user navigates away from the screen, the scope will be canceled, which guarantees this very expensive work won't leak. So viewModelScope can help you avoid coroutine leaks by guaranteeing all your coroutines are canceled whenever a user leaves the screen. I'm going to pass it over to Sergey who's going to talk about some other scopes we're adding. SERGEY VASILINETC: Yeah, thanks, Sean. Yeah. Another thing that very naturally provides scope is Lifecycle because, as you can see from its name, something that has a start and the end. And if you think, yeah, that's familiar with this Lifecycle owner interface, you actually are, because it is your activity. It is your fragment. And don't forget that fragment conveniently has two different lifecycles. And the second one is associated with-- we use inside of it. Unfortunately, for me, I now have to talk about that. But let's define scope more precise there. So as you know, your fragments get recreated over your configuration changes. So its lifetime can be shorter. It can be longer. And lifecycleScope just mirrors that, meaning that once your Lifecycle owner receives destroy event, lifecycleScope gets canceled. And all its inner jobs are canceled as well. So, as you can see, the lifecycleScope is very tightly coupled with UI. And it works best in situations like that. So previously, you would do something like this when you decide to show some UI with delay. And, well, this looks pretty simple, so we can make it a bit harder. And if we have two steps, it becomes to look very ugly because of this deepness thing. And actually, if you take a closer look, you have some real issues here, because this mainHandler. And those functions that touch UI don't really work nicely together, because mainHandler is kind of a GlobalScope. It doesn't care about your Lifecycle at all. And those functions have reference to fragments or activities. So if your delay is long enough, you can easily leak a lot of them and receive out of them their exception. While lifecycleScope will cancel those codecs, they are showFullHint for us. It's kind of a callback because it's a suspend function. It will cancel it off the [INAUDIBLE] once your Lifecycle is destroyed. So this code looks nicely, because it's very sequential. And it is actually safer. However, I have to say that lifecycleScope is a bit of a danger zone. So let's rewind a little bit. I was the one who showed you that Retrofit and Room supports suspend functions. Yigit showed you something that-- very familiar-- looks like that. When you say, OK, I'll combine those functions to network and database into some repository pattern, you now have just one function, which is the suspend function that orchestrates all of this work. So I just need the scope to call it. So why wouldn't I just call it in my lifecycleScope? It's actually not the brightest idea, though. Why? And don't get me wrong. Yigit and Sean sold you everything correctly. It won't lock main thread. It won't leak [INAUDIBLE]. However, do you remember this picture? lifecycleScope get canceled, and every configuration change, meaning that your network request gets canceled every time, so it is just wasteful. You're wasting user resources, better resources. It's just bad for environment. So how you would do it properly. Well, one of the things actually was presented by Yigit. Like this LiveData builder will work very nicely in these kind of situations. I'll present to you in our way how you can approach this. So your starting point for this kind of task is a ViewModel scope. So you just run this loadNote function in this ViewModel scope. Then we introduced a function in ViewModel that will connect our UI in the ViewModel when you grab a note. Well, as we discussed, it's a network call somewhere inside of its loadNote. So it's a synchronous operation. So it should be suspended. And, well, now we need to somehow connect this node that is loaded in one scope. And loadNote function will be called in some other scope. Well, I will use CompletableDeferred. Well, it sounds a bit scary, but it's actually a very simple thing. You'll see in a second. So how we use it-- we complete our deferred with a note that we loaded. It just put the note into this object. Nothing happens. And readers request the note with a weight function from this deferred. If a note isn't ready yet, then the reader will be suspended. If it is ready, reader will resume right away. So this is how we implemented our ViewModel. And last step, we just call that in our Lifecycle scope this loadNote function that we introduced in the ViewModel. And our network call is properly executed in the ViewModel scope, so it's not affected by configuration changes. And our update UI function doesn't leak once your Lifecycle owner gets destroyed. However, once we add the fragment into the picture, things get complicated as always. So we decided to run the fragment transaction. And you will get a legal state exception, because nothing guarantees you that you are in the correct state that allows you to execute fragment transactions. And we did something smart and introduced some special functions that help you to deal with these kind of situations. And this is going to be a bit tricky, because it's actually a fairly complicated thing. But what it does, this block will run only when your application is started or resumed, meaning that it's in the foreground. And this block will be suspended when your Lifecycle is just created. So let's take a look on the example of what it actually means. So you have this function. It is called, probably in the beginning. Your block will be suspended, because note is not ready. Then, once this is ready, in usual situation, we would resume execution and proceed to the next line. But with launchWhenStarted function, we are going to go and check Lifecycle If it's not started, we are going to suspend further until the Lifecycle will become started again. And once it is started, then we are going to proceed to the next line and easily execute this transaction. So we won't run into this exceptional situation. So one thing I want to highlight that this block is suspended during creation. And it is a different thing from being canceled, because cancellation is still provided by lifecycleScope when destroyEvent happened. And now, as you can see, it is something that we definitely need to test, and Sean will help with that. SEAN MCQUILLAN: Thanks, Sergey. So we talked to you a lot about coroutines today. We talked about how they can help clean up APIs by replacing callbacks to suspend and resume. We talked about different ways they can be used in different situations. And that's all great. That's awesome. But if they were difficult to test, that'd just be a non-starter. That wouldn't be something that I would take very seriously as a thing to use. So what I want to talk to you about right now is Kotlinx-coroutines-test. It's a new library that came out about a week and a half ago. It's currently marked experimental coroutines API, because it needs more feedback before it makes it all the way to the stable. It's a collaboration between Google and JetBrains to make testing coroutines on Android very easy. So it's not coupled to any testing libraries. So you can use JUnit 4. You can use JUnit 5. You can use your own custom test runner that you've built. And this library is going to help you test coroutines. So I'm going to focus in on that LiveData Builder that Yigit showed. And we're going to talk about how to write a test for that. So I'm going to emit one. I'm going to wait a second. And then I'm going to emit two. This is a relatively simple LiveData so I can focus in on how to write the test for it. So to get started, we need to mock out that main dispatcher. The LiveData Builder uses dispatchers.main by default, which is the actual main thread on Android. We can replace it with a test coroutine dispatcher. This is a special dispatcher designed for testing coroutines. And we can make a test coroutine scope. This is a scope designed for testing coroutines. So then in Setup, you can switch out dispatchers.main for a testing dispatcher. This will change the global value for dispatchers.main immediately, so the LiveData Builder will use the dispatcher we give it. And then in tearDown, resetMain to the default value. And then this last line here on the bottom is really, really important. It says testScope.cleanupTestCoroutines. If you think about what a dispatcher and a scope are doing, they're very stable, right? They have to keep track of your coroutines and actually run them. If you don't call this, it's very easy to leak state between tests. So that's a lot of boilerplate. So you can go ahead and put that together in maybe a JUnit 4 rule. This doesn't come in the library, but you can write all of that code into a rule. And I would expect to see a library that does this relatively shortly. So whatever testing framework you're using, however, you should build an abstraction that's appropriate for your testing framework to do that code. The rules that I'm defining here exposes testCoroutineScope interface, which lets me call runBlockingTest. This is a coroutine builder that's optimized for testing. It works kind of like runBlocking, but it makes writing a lot of tests easier. Oh, and it returns unit, so you can use it in single expression style in your test. Then we get the subject. And then we need to start observing the LiveData so it will execute. Remember the LiveData Builder won't run until someone's observing it. I'll define a little test helper called observeForTesting. This is just my test code. It's not in a library anywhere. It's just going to start an observer and then call the block that I passed in. And back to the test. The first value has already been emitted, because I've made everything deterministic with this test rule that I'm using. I'm going to use [INAUDIBLE] assertions to check that the value should equal one, and then I'm going to advance the time by one second. This is one of the big advantages of testCoroutineDispatcher. You can control virtual time. So advancedTimeBy will cause that delay to return immediately. And I have control over it in my test. So the second emit is already done when I get to this line of code. There's no need to spin and wait for a result. And this test won't be flaky. I can just say subject.value should equal two. And if we run it, we see that our test passes. The test runs instantly instead of taking an entire second. So go check out the library. Be sure to file any bugs that you find. It's currently marked experimental coroutines API until it's had enough feedback to elevate the stable. And now, I'm going to hand the mic back to Yigit to suspend the talk. YIGIT BOYAR: Thanks, Sean. OK, so much of this stuff-- what is next. So today we talk to you about how you can already use coroutines in AndroidX and other Android libraries. We introduced a new LiveData Builder that lets you integrate live data with coroutines. And the new Lifecycle skills for your view model. So coroutines scopes for your view model and your life cycles. And then we also introduced this new functionality we started which allows you to run coroutines based on your lifecycle state. And last but not least, we have introduced a new testing library for coroutines. So earlier today we announced Kotlin first, and for Android [INAUDIBLE] and Jetpack it's more like coroutines first. This is a recommendation. We believe coroutines provide the best functionality and ease of use for concurrency on Android. But we acknowledge that this is work in progress. Most of these libraries we have shown are either experimental or alpha one, but we want to develop this with the community the same way we do with architecture companies and other Jetpack libraries. So you can either join us or wait six months and then start using them. And as part of this, you will see more and more of Kotlin and coroutines coming out of Jetpack. So all of these are available in lifecycle 2.0, offer 01 starting today, so please take a look at it and let us know how you feel about that. Also, we really, really like coroutines. Thank you. [MUSIC PLAYING]
Info
Channel: Android Developers
Views: 143,235
Rating: 4.8943839 out of 5
Keywords: type: Conference Talk (Full production);, pr_pr: Google I/O, purpose: Educate
Id: BOHK_w09pVA
Channel Id: undefined
Length: 37min 49sec (2269 seconds)
Published: Wed May 08 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.