Single activity: Why, when, and how (Android Dev Summit '18)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

Got my first ever developer job in January as the only Android dev on the team and suggested we build the app like this. Haven't regretted since. Love the flow of things. Only problem is I wish we stuck with dagger for dependency injection.

👍︎︎ 18 👤︎︎ u/pculv 📅︎︎ Nov 09 2018 🗫︎ replies

So please. Since you guys already push fragment to be the major part of Android App. Add a mechanism to manage backpress in Fragments.

👍︎︎ 17 👤︎︎ u/W_PopPin 📅︎︎ Nov 09 2018 🗫︎ replies

They finally admitted that the fragment is the controller. Vasiliy keeps getting acknowledged!

👍︎︎ 19 👤︎︎ u/MrXplicit 📅︎︎ Nov 09 2018 🗫︎ replies

Hrmm their deeplink support is fancy enough that it makes me re-consider my stance, though I'm still not entirely sold on its customizability or lack thereof; I'll probably have to write a custom navigator eventually once it stops being beta.

👍︎︎ 7 👤︎︎ u/Zhuinden 📅︎︎ Nov 09 2018 🗫︎ replies

[removed]

👍︎︎ 5 👤︎︎ u/[deleted] 📅︎︎ Nov 09 2018 🗫︎ replies

An 'argument' I have with myself every other week at work. It's late here but I'll def watch tomorrow morning. Thanks for posting.

👍︎︎ 4 👤︎︎ u/TheLyndonRay 📅︎︎ Nov 09 2018 🗫︎ replies

They haven't acknowledged yet a big issue, in my opinion. They don't support nested fragments, ie. a navigator inside a fragment that is inside another navigator.

Since we can't do this, we only have two scopes available, activity and fragment. How can we share information between fragments without holding it in a viewmodel scoped to the single activity? This is very useful to represent multi step flows. Otherwise you have to pass the information along.

👍︎︎ 3 👤︎︎ u/kakai248 📅︎︎ Nov 09 2018 🗫︎ replies

The Single Activity approach is really good, but it becomes a nightmare if you need to override windowSoftInputMode for specific fragment.

You can set in on Attach/Detach fragment, however it doesn't work for Master-Details pattern and I still can't figure out how to handle it properly.

👍︎︎ 1 👤︎︎ u/ReginFell 📅︎︎ Nov 09 2018 🗫︎ replies
Captions
[MUSIC PLAYING] IAN LAKE: Hi, everyone. Thanks for joining me. My name is Ian Lake. I am a developer on the Android team. And I work on quite a few projects, but most notably the Navigation architecture component, as well as Fragments, as well as some of our new libraries, like the AndroidX Activity Artifact and Loaders. And today, I wanted to talk to you about Single Activity-- Why, When, and How, and really try and share some of the best practices from the Android team and from the architecture component team on what actually is going on in this world. There's been a lot of questions way back from 2014, '16, and even here in 2018. So we're here today to kind of talk over all of those wonderful things that make up what are activities. So activities are really a component at the Android level. So they're at the same level as content providers, broadcast receivers, and services. And they're registered in your Android manifests. And really, they are the UI facing pieces of your app. So when the Android framework goes to start your application from a launcher icon or an app shortcut or what you see when you're doing multi window, those are all activities. So they really are kind of the entry point into your app's UI. When the user goes to launch your app, they're launching an activity. And we had a very interesting quote from Dianne Hackborn back in 2016 that, "Once we've gotten into this entry-point to your UI, we really don't care how you organize the flow inside." Now, this was 2016. And I think it was controversial, I guess, in 2016. Maybe we'll just say it had 77 comments on Google+. So a lot of people were really enthusiastic about this post. But really, what does it mean? Well, what I think it means is that the framework shouldn't care. The framework shouldn't care about your application's architecture. It needs to provide the hooks needed for the Android framework to start your application. But you should probably care about the architecture of your app. That's why you're all here today. And I love having you here. So the biggest problem is that you really don't know what an activity is actually going to do. So what's the default animation for an activity? Well, it depends. It depends on the version of Android, what manufacturer you're on, and even what themes the user has selected. So similarly, we had property animations that were added in API 11. And they're much superior to view animations. And the thing is that while these new things become possible on a new version of Android, they're not always applied to everything. And things like activities, they don't support property animations at all, even today. And even if we were to add them in the next version of Android, maybe the letter after P, we wouldn't be able to backport them, because they're part of the framework. So thinking about all these things, it's like, well, OK, what should an activity be used for? Why do we even have activities? They're useful as this entry point, but then beyond that, you're in your realm. You're in your realm of what your app needs to do. And you don't necessarily need to rely on activities being the thing that you have to work with. So let's take an example. Here we have some code like this. We're calling startActivity. And we have to use ActivityCompat, because we want to do a shared element transition. So create our intent. And then we say, oh, we want to have this one element shared. Well, what API does this actually work on? Well, it depends on what you mean by "work," right? It technically launches an activity. That's true, but it's only actually going to do a shared element transition on newer devices, API 21-plus. And really, how many compact shims do we need in our life? It technically works. You're saving some API checks, but really this isn't the prettiest code to look at. And similarly, are there any hidden gotchas? If you are testing this on the latest version of Android, if you tested on an older version of Android, are you actually going to get the same experience? Well, in this example, I actually ran into this. And I was like, oh, well, things are fading in and out. And it's like, sure, I chose fade. But it turns out you need to exclude things like the status bar and navigation bar. Otherwise, they'll flicker. And I was like, OK, well, that's fun. I wouldn't have ever known that unless I tried it once. And I tried it on a whole bunch of different devices. And it turns out on some devices, it's totally fine. On other devices, not so much. So there's a lot of little hidden gotchas here that you can't actually control or rely on in your app. Another example where we have multiple activities. And really, each activity is kind of its own component in your app. So if you have two activities and you want to share data between them, well, there's not really a scope for that. There is a scope for that, but it's called your application scope. It's the same scope that's shared by services and everything else in your app. But you really want kind of a shared scope that's just within a couple of components. So this is a structure that Android provides, but it's maybe not the one you actually want to use. So what you actually want is you want to build the layering you actually need. So in this case, we can have multiple things, multiple destinations within an activity, and share information across each of these destinations by using the activity scope as an actually useful element now. So for example, you could have a shared view element or view model that both destinations talk to. So one destination could put data in, and the other one could observe changes to that data. You don't need to work at the application scope level for this to work. So I mentioned this word "destination." So what is the destination? Well, really, it's just a subsection of your UI. So for most destinations, they're going to take over the majority of your screen. Like when you move from one screen to the next screen in your app, it's going to change the vast majority of your screen. Maybe you have some global navigation, like a bottom nav, or maybe you have an action bar at the top. But the rest of this is all in a destination. So the subsection of your UI. We have a word for this called a fragment. But a fragment is really just one implementation of this idea of having separate destinations for each screen in your app. And really, this fragment is serving as kind of the view controller. The thing that isn't a view itself, but something that owns and changes those views, which are really more about display. So really, the activity, instead of owning all of this business logic and things like that, is really as small as is physically possible, and really only working on the kind that shared UI that's shared across all destinations. However, thinking about this, if we're moving from this activity world to a destination world, we really want to make that world as easy as possible. Otherwise, why would we move? And we focused kind of two things. One was that global UI kind of thing. How can we make that part easy? It's something that every app kind of has the same kind of patterns. And we really don't want that to be something that takes a lot of effort to do. Also, the navigating between destinations, that start ActivityCompat thing, can we make that even easier? So we started the Navigation architecture component and we introduced it to you all in I/O this last year in 2018. It is still in alpha right now. And we're looking to fill out all the feature gaps before taking it to 1.0 here soon. But really, what this allows us to do is take a super simple activity like this, this set content view. We'll set an action bar, because that's a thing, right? And we want to make this smart. We want to make this useful, but we still want it to fit on this one code slide. So some of this is we need a NavController. A NavController is really the heart of Navigation. It's the thing that knows everything about how your app works via a navigation graph. And we can get one-- we're using Kotlin here, Kotlin extensions-- and call findNavController. And here we're just giving it the ID of a NavHostFragment. The NavHostFragment is basically that part of your UI that's going to change whenever you change destinations. We'll also add an app bar configuration. This is what controls the up action and what needs to happen when you move up from a destination. And how do we hook that up? Well, we have a nice one liner that just says, set up the action bar. It gives it a NavController and gives it an app bar configuration. Now, because we're using a drawer layout here in our app bar configuration, we also want to open and shut the drawer when you hit the Up button. So we'll call navigate up in our support navigate up. And we've set up our whole action bar. Now it changes titles as our destinations change. We're good. If we want to add the navigation view and make sure that we can click on things in our side nav, and then go to the right place, that's, again, one line. We can do this all because the NavController knows about our destinations in our app. So then how do we actually do navigate actions if we're not doing fancy one liner stuff? Well, we can get a NavController from basically anywhere. We're in our activity. We can use findNavController. It's even easier from a fragment. We can just call findNavController. And we've built the Kotlin extension for this. And similarly, even from a view. Any view that's created by any fragment in your navigation graph can just call findNavController from a view. So you have this reference to it from basically anywhere. And we really tried to think like, all right, well, if you have arguments to something, how do we make this nice? So we built a Gradle plugin called Safe Args, which for every destination in your graph, such as this Main fragment, we generate a directions object, which has a nice simple Show Profile method, which gives you a directions object with type safe arguments that you defined in your navigation graph, and then you just call Navigate. And that's it. We'll take care of all of the fragment transaction, all of that sort of stuff for you. So it makes it a lot easier. But we can really go a lot farther with navigation. So has anyone ever built an intent filter before, deep linking, in your app? Has anyone enjoyed that experience? Great. One person enjoyed that experience. And really, you have to do this, because this is what the Android framework knows. It knows, I can parse an intent filter, and start an activity. But oftentimes, that's not quite enough. You need to go a little bit farther. So what we've done in navigation is for any one of your fragments, any destination in your graph, you can add a deep link. It's a simple one liner. And you can even add arguments right here. And we'll parse those out even for things like query parameters. We'll parse those out, and give you them as arguments to your destination. And then, because no one likes writing intent filters, we'll also generate the intent filters for you by adding a navigation graph. So this is something we actually added to manifest merger to kind of generate that for you. So all of this layering helps us build nicer APIs. But it also makes it easier to test your application. If you're testing at the activity level, all of sudden that means, well, how do I test that start activity actually did the right intent? And we have to build extra testing frameworks on top of testing frameworks to try and mock up these things. If we're moving towards more of a single activity model into the navigation controller world, we still want to test all of those things. We want that to be easy to test. So rule number one of testing things at the destination level is don't test the destination level. It's really the number one thing with testing, is making things nice and separate, and extracting some of that business logic out of a destination and into something you can test in isolation. So an example, a view model is a really nice place to put some of your business logic, because you can test it in isolation. We have a view model provider factory for providing view models, where you can just inject things into your view model. Test that totally separate from your UI. But that doesn't mean you don't want to test any of your UI stuff at all. We have Espresso tests for a reason, right? We want to make sure that all parts of our app work well and are testable. So how can we do this? Last-- this Monday, we released Fragment 1.1, the first alpha. And with this came a new artifact called fragment-testing, which is about like six years overdue. And it's really around being able to test your AndroidX fragments in isolation, separate from an activity, separate from everything else. But being able to test and verify that that fragment is doing the right thing. So super useful for things like Espresso tests, where you do want to test that logic. Your business logic, separate object, but your UI logic, what happens when you click that button, is still something that we want to verify is correct. Now, the nice part about this-- it's called FragmentScenario-- it's actually built on a different class called ActivityScenario, which is part of the AndroidX testing team. And actually, the testing team was instrumental in getting FragmentScenario out there. But the best part about this whole scenario is that it works both on instrumentation tests, tests on your actual device, and on Robolectric. So you've got one test framework that works on both of these. So a really exciting opportunity, and something that now you can test with fragments. So what does this look like? So let's say we want to test our profile fragment. We did our directions thing. We're passing in a fake user ID here. And we call launchFragmentInContainer. That's it. This one line has both created an empty hosting activity, added the fragment to it, and waited for it to be resumed. And now it's ready. You can now use this fragment. So if you want to call onFragment, and run some code on your fragment, and say, well, is the fragment in the right state? Great. You can do that. Here, we're just going to check to see if our args are what we think they are. We've passed in a user ID. We can use the other half of safe args using the args class, another generated class, and just say like, well, is the args user ID actually equal to the user ID we passed in? Did we not mess up on all of that sort of stuff? But you can see you can run any logic, any method on your fragment right from here. Or we just run an Espresso test. You say, is the user name actually equal to the user ID we passed in? When we click the Subscribe button, does it actually change the text to subscribed? Does it do its thing? We can do this with just that one line of launchFragmentInContainer. For Java users, it'll be FragmentScenario [? launch ?] in container. We obviously make that a little bit nicer for you guys. But you don't test a fragment in isolation, because fragments do talk to other fragments. And like, I work on navigation. So there's that other bit of testing of, how can we test the links between different destinations, between different fragments? And really, the nice part here that we have, because we're using these higher level components and not something like activity, is that we have a mockable layer. One of the things that we found when building navigation is that most companies, once they got to a certain point, and they're like, wow, we should add some testing. And they're like, wow, we can't really test start activities. So they built their own navigator, which just provides a layer to mock out the start activity calls. Well, that layer is handled for you. It's called NavController. We test NavController. So now what we can do in our activities is just mock out that NavController and confirm that, yes, you're calling the right navigate calls. So here we have our profile fragment again. And now it's getting our user ID. And really, what we want to test is this on viewSubscribers button. So you can tell, we click this. And like, oh, my god, it's like doing something complicated in the fragment. How are we going to test this? Here, it's calling Navigate. How can we make sure that this is actually doing what we want it to do? Well, it's pretty easy. We can do our scenario thing just the same, launch fragment. And now we can just mark out our NavController. And now you call onFragment. And what we're doing here is actually just creating our own NavController. There's no nav host here. But we can just inject one. This is actually what NavHostFragment is doing under the covers. It's calling setNavController on a view, and saying, here's my NavController. But now what we've done is from this fragment's point of view, it has a navigation controller. All those findNavController calls that normally you'd have to inject something in to get your NavController, now it just works. They're in there. And now we can just run Espresso tests, and say, click on the viewSubscribers button. And the nice part is that because we're using these directions class, we can use them also in our tests. And because they implement equals, we can just do a simple verify, and say, verify, did you actually navigate to where we think you're navigating? And even if there's a lot of parameters in there, if there's extras and other options in there, we can now just verify. And this makes it so much easier to test those interconnections between each destination. So nav control is kind of a special case, because we find a NavController. So many other things aren't a service locator kind of pattern. It's we need to inject in those dependencies. And this is another one of those like "six years too late" kind of a thing, but we're finally working on it. So there's a class in Android P called AppComponentFactory, which allows you to construct activities, services, broadcast receivers, and services, all via dependency injection. You get a chance of calling the constructor, instead of the system calling your constructor. The same thing here with fragments, where now you can actually do constructor injection into fragments. You no longer need to only have a no-args constructor to use fragments. You can use a FragmentFactory to instantiate your fragments for you. So this is really useful also for cases where your fragment was like passing your activity to something. I know we probably still have a template that does this. We'll fix that. And there's lot of ways where really we want to inject in all of those external dependencies so we can test again in isolation. And FragmentFactory works great with our FragmentScenario. So what does this look like? We know how to test a view model. It's just an object. You instantiate it. You do the thing. And it has a real method called Subscribe. But really, we want to test our fragment. And our fragment has an onSubscribe method that calls viewModel.Subscribe And it does its thing. How do we get this viewModel? Well, we can inject the factory itself, inject the viewModelFactory. And here we're using some of the other new stuff in Fragment 1.1 that's by viewModels, another Kotlin property delegate that does all that viewModel providers of kind of stuff for you. But we now have a fragment that, well, we've injected something, but then we still need to test like, OK, well, did it actually call subscribe? We're back to the same situation of building testable code. We can build a navigation activity. This is what it's going to look like in real life. We're going to inject our viewModelFactory. And then because code is hard and I wanted to write things on slides, I built a helper class called Initializer FragmentFactory. That basically just calls add Initializer for each fragment. And we call that method to construct your fragment rather than use the default no-arg constructor. So a little bit of magic. There is a link here if you want to check it out. We're looking at trying to integrate this more deeply into the actual library itself. But once you've called this FragmentFactory, now whenever your activity creates a profile fragment, instead of using that no-arg constructor, it's going to use this constructor. It's going to pass in our viewModelFactory. So our activity looks fine. But our test, how does that look? Well, we create a mock of our profile viewModel. And then we can set up a factory for it. And then, again, kind of use a FragmentFactory here that, again, does the same type of thing where we're passing in our mock viewModelFactory. And then our scenario looks almost the same. We just add it in and add instead of just the arguments, also the FragmentFactory. Great. Now we can do our same thing on view. Perform the click. And then verify that, yes, our mocked out viewModel did the subscribe call. So now we have a testable fragment, a testable viewModel. And we've injected all of the dependencies into our fragment. We actually have a testable thing. Now, we are looking at some improvements to this API, because we want to make this even easier. So in this case, because we know you're constructing a profile fragment, what we want to change this into is actually something that looks like this, where you can say launch, and then give it a method saying, oh, launch this fragment, and specifically give it the constructed out instance of your fragment. So you don't have to actually know that, oh, it's using a FragmentFactory under the hood. You can test just one fragment just fine. Now there are a few cases where you might think, oh, man, maybe I do need multiple activities. And there's got to be reasons to use multiple activities besides just momentum. I understand a lot of apps if you have multiple activities right now, this isn't actually an easy sell. So there are a few cases where even today we do recommend using multiple activities. Not a lot, though. So what I'd like to say is you don't actually need multiple activities. What you need are multiple tasks. So what are tasks? Tasks are actually the thing that users are actually interacting with. So a task is a stack of activities. And each task has a back stack. So in your Overview menu here, each one of these entries isn't just an activity. It's actually a whole task stack. So you're only just seeing the topmost activity of that stack. So each element here is a stack. When you're doing split screen multi window, that's two tasks side by side. On Chrome OS devices, things that support floating multi window, each one of these tasks is a window. So a one to one between windows and tasks-- not activities and windows, tasks and windows. So launching a new task on one of these Chrome OS devices gives you a new window. So your app, maybe it doesn't need multiple activities, but maybe it wants multiple windows. So this is a case where, yes, you need to use activities under the hood. Each one of these tasks is going to be a separate activity. But you may not use some of the other things, such as a stack of activities, in one task. So what does this actually look like? Well, a lot of this is that there are a lot of different ways of saying new task. Has anyone looked at all those wonderful launch mode flags and all that fun? Yeah. How many people that have used it are still sane? OK. Well, I'll say that there were a lot of good flags out there in Android 1. They were great back in Android 1. Today, in 2018, they're maybe not the best thing to use. What you actually want to use is documentLaunchMode. documentLaunchMode was added actually an API 21. So please if you're thinking about pre-API 21, first, what are you doing? And second, probably try and steer away from hacky solutions. Maybe it's just not worth it for those users. But try and avoid things like launch mode flags and task affinity and those type of things, because while the framework does honor those, it maybe doesn't honor them in the way you want them to honor it. They're certainly a very different kind of thing. So what can you actually do with documentLaunchMode? Well, the biggest thing is multitasking. If you can have multiple tasks, then you can have multiple windows. You can have multiple entries in your Overview screen. So the first way of really doing multitasking is intoExisting. Now, intoExisting basically means whenever I launch this activity, that activity has its own task. Every time you launch this activity, it has its own task. But if we already have launched that task, don't create a second and a third copy. So this is really useful for things like documents, conversations, things where someone might want to side-by-side compare two different documents. If they're copy-pasting from one document to the other, they're not going to exit out of one doc, open one, copy it, and then open the other one, and then copy it into there. This is kind of taking that multitasking model that is Android, that is that recent screen, and making it so that your app actually gets to use this. Now, of course, the intoExisting assumes you have some notion of uniqueness. So it does assume that from an intent filter equals kind of point of view, like if you're using the data URI on your activity, that there is some sort of unique ID, a conversation ID, a document ID, something to uniquely define that task in and of itself. Now one great example of this that you can try out on your phones is Google Docs. So Google Docs, when you open a doc, it actually launches it in a another task. And if you have multiple docs, you can actually load them up side by side on a phone, two different windows on a Chrome OS device. And it just works. Even though it's one app, it can have multiple windows and really allow a different level of multitasking between different things. So another big one is creating new content. So new content is a little different, because there's not really any unique ID. But you still want that kind of multitasking behavior where you can reference existing material while you're creating something new. So the Always flag is very similar to into existing, but it just always create something new. Wow, it's like self-descriptive names. We can do this, guys. And it allows you to do multiple things at once. That's great. So one example of this is Gmail. So Gmail actually uses this kind of mode when you create a new email. So this allows you to create a new email and reference your existing email at the same time. Magic, right? This is the equivalent on mobile of when you do it on the web and it pops up a little mole that's separate from the other one. You still need that other material as reference even when you're creating something new. Of course, on a phone or on a tablet, it looks slightly different. The other case is picture-in-picture. Now, picture-in-picture actually has two entirely separate modes for how you want to approach picture-in-picture. One is using a separate task. So this would be a separate activity just for your playback. So this is really common on Android TV devices. For example, Google Play Movies and TV uses this approach so that you can actually put things into picture-in-picture mode, and then browse through other movies. So in this mode, it's very much that you have a specific Picture-In-Picture Mode button in your UI. The other mode is using just a single task, one activity. You actually don't need anything at all here. And this is the approach the things like Duo and Google Maps use, where your whole task is becoming the picture-in-picture activity. So when would you want to choose one or the other? And it's really this case where once I'm in picture-in-picture mode, if they were to click my Launcher icon, what would happen? Because the Launcher icon always launches the default task of your app. So in Duo's case, where they only have one task, when you launch that Duo from your Launcher icon, it's just going to pop open the picture-in-picture. You go from a little tiny picture-in-picture to full screen, because it doesn't make sense to have multiple conversations going at the same time. You're never going to replace one with the other. You're never going to continue to reference something even though something is already going. So that's kind of the differentiator here, where do you want to be able to browse and picture-in-picture at the same time? If you do want to browse, then yes, you are using the Android framework, and therefore they need to position those separately, separate tasks, separate activities. But that's kind of it. The one thing I didn't mention are things like Instant Apps. Now, Instant Apps kind of works at the activity level. You call startActivity. It downloads your Instant App module. But there's actually some really exciting things that are being worked on by the Play team. I think there was a talk here at Android Dev Summit. But a lot of it is around the instant experience for your app bundle, making your whole app instant, and also dynamic feature modules. These are really interesting ways of adding things onto the thing that don't require a specific app architecture to implement. We can actually do more things with this. So for the instant experience, it's adding this distribution instant equals true. Now, this means that someone can try your entire app, your entire base module, all at once, totally instant. And this works really well with things like app links. So those deep links that I said you can add to any destination, you can also make them into app links by adding the autoVerify="true". And that means you skip the disambiguation. And when someone launches your deep link on the web, they're going to open up your instant app experience. They're going to download that whole base module for you, and your whole navigation graph is already there, ready to go. But this doesn't work if your app is too large. So you want to dynamically deliver things that are not used very often or rather big things. That's what dynamic feature modules are all about, about being able to download them on demand. Now, the really interesting part about dynamic feature modules is that you're adding classes. You don't need to add activities. You can add just destinations, just a number of fragments. So in this case, you can add new destinations on demand. Just because you've built out an XML file for your navigation graph statically, each one of these dynamic feature modules can also add their own navigation graph. So this means that we're not tied to separate activities. We can now actually still use one activity. Now, there's still more to be done here, both on the Play side and on the Navigation side, but we want to make this really easy for developers to use. So what we want to get to is where you can add something to your navigation graph with just a feature tag, just like you'd add a fragment tag or something like that. And when you navigate to this destination, that's actually going to do all of the downloading and making your feature module available from whatever your split name is. So that's the world we want to get into, where you can use a single activity, where you can use a lot of the useful things, like deep linking and navigation, without necessarily contorting your app architecture around everything else. So I'd really like to end this with one note. A lot of you have existing apps that have very different kind of experiences. And I'd like to say, do what's right for your app. I think single activity is great. If I was writing a new activity, it would also be a single activity. But I realize that going to your PM and being like, hey let's rip the whole app apart, is sometimes a hard sell. Some of them don't like your current app. So maybe you'll actually get some, yeah, OK, go for it. It really depends on your own experience. If you find yourself contorting your own experience and it's not making sense to you, don't do it. If something is working, that's good. Keep it working. But if you're finding you're running into issues, you're having inconsistent behavior, or you want to do things like share viewModels, maybe that's the time to think about moving towards a single activity structure. So thank you. Q&A will be outside. I really appreciate you all coming. [MUSIC PLAYING]
Info
Channel: Android Developers
Views: 118,927
Rating: 4.8765044 out of 5
Keywords: type: Conference Talk (Full production);, pr_pr: Android, purpose: Educate
Id: 2k8x8V77CrU
Channel Id: undefined
Length: 39min 16sec (2356 seconds)
Published: Thu Nov 08 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.