Using Hilt Custom Component
[ English | 한국어 ]
I am not going to explain all about basic usage of Dagger and Hilt here but I will offer the links related to them.
If you already know what DI is and just want to read how to use Custom Component, go to section “Custom Component”.
DI (Dependency Injection)
What is dependency injection and why we use this? Let’s assume we are designing object oriented program. We could single massive class to make the program but no-one build their program in this way. This pattern makes you and your colleagues depressed and mad (Of course, you can be irreplaceable employee as you become the only developer who can handle the program. #How_To_Write_Unmaintainable_Code 😁). You and I, as a smart developer, architect the program and design the relationship among classes in the program as following object oriented principal so that our colleagues including us can enjoy happy hour in the afternoon and cozy holidays (There is interesting medium article which explains SOLID principal through images).
There is an opinion that the OOP is not applicable for programs because programs don’t have to be like real life which is complex and unpredictable. However that is different topic with this posting; moreover, this opinion doesn’t also mean making a program using only one big class is desirable. 😅
When you design your application with various classes, those classes could have relation each other and we call it DEPENDENCY. Let’s assume you design a class which mimic water purifier offering hot and cold water. This class needs various parts such as cooler for cooling water, heater for boiling water and many kinds of filters for filtering dirty things from water. We can call these parts as dependency of the water purifier.
As you can see, you can make the water purifier flexible and testable as you declare each part of it independently and use interface instead of concrete class. For instance, you can declare your WaterPurifier class like below so that you can use various types of heater, cooler and filter to make water purifier.
In the above code snippet, All of the constructor parameters such as WaterTank, Heater and Cooler are interface declaring its functions and you can use any implementations which follow the protocol (interface). Of course, If you pass fancy implementation to the constructor, the water purifier would be performant but could be pricy 😅. And this strategy, that loosen the coupling among the dependencies, makes your code testable which means easy to test. You can use Mock objects as the parameters of the constructor so that you can test the condition of the water purifier in various circumstances.
You can create and inject each dependent object by yourself when you create the water purifier object. This is called Manual Injection. As you can see, there is no need to use specific tools to use dependency injection and you can just do it yourself. However you can manage the dependencies easier and more stable as using the DI tools. DI tools mainly provide dependency injection and management functionality. When you make your application using dependency injection, some of the dependencies share its scope with the application but others could have its own scope and dealing with this scope management is tedious and error-prone. You can set the scope of the dependency as using the method provided from the DI tool.
I’m going to introduce you the “Custom Component” in Hilt which is famous for dependency injection on Android. Hilt is supporting tool for Dagger which is one of the powerful DI tools but has high learning curve. This posting assumes you already have enough knowledge about Dagger and Hilt. For more details about Dagger and Hilt, See the Official Web Site and Android Developer Guide.
Dagger has the concept of component which is a tree of dependencies sharing the same scope. All the objects in a component has the same lifecycle. In addition, components can make parent-child relationship that can form tree hierarchy.
In the above picture, there is dependency tree in the parent component and the child component. Both components are composed of many objects that has relationship each other. The most important thing here is that the objects in the child component can access the objects in the parent component but not in the opposite way.
If you want to declare some customs scopes that are bound to android UI component such as Activity and Fragment and use them to manage the lifecycle of dependent objects, you need to write lots of boilerplate code to do that. Android Framework team had also known about this issue and released the new tool called Hilt which has lots of built-in dagger component for Android and useful tools. You can make your app memory efficient and performant as making the full use of predefined components by Hilt (Actually, I was surprised with the naming Hilt because the Dagger is a kind of knife and Hilt is a cover for a sword or dagger 😍).
There is useful picture describing built-in hilt component in the android official guide.
If you are not familiar with the android component, you can consider SingletonComponent as the scope of application, ActivityComponent as the page scope on UI and FragmentComponent as the smaller sub page scope like content page of tab UI.
For instance, the objects installed in the singleton component as the component scope keep the same references until the application finishes and the objects installed in the activity component as the component scope keep the same references until the activity UI finishes (component scope means singleton in the component).
Meanwhile, if there is a circumstance that you need special type of scope which is not provided from Hilt, what can you do? Fortunately, Hilt provides the way you can define the custom component like below.
However there are two limitations when you define custom component.
- Must inherits from SingletonComponent directly or indirectly.
- Cannot define it in the middle of built-in components.
Then, when will you need this custom component? One of the most useful examples is when you need a component bound to a user logged in. Let’s take a look at below component tree.
As you already know, Singleton, Activity and Fragment components are provided as the built-in component from Hilt. Singleton Component, Activity Component and Fragment Component mean the dependency tree scoped from the creation to the destroy lifecycle of Application, Activity UI, and FragmentUI respectively. The UserComponent on the left side is custom component and it is scoped to the user’s login session in this case. When user A is logged in, this dependency tree injects objects for the user A. When user is changed to B, the dependency tree will be rebuilt and injects objects for user B. Of course, if there aren’t any valid login, it injects invalid objects or just throw error.
There is one thing I want to tell you at this point. When beginners learn dagger and hilt for the first time, they used to misunderstand the scope of component and module. If a module is installed to a singleton component, it doesn’t mean all instances provided from the module is singleton. For example, when a module is installed to a singleton component like below,
MyRepository1 is provided as the singleton object because it is annotated as @Singleton. In the contrary,
MyRepository2 is provided as new object everytime it gets called because it has not been bound to any scope — It has not any scope annotations.
How about the dependency tree is ActivityComponent?
MyRepository1 is annotated to @Singleton and it means that it is singleton only in the same activity. When there are two different activities or there is one activity recreated, they will get new objects as their dependent objects. Of course,
MyRepository2 is always provided as the new one.
Now, let’s make a real world example that uses custom component in order to manage dependencies scoped to the logged in user. The sample application is simple ToDo application and it has very few functionalities such as query, append and delete ToDo data for the logged in user. Below pictures are from the sample application.
A : Screen when there is no logged in user.
B : Screen when user
test1 is logged in.
C : Screen when user
test1 is creating new ToDo.
D : Screen when user
test2 queries ToDos.
You can find whole project on the below link.
I’m going to use Dagger and Hilt to make this sample application and define AuthUserComponent using custom component for the ToDo data layer because the layer should be able to manage ToDo data per user. You can see the relationship among all components involved below.
As I mentioned earlier, Singleton, Activity and Fragment components are provided by Hilt out-of-the-box. UserAuth class is defined as a sealed class in the sample application to deal with the status of user login like below.
In addition, I also define UserAuthSession class and install it to the SingletonComponent. When
login() function is called, it tries to login using user id and password provided and change current state to Authenticated if login succeed. When
logout() function is called, it tries to logout and change current state to Unauthenticated. The flow collectors that are subscribing the state
currentAuth receive the most recent state from it.
You can get the brief overview from the below picture. Login and logout events occurred in the UserAuthSession trigger rebuilding AuthUserComponent(Custom Component) so that all the relevant objects can be recreated.
Let’s start from the center of the picture.
There is a view displaying user information on the top and this view observes user changing event from the UserAuthSession and redraw user information when current is changed.
There is a MainFragment Right below the user information view and it shows ToDo list of current user. This fragment get user’s ToDo data from the ViewModel which uses ToDoRepository to get the data. ToDoRepository get the data from various of data sources. All the objects in the area outlined red including MainFragment will be recreated when current user is changed (I’ll explain about AuthUserComponentManager later soon below).
I will bind ToDoRepository and all the related data sources to logged in user scope and inject it to view model. In other word, When a user is logged in after launching the application, the user keeps using the same ToDoRepository and data sources until the user logouts or changes to another user.
To do that, Let’s make the custom component which will provide ToDoRepository and data sources. I’ll declare the custom scope which is the scope of the custom component first (This is not mandatory).
After that, I’ll declare the custom component which use the scope like below (Declaring the component and the builder separately is recommended).
Now, Let’s make the ToDoModule which provides repository and data sources so that the custom component uses them (There is only one data source declared in the sample application for brevity).
ToDoModule is installed (@InstallIn) to the AuthUserComponent which is defined previously. This module declares the repository and the data source as the form of binding and the repository has @AuthUserScope additionally. Then, what is the difference between the two? The repository that has @AuthUserScope annotation keeps the same object until the dependency tree of AuthUserComponent but the data source will be regenerated every time it gets called. This implementation is sufficient because the view model in this example only access the repository to manipulate the data and the repository is the only accessor to the data sources. However if the data source is injected to many other places and you want to keep it singleton per user, you have to add @AuthUserScope to the data source binder (bindToDoInMemoryDataSource).
Next thing I am going to do is injecting the objects from the AuthUserComponent to UI. You can inject all the objects from the standard component of Hilt without any additional code but you have to use
EntryPoint to access AuthUserComponent (the custom component you defined earlier).
ToDoEntryPoint provides ToDoRepository only and you can inject the repository instance to UI model like below. The first parameter of
EntryPoints.get() function can be a hilt component or the object annotated with @AndroidEntryPoint and I pass in authUserComponentManager as the first parameter.
You can get the entry point type-sefe using EntryPointAccessor.
AuthUserComponent is implemented like below. EntryPoint can access AuthUserComponent thanks to the AuthUserComponent implementing GeneratedComponentManager<AuthUserComponent>. The only function the interface has is
fun generatedComponent() and you can see the implementation at line number 31. As you can see, it just returns current authUserComponent in AuthUserComponentManager.
AuthUserComponent provides one more functionality and that is observing userAuthSession and rebuilding AuthUserComponent when there is login related event such as login, logout and user changed (line number 14 ~ 24). Additionally, you can use rebuildComponent() function to recreate AuthUserComponent.
Um… But that looks cumbersome to access the objects provided by AuthUserComponent. You can make the bridge from SingletonComponent to AuthUserComponent like below so that you can inject the object provided from AuthUserComponent to anywhere in your application.
I usually design UseCase class access repositories and only UserCase can access the entry point to AuthUserComponent. All of the subclasses of base UseCase class can use @Inject annotation freely in order to get objects from the custom component.
You can use custom component to define a scope of dependent objects that is not provided from Hilt so that you can deal with the lifecycle of dependency. However custom component can increase the complexity of dependency object graph and decrease its performance. Moreover it makes the scope hard to be used in the other library because it is non-standard component.
Everything has pros and cons.
If you use the custom component carefully, it will be the string weapon for you. You can decrease unnecessary memory consumption.
The end. 😁