Android Jetpack: Sweetening Kotlin development with Android KTX (Google I/O '18)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

Great talk, u/JakeWharton!

πŸ‘οΈŽ︎ 35 πŸ‘€οΈŽ︎ u/farmerbb πŸ“…οΈŽ︎ May 10 2018 πŸ—«︎ replies

Don't let the boys at r/mAndroidDev see this

πŸ‘οΈŽ︎ 27 πŸ‘€οΈŽ︎ u/iCodeInFlutter πŸ“…οΈŽ︎ May 10 2018 πŸ—«︎ replies

Woah I never thought about adding extension operator overloads for componentN to turn something into "data class like", that's cool!

For people who want button.click {, you can add

implementation "org.jetbrains.anko:anko-commons:0.10.4"
implementation "org.jetbrains.anko:anko-sdk15-listeners:0.10.4"

And you'll have a view.onClick {.


This @ExtensionFunction KEEP sounds really cool, and simple compared to typeclasses that is for sure :D

πŸ‘οΈŽ︎ 11 πŸ‘€οΈŽ︎ u/Zhuinden πŸ“…οΈŽ︎ May 10 2018 πŸ—«︎ replies

I am most curious is how Google is going to get from Android to Fuchsia and if things like JetPack are being created with an eye towards that future?

We now have an ART branch on the Fuchsia code tree. So looks like Android will sit on top as a first class citizen and then also Flutter. What is the plan on bringing this all together over the next couple of years?

Then the biggest question. Will Fuchsia finally force OEMs to update their damn phones? Now have a Pixel 2 XL so no longer have to worry about it but for everyone else.

πŸ‘οΈŽ︎ 8 πŸ‘€οΈŽ︎ u/bartturner πŸ“…οΈŽ︎ May 11 2018 πŸ—«︎ replies

That troegs

πŸ‘οΈŽ︎ 2 πŸ‘€οΈŽ︎ u/TheHal85 πŸ“…οΈŽ︎ May 11 2018 πŸ—«︎ replies

Wharton bless

πŸ‘οΈŽ︎ 1 πŸ‘€οΈŽ︎ u/iwouldntknowthough πŸ“…οΈŽ︎ May 11 2018 πŸ—«︎ replies
Captions
[MUSIC PLAYING] JAKE WHARTON: Hi, everyone. My name is Jake. I work on the Android team on Kotlin stuff. And so today, I'm going to be talking about Android KTX. And I'm not going to be just going over a bunch of the stuff that's in there. I want to make it a little more interesting than that. So I'm going to start with a little bit of what happened last year at Google I/O. I was here last year, talking about how you can write extensions for Android types, such as this example, where we have code that iterates over the views inside of a ViewGroup. You can pull that common code out into an extension. And what this extension does is enhance a type that we don't control, the ViewGroup type. We're allowed to, essentially, create a member function that's not actually a member function. It actually turns into a static function in the bytecode, with the functionality that we want to enhance. And so we can take our original code that had the explicit for-loop in it and use this new member to create a more concise version of what we intended to do. It is actually visually distinguished from a normal member function, that it's italicized. If you use dark yellow, it'll actually be yellow. But it's semantically equivalent to calling a member, or the intent is to feel semantically equivalent. And so oftentimes, when you start talking about extension functions, you think, well, if this is so useful, why don't we just put the function directly on D-group? Why doesn't ViewGroup just offer a for-each and a for-each index that takes in a lambda? And really, the reason is because of the lambda. When we pass a lambda in Java 8 or in Kotlin, by default, that has to create an anonymous class, which eats up methods and causes class-loading. Kotlin, however, provides language functionality, which allows us to eliminate that lambda's allocation. By marking the function as inline, the body of the extension gets copied into the call-site, and we have a zero overhead abstraction. Let's take a look at another example. In API 23, we were able to get a system service based on the class type. And in 27.1 of the support libraries, a ContextCompat version of this was added that allowed it to work on all API levels. We can pull this into an extension that is also inline, like the previous one, but doesn't contain a lambda. What this one has that's different is something called reified. Now, this is a compiler trick. And what that trick does is it forces the type information of the generic to be known at compile time, so that it can be made available at runtime. And so this is what allows us to-- where we would otherwise be calling class.java on notification manager, we can now abstract that away behind this extension. And so our calling code now becomes simplified to just be able to pass the generic. And because it's reified, the implementation of that has access to be able to call class.java. So if we want to update the padding of a view, just where we're only specifying two of the four parameters-- in this case, we want to update both the left and the right. We have to pull out the existing padding for the top and the bottom because Android requires you to specify all four. This is something that we can remedy, again, using an extension function. The key here is that, for each of the arguments on this new function that we've defined, we're specifying a default. And that default will be used when a value is not provided for that argument. So it allows us to take the calling code where we're specifying all four and, now, specify them as just two. But the problem here is that we've eliminated two of the arguments. But since we're only supplying two, Kotlin takes that as meaning the first two, and the latter two are the ones where the defaults are used. This is not what we intended. We intended to do left and right, which are the first and third. Another language feature comes to help here, which is named parameters. By specifying the name of the parameter, we're able to tell the compiler which of the two arguments we're specifying, and allow it to fill in the defaults for the others. OK. Android APIs have a bunch of composite types. These are things like point, rectangle, pair-- even the location class. These composite types are just wrappers around smaller individual pieces of data. In this case, I'm calling an API, which has a rectangle, which is a composite around the four-- the left, top, right, and bottom-- values of a rectangle. And if you need to do calculations based on the values inside of these composite types, you have to pull them out into individual values or variables in order to do that calculation and then, potentially, put them all back together. So with the help of the extension, we can avoid this. This one is a little bit different. We have a new keyword called an operator. An operator means that Kotlin will allow us to use a special call site syntax. And each operator function has a very specific name-- a well-known name. You can't just make up any name. And the name defines which call site syntax that you're intending to create. In this case, it's called Component. And Component allows us to use a feature of Kotlin called destructuring. And so our original code, which had to individually pull out the four different components, can now use this call site syntax where the rectangle was automatically unpacked into the four values and assigned to four variables with the names that we choose. What's really nice about this is that, if you don't care about ones later on, you can omit them. And if you don't care about ones in the middle, you can specify them as underscore. And so, if we just need to pull out two of the values, we can do that very succinctly. OK. An experienced Kotlin user might know that-- well, I guess we flipped through-- could go back a slide? OK. So this is some code that shows how we could determine whether or not a string contains only digits. It basically just loops through the characters, using Kotlin's for-in syntax; checks whether it's a digit, using an extension function on character; and then sets a value to true or false whenever it detects a non-digit. If you're an experienced Kotlin user, you might know about the All function, which exists on string, which encapsulates the same looping and allows you to specify a predicate, which, in this case, is digit. It's actually an inline function, so it desugars into the exact same thing we would have wrote in the previous slide. But what's interesting is that Android actually has a built-in function for this. And I suspect that a lot of people don't actually know this exists. And so this is something that we can actually take and turn into an extension. But you start to wonder, is this actually worth its weight in an extension? What value do we gain by turning this static method that we can call into an extension? Well, for one, it changes the way that we invoke to feel a lot more natural and idiomatic in Kotlin-- sure. But still, is there really value that we extract from this? The biggest one that I think we gain from this is that, when you're in the IDE, you have your string, and you're wanting to make this query as to whether or not it contains only digits. If you didn't know that static method on TextUtils was there, you probably would never find it. When it's extension, if you start typing in the IDE, it will actually show this extension and auto-complete, where it's much more discoverable than otherwise. So you just press Enter. And you get it. All right. So I covered a few extensions here. I just wanted to remind you a bit of the power of these extensions, the fact that we are leveraging language features that exist only in Kotlin, not in the Java language. And actually, some of these examples we're going to keep coming back to throughout the rest of this talk. So all of the extensions that I just showed are part of the Android KTX library that we announced in early February. There's been two releases since then. And as of Tuesday, it's now part of Jetpack, and versioned with Jetpack. So on Tuesday, Core-KTX is now 1.0 Alpha 1. It's going to be versioned and released with future Jetpack libraries. So we called this Core-KTX when we launched, which was kind of a weird name. It didn't make sense. This was for extensions for types only in the framework. A lot of people suggested, can we add support library stuff? And we were very adamant about saying no. That should, hopefully, make a lot more sense now. But even this isn't exactly true, because Core-KTX initially depended on support-compat. Support-compat is there to provide backwards compatibility versions of things that are in the Android framework. And so earlier, I showed the example with ContextCompat. That's something that came from support-compat. And so now, with the Jetpack rebranding and the Android X packages, support-compat has become core. And so now, Core-KTX lines up with Core. So we kind of knew what we were doing back when we started this. And now, it's only starting to pay off. Along with the other Jetpack libraries, there's actually a few new KTX libraries that are launching with it. So we have ones for fragment, collection, SQLite. For the newer components, navigation, and work runtime. I'm going to touch on how you can discover these a bit later. But I want to talk a bit about scoping, about how we determine whether or not something should go into one of these libraries. All right. So in Core-KTX 0.3, we offered an extension that looked like this. If you look at its signature, it's an operator. And it operates on color. And the name is Plus. So this allows us to use the normal plus syntax for adding, for compositing two colors together. So the signature-- it's definitely an extension. But the body of this looks very different than the other extensions we looked at. There's a significant amount of code in here. If you look inside, support-compat, which is now Core, there is a color utilities class. And that color utilities class has a method called composite colors that works on integer colors. It allows you to take a foreground and a background and turn them into a single color. So this is the perfect candidate for placing the implementation of what that extension function was into this class, so that everyone can use it-- so that can be used from the Java language or the Kotlin language. And so in Core-KTX 1.0, this actually has been rewritten to just delegate to that ColorUtils. So the Java language users get that functionality. But the Kotlin users get the enhanced syntax. And if you look at the extensions that we talked about so far, the bodies of them, the implementation of these functions, they're all trivial. They're exceedingly trivial. And that's by design. And this gets me into covering some of the principles that we defined that KTX extensions should have. And so this first one is that we want to adapt functionality that already exists. And if we want to add any new features, those should be redirected upstream to a place where they're language agnostic, where both languages can take advantage of them. Other examples of this-- there was some HTML compat stuff and a path iterator that were implemented first in Core-KTX that have since moved upstream into Core to be able to be used in both languages. Another thing that's common to all these extensions is that they're marked as inline. The reason that we do inline on the first one-- the one at the top-- is that we want to avoid the lambda allocation. For the second one, because we're using reified generics, we're actually forced to use inline by the compiler. The third, the component ones, and the very bottom one are all inline, mostly because they're just aliases to what you would otherwise write if the extension didn't exist. If we look at an example of something that's not inline in Core-KTX, we have this iterator extension to ViewGroup, which allows us to use Kotlin's for/in syntax to iterate over the views and a ViewGroup. This is not inline for a very specific reason. And that is because the implementation of this function defines an anonymous class. If we were to inline this, that means that every time you use it, an anonymous class would be defined at your call site. And so this would increase your deck size, method count, and class loading. We explicitly make this not inline, because we want that single implementation to be reused by all of the callers. So we default to an extension being inline unless there are allocation reasons. And I should note that this is really only for KTX-style extensions. In normal Kotlin code, this is not a good recommendation. You don't want to default to inline because it has the potential to lead to actually having a negative effect on your code rather than a positive one. All right. So earlier when we showed this extension, I talked about how the inline modifier, coupled with the fact that there's a lambda, allows this extension to be a zero overhead abstraction. In the reified case, we get the ability to have a more declarative version of the lookup at the call site without having to specify the colon colon class.java. For updating the padding, we get to use default values to not have to specify each of the arguments, and name parameters to specify which subset of arguments we want to actually provide. For the destructuring case, we get the fancy syntax that allows us to pull apart the component variables out of a composite object. This is useful. This is enabled by the fact that we have operator overloading in Kotlin. We also talked about how we were able to add the plus for color. For this one, we're aliasing an extension to a static method. And this is just to help improve discoverability for built-in helpers that you might otherwise not know exist. And then, for types that are collection-like but not actually collections, we have the ability to turn them into pseudo collections, where we can use the affordances of the language as if they were actual collections. And so each one of those has a very Kotlin-specific language feature that it uses. And we want to make sure that all these extensions that we're defining leverage some feature of the Kotlin language that doesn't otherwise exist for Java callers. We want to resist trying to fix an API just by creating extensions for it, but rather enhance it to become more pleasant to use by leveraging these Kotlin-specific features. OK. One of the suggestions we get quite frequently is to take something like setOnClickListener and write an extension, which allows you to call it using something like Click or OnClick. This allows the calling code-- instead of having to call setOnClickListener, we get the shorter version of Click. Are we leveraging a feature of the language here? Well, we're leveraging extension functions, but not really. We're really just creating a shorter alias. What value are we extracting from this extension? Well, we're typing a few less characters. But really, it's autocompleted anyway. But even worse, what precedent will we be setting here by adding this extension? Are we going to do this for every listener? And so this is a great example of something we explicitly do not want to do in the KTX libraries. If you're not familiar with the term, we call this code golf, where you have the desire to create the shortest code possible. This is something we do not want to do. We're not here to just make the code shorter. OK. There's another one that gets suggested every now and then, and that I've seen people using. With Android, because of the different API levels we have to support, you very frequently see these IF checks around the SDK int. So it can be tempted to pull this out into an extension, where you have a little bit more declarative version of this. We move the comparison into an extension function. It's an inline function, so we don't have the overhead. The lambda's the last parameter, so we get the nice Kotlin call site syntax. And it turns our IF statement from this into this. Now, this by itself is not too terrible. We're really not leveraging any of the language features. Again, similar to the last one, it's still kind of an alias. But at least this one, you can argue a little bit more for its merits. But there's a problem. While these two statements are equivalent, what happens when-- oh, one thing is that you can at least static import SDK int, and then they're a little bit closer. So that's one reason why this is less justified. But one thing is that an IF statement is a very primitive construct of a programming language. And because an IF statement is not just an IF statement, there's constructs like ELSE. So what if your requirements change such that you need to alter the behavior on these two different versions? Well, if you were using this extension that you wrote, in order to support this case, you either have to change back to using an IF statement or you have to modify the function, where maybe it takes two lambdas now. One for the case where you're above 19, one for the case where you're not. Because we're not taking two lambdas in this function, we've lost the special trailing lambda syntax, where we now have to pass them as arguments inside the parentheses, whereas before, we didn't. So immediately, this extension starts falling apart. If we introduce another conditional branch-- maybe we need to vary the behavior across APIs in three different ways-- well, there's really no way that we can make the extension do this. The other thing that is different about this extension compared to the IF statement is that we're assuming the conditional that we want to check is greater than or equal to. That the behavior we want to run in the lambda, we only want to run on 19-plus. Well, a lot of times, some of the IF statements-- again, SDK int-- will be less than or equal to. And so now, we need a second extension in order to support that use case. So this is another example of something that we're not looking to do. We don't want to optimize for just a single use case or a specific use case, where the extension only supports one way of doing something. And then, when you need to move to something more complex, you have to revert to the original behavior. We want the extensions to allow you to express everything you would need to express if it didn't exist. OK. So all the extensions we've been talking about thus far have been ones that are in the Core-KTX library. I don't want to go through a ton of the extensions that are in these other libraries. Again, I'm going to show you how you can discover them in a bit. But I want to touch on one. So for the fragment KTX, we have an extension, which encapsulates transactions. We move the beginTransaction and the commit function calls into an extension. We use the fact that we can use an inline function and a lambda, again, to turn this into a zero overhead thing. Our calling code then becomes a little bit shorter, where we now use the transaction with a lambda body. So if you've used fragments, you'll know that commit is not the only commit function. There's actually more than one. And so we can model this by doing something like allowing you to supply a Boolean as to whether or not you want to allow state loss or disallow state loss when you're committing. This is really easy to accommodate. But it sort of goes against something I said earlier, where-- oh, and we can update our call site to be able to use this. It goes against something I said earlier though, where I talked about minimizing the impact of the implementation of these extensions. Since this is an inline function, and then we've now put a conditional inside that inline function, that conditional is being inlined into all of the call sites. And so all of the call sites now have to have that conditional inside of them. So is this actually a bad thing? Well, if we look at the bytecode that gets generated from the call site, when we specify allow state loss true-- you don't really have to understand bytecode to understand what's going on here. There's essentially three function calls. The first one is beginTransaction. The second one is that replace, which was inside the lambda. And the third one is just a call to commitAllowingStateLoss. There's no IF statement here. There's no conditional. And that's because, since this is an inline function and since the argument is a Boolean, the compiler actually knows, at compile time, what value you're supplying. And so since it knows at compile time, it can actually do dead code elimination and eliminate the branches that can never possibly be executed. And so you actually get, in bytecode, what's equivalent to what you otherwise would have written. And there's actually more commit functions. There's one which allow you to commit now and commit. So we can also support that by adding an additional Boolean. And the same thing happens here. Even though they're now nested, dead code elimination will make it so that there is only one function call in the resulting bytecode. OK. As part of this effort of all these releases at I/O, one of the things that we've done is start creating a Kotlin-specific view of the libraries that we publish in the Android framework itself. So if you see, in that blue box there, when you visit the reference docs, it'll actually ask you if you want to view a Kotlin-specific version of the platform or Android X libraries. And also, if you scroll down in that left navigation pane, at the very bottom, we have links to them, as well. And what these are are a Kotlin view of these libraries. And so when you're browsing through, say, the fragment package, you'll be able to see the extensions for fragment inside the documentation. It's no longer completely separate. One thing that's missing right now is that we don't actually tell you the maven coordinates of the artifact that these come from. That's coming soon. And also, the extensions in Core-KTX, which extend the platform types, don't yet show up on the platform docs. But this is something that we wanted to get out to show you that it's being worked on. And so hopefully, those two things will be coming soon. All right. I'm here to talk about Android KTX. But Kotlin extensions-- there's nothing Android-specific about it. What we're doing is building extensions to try and make these libraries more Kotlin-friendly. And that's something that any library can do. And so I want to talk about the ways that we think about how we can make libraries more Kotlin-friendly that apply to both the Android libraries, but also apply to libraries that you might be writing or you might be using. The first way to make a library really Kotlin-friendly is just rewrite the whole thing in Kotlin. Obviously, this isn't feasible for every library, but it's certainly an option for some. If it's a library that's private to your app-- it's in your repository or it's internal to your company-- and you're already using Kotlin, this is a viable option. It doesn't seem like something that's totally viable for, say, the Android framework. And I'm not quite sure we're at the stage where an Android X library could do this. Maybe a future Android X library could be written in Kotlin. That seems like a strong possibility. What we've chosen to do with most of the things that we publish is sibling artifacts. So the main library remains written using the Java language. And we ship Kotlin language features as a sibling artifact. What's great about this is you don't force the Kotlin standard library onto your consumers unless they explicitly want it. You can curate the extensions to be exactly what's needed to augment your API, where you get the Kotlin-specific features. And what's really nice about this is you don't have to control the library that you're extending. So if you're just consuming a library and you want to make part of it more Kotlin-friendly, you can do that. You can do that either in your own app or you can publish a set of extensions for a library that someone else publishes. But are these the only two options? I want to take a look at something that I think will lead into a third somewhat hybrid option. And I go back to this simple alias extension, where we've taken the static method defined in the Java language and turned it into an extension method in the Kotlin language. If we look at the implementation of this class on the Java side, I've included the first line, because we can see that it immediately dereferences the argument that we pass in. As soon as we pass in a string, it says, what's the maximum number of characters that I can iterate over, in order to determine whether or not there are digits? And so if you've been using Kotlin with Java APIs, you might know that this means that the parameter is going to be exposed as what's called a platform type. It has unknown nullability. But from the implementation, we know right away that this method simply cannot accept null values. And the way that we would fix this is by adding the non-null annotation. So what this annotation does is it informs the Kotlin compiler that there is a restriction. That there is special behavior that it needs to take into account, where it needs to enforce that no one passes a potentially nullable value, or null, into this method. And so this is enabling a language feature in Kotlin that simply doesn't exist in Java. Now, you can use tools that will allow this enforcement to work for Java. But it's not intrinsic to the language itself. So if we can do something like that for nullness-- if we can add this annotation for nullness to inform the Kotlin compiler that it needs to change its behavior when we invoke this method, can we do this for something else? Say, I want to take this static method where the first argument is really the receiver. And can I say that this is actually going to be an extension function when invoked from Kotlin? And what this allows us to do, potentially, is eliminate the need to have this explicitly-defined extension at all. This extension only exists to change the calling convention, to inform the compiler that we want to allow you to call it in a different way. And so now, we're left with just this. The Kotlin compiler sees that annotation, just like it saw the non-null annotation, infers something from it, and allows you to call it in a way that's more idiomatic for that language. In the bytecode, we get what we otherwise would have written. We still get the call to the static method, and the receiver becomes the first argument. How about this example? One thing you might have noticed is that this extension is named updatePadding, not setPadding. Now, we can actually call this extension setPadding. But the problem is that it will only work for a subset of arguments. So in this case, where we're just passing left and right values, we could call that setPadding, and it would work fine. But if we passed left-right and then top-bottom, we'd be supplying four arguments. And the Kotlin compiler is going to see that the real set padding also accepts four arguments. And it's going to prefer calling the real one. And the real one doesn't have named parameters. So you're going to get a compilation error. That's the reason we have to name this updatePadding. If we look at the real setPadding-- a simple method that takes four integers-- what if we could inform the Kotlin compiler that these parameters have names associated with them? Now, it'd be nice to infer this just from the parameter names directly and not have to specify the redundancy. But I'll argue that, for one, it's very nice being explicit about these names in the annotation. In Java 8 bytecode, there actually is a way for you to retain parameter names. So the Kotlin compiler could, in theory, use those. But one problem is that then it becomes an all-or-nothing thing. You have to opt in to this behavior. And then suddenly, every parameter name across your library is set in stone. Whereas, with annotations, that's something that you could incrementally migrate. So this has the potential to solve the naming part where, now, we can call the real method from Kotlin and specify the four arguments in any order that we want, based on what names we provide. How about the default value? What if we could specify a Kotlin expression, which allowed the compiler to supply a default when one wasn't supplied by you? This would change our original extension calling convention from calling our extension to actually just using the real method. And then, in the bytecode, we get the thing that we started with, the thing that our explicit extension would inline to. But now, the extension doesn't have to exist. The metadata that we added in the form of annotations informed the Kotlin compiler that we wanted to enhance our ability to call this function in a Kotlin-specific way, leveraging the Kotlin features. And so we're able to do so. So Kotlin has this process, which is called KEEP. It's Kotlin Evolution and Enhancement Process. And just this morning, we proposed these annotations as KEEP-110. So this is something we're proposing to add to the Kotlin compiler, so that it can understand these annotations. We have ExtensionFunction and ExtensionProperty, which are for static methods; DefaultValue, which allow supply and default values for parameters; and then KtName, which allows you to provide an alternate name for methods, fields, or parameters. Now, it's very important to note that this is extremely early. These names might change. The semantics might change. This may never actually be accepted into the Kotlin compiler. We have been working with the JetBrains team for quite a while on this. And some of this is already prototyped inside the Kotlin compiler. We really think this would be a way that we could enhance the Android framework for Kotlin callers, without actually having to go and rewrite the Android framework, or at least its API, in Kotlin, which is really not feasible. And it's also important to note that, while this is an option-- assuming that it actually makes it into the Kotlin compiler-- it doesn't totally solve every problem that our existing extensions are solving. We determined these annotations that we proposed in KEEP-110 through looking through a bunch of open source libraries, looking through our own libraries, and seeing what we thought would be the most useful extensions-- what the pattern of Java methods were, such that they would want to be turned into extensions. And so the latter two really are complementary. The big advantages of the annotations is that you retain the single source of truth. You don't have to really know Kotlin. You don't have to add Kotlin compiler to your build system. You don't have to publish sibling artifacts. Even if you're a pure Java library, you can add these annotations and just enhance your API, so that Kotlin callers get the more idiomatic syntax. All right. So to sum up, Core-KTX is now part of Android Jetpack-- versioned with Android Jetpack, released with Android Jetpack. There's a few new artifacts, as you can see here on the screen. There's definitely more coming. Notable ones that we think are missing are slices and view model. So I would not be surprised to see artifacts for those in the coming months. Please check out the Kotlin version of the reference document. This is extremely early. This required changes in [? doca ?] and how we produce docs. And so it's something that we just wanted to get out there and show you as a preview. This is definitely something that's being actively worked on. There's a new component on the Android bug tracker for Android KTX. Because Core-KTX and all the KTX libraries are now a part of Jetpack, the source of truth has moved into the Android Support Repository. We're going to be migrating the GitHub issues on the GitHub project over to this bug tracker in the coming weeks. But it's important to note that we're still going to be accepting pull requests to the GitHub repo and syncing things back out to the GitHub repo. It's just that the issues will no longer be the source of truth on GitHub. It will be on the Android bug tracker. The [? KEEP ?] was proposed-- I created the pull requests about an hour ago. Please go check that out. The document contains a lot more detail about examples. And like I said, the annotations that were chosen were the ones that we think have the most impact. But at the bottom of the document, you'll see that, if something like this gets accepted, there's a potential for future enhancement of even more. The link to that should be this. I made this link last night before I submitted it. So hopefully, it's accurate. And that's it. Thank you. [APPLAUSE] [MUSIC PLAYING]
Info
Channel: Android Developers
Views: 48,057
Rating: 4.9482203 out of 5
Keywords: type: Conference Talk (Full production);, pr_pr: Google I/O, purpose: Educate
Id: st1XVfkDWqk
Channel Id: undefined
Length: 40min 24sec (2424 seconds)
Published: Thu May 10 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.