Hilt - Android Dependency Injection

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[MUSIC PLAYING] DANIEL SANTIAGO: Hi. My name is Daniel Santiago. And I'm a software engineer at Google. And in this video, I'll be talking about Hilt and Android dependency injection library. I'll go a bit into how to set it up, how does it work internally, and a quick recap on what is dependency injection. Because we can't have a video about dependency injection without first defining what it is, you might have heard of this already or already know it. It will be a quick recap. And it'll be good for some follow-up points I'll be making. In essence, dependency injection is a programming pattern. Here's a concrete example. If we have a music player, it's a class. It needs a database and some codecs to work. In a world with no dependency injection, these dependencies to make music player work are instantiated by that class itself, in this case, a music player. To use a music player, it's pretty straightforward. You instantiate it and start using it. Now with dependency injection, these classes that the music player needs are not built by the player himself. They come as constructor arguments. And when using the music player itself, we have to build those dependencies, then build the music player and start using it. So it kind of shifts a little bit the logic of where things get created. Now we actually had a dependency injection talk in the Android Dev Summit of 2019. Here's a link to that video. And it goes into more detail on the theory of dependency injection and the benefits of it. But to basically recap, we highly recommend that you use dependency injection in your code base. It does have quite a few benefits, easier configuration changes. That music player had an SQLite database. Maybe I want a different type of database, or I want to add more codecs. It should be easier on testability. And things are loosely coupled, and it's easier to reuse code, and a few other things. We also mention on that talk that you should definitely use a dependency injection library, because doing mental injection can get pretty hairy. It could get a lot-- it could get a lot harder, because if you have a bunch of dependencies, then you have to create all of those in the right order, and so forth. The thing about dependency injection, or specifically, it's that it's hard in Android because you don't own the framework classes. Classes like activity service and broadcast receiver, you don't instantiate them. The OS will create those for you and then it will start calling methods into it. And that's where your application code takes over. But you don't own those constructors. So it's not like you can add parameters to it for your dependencies. There's no way of passing those. There are some factories that were added in API 28 to work around that. But that's not a realistic solution, at least not for now. One of the libraries we recommended on that summit talk was Dagger. And Dagger is great. It's a dependency injection library that offers compile time validation of the dependency graph. And it has amazing runtime performance. But we did notice that it's hard to use. Especially it's hard to configure, there's many ways to configure it. And we saw a handful of developers that had a hard time changing that configuration. Specifically if you still knew how to use-- if you were familiar with Dagger and you switched to another app that had Dagger, you would still have a hard time using Dagger, because the configuration might be different, or you wouldn't know exactly what are the subtle changes between the components and so forth. The thing is that we did a survey and a lot of developers still asked us for a solution. So what kind of solution did we have in mind? Well, we thought of some few goals. And one of them was that we definitely wanted a little bit something more opinionated, and especially so that we could make those choices for you. And because we make choices for you, things are easier. So something that's easy to set up. But more importantly, we want you to focus, within the realm of dependency injection, to focus back on those dependencies, the definition of them, and just using them, and not have to worry about the wiring of these dependencies. Those were kind of our goals. And this is where Hilt comes in. Hilt it's actually a dependency injection library that was being used internally in Google for a bit and we just rename it, change a few things, and open source it. But Hilt in essence offers a standard way to do dependency injection in Android. And what that really means is all of those different ways to configure dependency injection change to just being the same. So if you ever work on an app that has Hilt and then go to another app, start some other work on some other app that has Hilt, things will be familiar. So you get that transfer of knowledge, because things are standardized. Hilt is built on top of Dagger, so you get those same benefits that Dagger offer, compile time validation, amazing runtime performance. The solution that we had in mind is not only about library. We also wanted to do tooling support. So you can see this with Android Studio and the Dagger navigation. And because we know Android is an ecosystem, Hilt also offers extensions to work with other hard APIs that for DI, mainly, it has some AndroidX extensions for WorkManager and ViewModels. And we'll show some of that soon. All right. Let's quickly go through how to set it up and how things work. So going back to our original example, we had a music player. This one is simple. It doesn't have anything on its constructor, not yet at least. To say we want to make the music player injectable in two places, we annotate it with @Inject. This is not a Dagger or Hilt annotation. It's a Java extension annotation. We have to create an application annotated with Hilt Android app. This kicks off all of the injection going on in your application. And then for activities or fragment, we start annotating them with AndroidEntryPoint, and this makes Hilt inject these Android classes. And when Hilt injects them, it'll inject dependencies that you have to clear with that inject. So we have a few here of that music player. And then once onCreate happens, you are able to use that player pretty easily. That's it. That's all you need to do to start getting injection going on in your application. It's pretty easy. To go into a more real world case, that music player originally depended on a database. So if we add a database constructor parameter, we now have to define a way to provide this database. We're using Room, so we can't technically just annotate a constructor. Instead, we need another way to define dependencies. And for that, we have something we call modules. Modules are a Dagger concept. But because Hilt is built on top of Dagger, it comes along with that. And modules are basically a class that defines how dependencies can be provided. You create a class annotated with that module. That's the Dagger annotation installing-- it's a Hilt annotation, and it tells Hilt where this module will be available. Here, we use ApplicationComponent. So dependencies declaring this module will be available all around the application. The dependency definition themselves are methods when this case with provide, another Dagger annotation, the inputs of this method, the parameters, will be provided to you by Dagger. And the output is the return type in this case, a Room database that we created using the database builder from Room. That's it. With this, the music player now we'll be able to be created with our Room database. We saw InstallIn. And InstallIn took a component, we saw application component. But what exactly are components? Well, I like to also think about them as DI containers. And I'll show you a bit why. Components are what Dagger define as that glue that knows how to create your dependency. If we go back to our original example, that music player needed a database and some codecs. That instantiation logic and the ordering of that, that's basically what a component is. To put it differently, component is made up of factories that know how to build stuff. But they also are in a way a container, because they can hold onto instances of the things that factory creates. And it's able to reuse them for other factories or for other parts of your code. And we'll see that in a bit. Hilt comes with a few predefined components, ApplicationComponent being one of them. And these match the Android framework classes. There's an ActivityComponent and a FragmentComponent. And dependencies flow from one component to another. They're encapsulated similarly to how an activity is encapsulated in an application and so forth. And what this really means is that data module that we had with our database, if we had installed that in our ActivityComponent, activities and fragment could get injected with that dependency with the database, but not the application component. Hilt offers components for services, too. And we also have ViewComponent, one with fragment and without fragment. To try to put a different perspective into this, ApplicationComponent is managed by the app. So components define the when and where dependencies are going to be available. And they do that because they get managed. The components themselves get managed by the Android equivalent app managing ApplicationComponent, activities managing each their ActivityComponent. And because things are encapsulated in a hierarchy, dependencies in the ApplicationComponent are accessible to the ActivityComponent, because they have a greater lifetime. If we show a practical example, we inject the music database into the activities, into these two activities, the ActivityComponent basically says, hey, ApplicationComponent, I know you know how to give me a database, give it to me. And it will create one. But something interesting here is that we actually get two different music databases for this activity. And that's not quite right. You kind of want to share your database, because you want to share that connection, you want to share some synchronization. So there are dependencies that you do want to share and have the same instance. And for that, what we use is something called scoping and scope annotation. So if we go back to our data module, we had a method that provided the database. If we now annotate it with a scope annotation, this or this one, we use Singleton. Then what Hilt and Dagger does is they retain that music database across that component. And this is the container part. It can hold onto that dependency so that if other factories or other places that need to be injected, you'll use the same one. So now we use the same music database for both of our activities. All of the Hilt components come with scopes. And again, components define the when and where, the life cycle of the dependencies. The scope then defines the retention of those dependencies. They'll get retained for the lifetime of that component. Now, another concept that Hilt offers is something called EntryPoint. Entry points are a way of assessing those components and their dependencies. And these are really useful for parts of your application that are not supported by Hilt out of the box. One concrete example is a content provider, where if you try to Android Entry Point a content provider, it doesn't work. There's no content provider component that Hilt offers. They're a bit weird. They have a weird lifecycle. They can be created before the app. So that's why they're not supported. But basically, in the content provider is where do you want to use a database. So if the content provider had a way to reach out into the application component and get that database, how would it do it, right? And that's where entry points come in. They are a way of entering in to your dependency graph. If we have a content provider, we create an entry point by defining an interface. We annotate it with EntryPoint, install it in the component where it's going to be on and get retrieved. This interface has a getter method. And the return type is that dependency we want. Now on our actual query of our content provider, we use utility methods like EntryPointAssessors to get that entry point out of the application context. And then from there on, we can start calling our getters and get those dependencies that we need. Pretty neat. So entry points are useful for all of those cases where Hilt doesn't offer out-of-the-box injection. We mentioned Hilt playing nicely with the ecosystem is important. So Hilt come with AndroidX extensions for WorkManager and ViewModel. Work Manager and ViewModel-- workers and ViewModels are interesting. You kind of don't own their construction injection. They have some weird instantiation. And Hilt tries to help you there with these extensions. One example of that is ViewModel [AUDIO OUT] to create these. You would usually end up using a factory. But with Hilt, it's as simple as add injecting-- annotating the constructor with ViewModelInject. Then you pass your dependency as usual in the constructor parameter, savedStateHandle, [INAUDIBLE] assisted. This just means that that dependency don't have to be defined in your component. Hilt will know how to create it for you and give it to you. So it's assisted. And then on any AndroidEntryPoint that you have, you can use that ViewModel that you had annotated previously, either by using the [? coupling ?] extensions by ViewModels, or simply using the ViewModel provider. So those are the new Hilt annotations that we've learned so far. Now the other side of Hilt, or a really important part of Hilt is the testing side. Hilt offers quite a few APIs for their testing, which are pretty handy. If we wanted to write a test for a music player, we create our music player. But in this case, we don't want to create it with a real database. We want to create it with an in-memory database. And the reason we want to do that is because we want to isolate this test. We don't want to use a real database. That leaks between tests. We just want a database that we create here and get rid of pretty quickly. Creating this music player is pretty easy. We just create that database. But imagine if MusicPlayer had 20 other [INAUDIBLE]?? And those dependencies have more dependencies. So what ends up happening is that you will have to exercise your whole dependency graph to be able to create an instance of the thing you want to test. If you have fakes, you can use those. But then, when you don't have fakes, you tend to mock. And that by itself has some other disadvantages. But with Hilt, we do injection in the test itself. So what happens is the things that you want to test get constructed the same way they would for your production [? build, ?] which has the benefit of you don't have to manage creating all of those dependencies and [? transmitting ?] dependencies. So to do that, we start with HiltAndroidTest. We annotate our tests with an annotation. We create a rule. And the rule will be useful for injection later. Similarly to when we inject into our app, we create a field with an inject. On our setup, we use the rule to inject the test. And this is where we control the injection. And once injection happens, then you can start using that instance of the player and start writing tests and assertions. Now one thing about this test is it created that music player with the same configuration we had on our real app. So we will use a real database, and that's not quite what we want. We wanted an in-memory database. What Hilt offers here to solve that is Hilt has a way to install modules. And then I can define a new module that creates the in-memory database. So I uninstall the data module that was the one used the production that created the Room database. And then I defined a new inner class for this test that creates the in-memory one. The cool thing here is that this module and this uninstalling only happened for this test. So multiple tests can have different configurations. So it's pretty flexible. There is some setup required. You do need a runner that creates an application that's of type HiltTestApplication. And don't forget to use that same runner when you build the Gradle. To recap, we saw two annotations here for testing, but there's actually a few more for other use cases. And I won't go into details on them. They're a bit special. But we've got documentation on what kind of issues they solve. Something to keep in mind with testing is, because you want to replace dependencies, that means you should put those dependencies that you want to replace into their own modules. And since creating modules is kind of easy, I think this is something fairly trivial to do, in terms of there's no cost in creating multiple modules, even if they just have one dependency that you know you're going to replace. Meanwhile, in a bigger codebase, you should try to create smaller test apps, if you have a multi-Gradle project with many modules for a big app. It's convenient to create smaller test app to take coverage in certain areas, because then the scope of things you need to replace and redeclare are smaller. We know dependence injection can be core to your code base, and refactoring that can be tricky. So with that in mind, we do offer some migration guides. And mainly, if you were already using Dagger or Dagger Android, Hilt can actually work side by side, either temporarily-- hopefully temporarily until you fully migrate. Hilt offers some APIs that I won't mention here to help with this. We actually have a whole guide that will give you some pointers on how to do this. Please check it out. But we will do know changing the dependency injections, that really can take a toll. All right. So that's Hilt on the basics. Show you how to set it up, how to use it. But let's take a look at Hilt in itself and how it works to give you more sense on some of the decisions that it makes for you. Hilt is really composed of two areas. One we like to call the aggregation part, and the other is the Android Entry Points part. Let's take a look at the aggregation. Aggregation here means if you have multiple modules and entry points, they get aggregated. Let's take a small example here. Say we have an app. This is a Gradle module. And that's composed of a source set, and source set is classes. You have a class that's a module and has InstallIn. In a bigger app, you have other Gradle modules. And all of those might have modules that also get InstallIn. And there is dependency between these Gradle modules. Well, Hilt is able to navigate through these transitive dependencies, and more importantly, through the classpath and find those modules and install them on the right places. In other words, it will aggregate those modules and entry points across your classpath. And this has a few benefits. One of them is that defining a module, if you're working on a big app and you're working on a small library part of that app, you can create a module, install it on a standard component, and you know it'll be easily accessible. You don't have to go into the root of your app navigating exactly where that module should go. It also has practical effects with variants in flavor. Because they're aggregated across a classpath, and variants in flavor affect your classpath. You can have different configurations between your flavors and debug and prod variants. One concrete example of this is say you have an OkHttp client or a gRPC client. These have interceptors, usually for logging, authentication, and things like that. If you have a module that defines that client, and you have no interceptors on your main, that means when you build your production app, you won't have any interceptors. But then say on your debug variant you want to have login, then you can define the module only on the debug source set. And only when building the debug variant, that source set becomes part of the classpath. Hilt discovers that module, aggregates it. And now ultimately, you have logging only on your debug OkHttp client. That's pretty neat. Going back into the aggregation, you might be wondering-- if you're familiar with Dagger you might be wondering what exactly is going on. And what's happening is in those Gradle modules where you have your source set and your modules and entry points, you're installing into a component. And these class components that you're using installing, is nothing more than a key to tell Hilt, hey, you should create a Dagger component based on this key. The way you define these component keys is through a DefineComponent. Because that DefineComponent tells Hilt to generate a component, you can also annotate that key with a scope. And that scope will be added to that component. In essence, DefineComponent is what Hilt uses to create the other Android Standard Component, ApplicationComponent, ActivityComponent, and so forth. And what really happens is Hilt looks at all those installs [INAUDIBLE],, aggregates them, and generates a vanilla Dagger component definition. The modules will go into the app component module list. And entry points are just interfaces that get to be implemented by that Dagger component. Hilt offers this similarly with sub-components. The find component can actually [INAUDIBLE] parent attribute. So a FragmentComponent is based off ActivityComponent. And similarly, ServiceComponent is a sub-component of ApplicationComponent. And that's how you form that header key within your component. The standard components that Hilt offer are fundamental to Hilt. They really offer that simplicity of you know where the component is going to [INAUDIBLE] in its lifetime. You have that knowledge transfer from one app that uses Hilt to another that uses Hilt. It really is the building block for other extensions. The AndroidX extensions, all they do is create more entry points and modules. And they get installed [INAUDIBLE] components. And they're key to the simplicity of Hilt, because they get standardized. Now you could define a component hierarchy yourself. But then you lose on the managing of those components. And this is the second part about Hilt. Hilt not only offers that aggregation mechanism and has the standard components defined, it also just offers a way to use those components via the Android Entry Point. And what Android Entry Point, all it does is really generate a base class that knows how to member inject your concrete class. We changed your superclass via some [INAUDIBLE] transform. That's pretty neat. But ultimately, all it does is member into your concrete class. And the way does that, because Android Entry Point also generates a vanilla entry point that has the member injection method that goes in the Dagger component and that Dagger will generate an implementation for. After all, Android Entry Point is not that magical. And you could define your own component header key. But then you will have to manage those component yourself. And similarly to Android Entry Point, you will have to do the memory injection yourself. So with that, we learn a bit about Hilt, how to set it up, some internal semi-complicated stuff, I guess. But what's next? Well, Hilt is in alpha. And there's still more work to do, bugs to squash. There's also features we want to keep working on. We want to be able to allow you to use Hilt outside of Android Gradle modules. And we want to invest in other areas, and not just on Hilt, but in Dagger. Being Hilt built on top of Dagger, we want to invest on Dagger. And we want to provide assisted injection out of the box. And expanding outside of the Dagger and Hilt, but still within the realm of dependency injection, we do want to provide more AndroidX integration, possibly navigation. And we're thinking far ahead with things like [INAUDIBLE] and things like that. That's about it. Please try it out. Give us some feedback. Here are some links to more documentation. Dagger.devhilt goes in depth on a lot of the APIs. Dagger and Hilt are public open source on GitHub. You can look at the source code, file issues there, or questions. And we also have documentation on d.android.com. These are more story-driven and use case documentation that are great for more newer beginners to the subject. That's about it. Thank you so much. Bye. [MUSIC PLAYING]
Info
Channel: Android Developers
Views: 41,670
Rating: undefined out of 5
Keywords: Android
Id: B56oV3IHMxg
Channel Id: undefined
Length: 28min 57sec (1737 seconds)
Published: Mon Jul 20 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.