Dagger 2 ( Caution: PLEASE TRY AT HOME ) Part 3
WOW, we got one more day so it’s time to make this day awesome by learning something new.🙂
Hello friends, hope you are doing good. So today we are going to enjoy the next part of Dagger 2 and that is part 3.
Motivation:
Motivation is same which I already shared with you in part 1.
Revision:
In part 1 we discussed:
- What is a dependency?
- What is a dependent?
- What is a Dependency Injection ( DI )?
- Famous techniques/strategies for DI ( Constructor Injection, Method Injection, Field Injection )
- How to make a dependency graph from code.
In part 2 we discussed:
- Annotation Processing
- Magic words
- @Component, @Module, @Provide
- How to move dependencies into component
- Relationship between component and module
Dagger 2:
First thanks for appreciation to all of my blog readers. In the last post, we refactor our two dependencies LayoutManager and HomeAdapter in HomeActivity.class.
Next, I am going to refactor remaining dependencies. So in this way, our last concept will be revised plus I can show you next benefit of Dagger 2 in our app. This part has code on GitHub in branch Part3Refactoring.
Refactoring of GitHubRepository:
public class HomeActivity extends AppCompatActivity {
...
private GitHubRepository gitHubRepository;
...private HomeActivityComponent homeActivityComponent;@Override
protected void onCreate(Bundle savedInstanceState) {
homeActivityComponent = DaggerHomeActivityComponent.builder()
.homeActivityModule(new HomeActivityModule(this, App.getApp().getGlide()))
.build();gitHubRepository = App.getApp().getGitHubRepository();
...
}private void loadData() {
gitHubRepository.getUsers()
...
}
}
As you can see in above code. We are asking to App class for GitHubRepository. Basically, that is a hidden dependency as we already discussed, hidden dependency in our first post when we are using Glide.with(…) directly in our method and later we refactor that hidden dependency by using a method and constructor injection. Now here I am going to move this dependency first into our Component.
( Note: This is not a proper way but I am going to give you one concept first in a proper way. Then for the second concept, I will do refactor my this mistake. So try to focus only on one thing. )
@Component(modules = HomeActivityModule.class)
public interface HomeActivityComponent {
RecyclerView.LayoutManager getLayoutManager();
HomeAdapter getHomeAdapter();
GitHubRepository getGitHubRepository();
}
As we already know any dependency we need in our app code. We always ask for that dependency to our Component. That’s why I added one more method in our Component which will give me GitHubRepository.
For revision purpose, relationships between AppCode, Component, and Module.
AppCode -> Component
Component -> Module
Module always knows how to create or generate that object.
In above code one step is complete. AppCode -> Component. Now I need to go to the second step, write a code in the module for the generation of this dependency.
If I will try to run after the first step. Remember Android Studio will give me the error. In Dagger 2, in my opinion, the most important skill is how to read the error on a console. So before going to next step, I am sharing with you some tips how to read error in Dagger 2.
First Tip: Never panic if you try to run and you got a lot of errors. Becuase that code generation is compiled time. So at any time you have a bug in Dagger 2 code, code generation will stop and you will feel all my project has errors. Like If I am using 100 Components and I have one error in Dagger 2 due to any change, IDE will show me 100 files have the error. Again never panic.
Second Tip: As a developer, we mostly used to get our crashes or bugs on Android Monitor but this is not true in Dagger 2 case and especially when you are using with Kotlin. You will get nothing on Android Monitor. So don't be scared if you are not able to see any bug on Android Monitor.
Third Tip: Always go into you command line options and add --stacktrace --debug. (Preferences -> Compiler -> Command-line Options: –-stacktrace -–debug). This will help you a lot.
Now going back to above image. So as I compiled my code. I got an error. Which I can see in Message Tab as shown in above image, left top corner. Next, my IDE is saying I did not have a class DaggerHoeActivityComponent on right top corner of the image. In the end, I opened my Gradle Console Tab and I got my error there.
/Users/waleed/Documents/Workspace/Dagger2/app/src/main/java/uwanttolearn/dagger2/java/home/HomeActivityComponent.java:19: error: uwanttolearn.dagger2.java.repositories.github.GitHubRepository cannot be provided without an @Inject constructor or from an @Provides-annotated method.
e:
e: GitHubRepository getGitHubRepository();
e: ^
e: uwanttolearn.dagger2.java.repositories.github.GitHubRepository is provided at
e: uwanttolearn.dagger2.java.home.HomeActivityComponent.getGitHubRepository()
So now we can easily determine by reading this error. Dagger 2 is saying man, you have done first step AppCode->Component now please complete the second step Component->Module.
@Module
public class HomeActivityModule {
...@Provides
public GitHubRepository gitHubRepository() {
return App.getApp().getGitHubRepository(); // This is not a good way instead this is not a way. :P
}
}
First, how I am providing GitHubRepository that is not a good way to use Dagger 2. But currently, my focus is to move all dependencies from App Code to Component. So try to focus on that concept later I promised with you guys I will show you what is a proper way for above dependency.
Now when I compiled my code there are no more bugs because I am completing all the criteria, which are required by Dagger 2. AppCode -> Component -> Module. It's time to use this new dependency from Component in our AppCode.
public class HomeActivity extends AppCompatActivity {
...
private GitHubRepository gitHubRepository;
...
private HomeActivityComponent homeActivityComponent;@Override
protected void onCreate(Bundle savedInstanceState) {
...
// gitHubRepository = App.getApp().getGitHubRepository(); // Before refactoring as a Hidden Dependency
gitHubRepository = homeActivityComponent.getGitHubRepository(); // After refactoring
...
}
...
}
Boom. Now our all dependencies are moved into HomeActivityComponent except UI Views like Progressbar and RecyclerView. For UI Views some developers or teams used to get from Dagger 2 Component and some are not. This is a personal choice if you want you can move your UI Views into Dagger 2 Component but for this example, I am not moving UI Views into Component.
Guys, it's a time of a party because I am going to show you one very powerful concept of Dagger 2, by using that you can remove a lot of boiler plate code. Instead, when I grab this concept first time, I am amazed instead that blow my mind. And I am asking these questions to my self. What? How? Seriously? Impossible? That is not possible? :)
That I will achieve with one magic word and that is @Inject.
FOCUS, I NEED MORE FOCUS OF YOU GUYS. MAY BE THAT WILL GIVE YOU TOUGH TIME, SO FOCUS, FOCUS, AND FOCUS.
Currently, we have a component with three methods.
@Component(modules = HomeActivityModule.class)
public interface HomeActivityComponent {RecyclerView.LayoutManager getLayoutManager();HomeAdapter getHomeAdapter();GitHubRepository getGitHubRepository();}
If I have 10 or 20 dependencies, according to our current knowledge, I need to add all those dependencies inside my component. It's mean later If I need to add any new dependency I need to open my component and add that new dependency method. So we can save our that time by using the @Inject magic word. This is given by Java but I am not going into detail of this magic word. Instead, we will learn how to use that magic keyword into our code. Next, I am going to refactor my component.
@Component(modules = HomeActivityModule.class)
public interface HomeActivityComponent {void inject(HomeActivity homeActivity);// RecyclerView.LayoutManager getLayoutManager(); removed
// HomeAdapter getHomeAdapter(); removed
// GitHubRepository getGitHubRepository(); removed
}
In above code, I removed all the methods and I added only one new method with name inject. You can use any name. Important thing is that method has void return type and have one argument. Which is our HomeActivity. Every Component when have an inject method that always has one argument, which is always an activity, fragment, view or any class for which you are writing a Component. On this stage, if you run your code you will get again a lot of errors which are shown below.
I don't think I need to explain all above errors. That is really simple because I removed all these methods that is why IDE is giving me the errors. Now it's time to refactor our AppCode (HomeActivity.class).
public class HomeActivity extends AppCompatActivity {....@Inject
GitHubRepository gitHubRepository;
@Inject
HomeAdapter homeAdapter;
@Inject
RecyclerView.LayoutManager layoutManager;// private HomeActivityComponent homeActivityComponent; // Removed@Override
protected void onCreate(Bundle savedInstanceState) {
...
DaggerHomeActivityComponent.builder()
.homeActivityModule(new HomeActivityModule(this, App.getApp().getGlide()))
.build().inject(this); // Added
// homeActivityComponent = DaggerHomeActivityComponent.builder()
// .homeActivityModule(new HomeActivityModule(this, App.getApp().getGlide()))
// .build(); // Removed// gitHubRepository = App.getApp().getGitHubRepository();
// gitHubRepository = homeActivityComponent.getGitHubRepository(); // Removed
...
}
...
private void initRecyclerView() {
// layoutManager = new LinearLayoutManager(this);
// layoutManager = homeActivityComponent.getLayoutManager(); // Removed// homeAdapter = new HomeAdapter(new ArrayList<>(), glide);
// homeAdapter = homeActivityComponent.getHomeAdapter(); // Removed
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(homeAdapter);
}
....
}
Afer doing an above refactoring. I run my code and app start working. May be you can ask a lot of questions. How layoutManger is working because we never initialize this field in our code and this time there is no homeActivityComponent.getLayoutManager() but that is working only after adding @Inject. If you guys are confused currently try to do that refactoring on your own by taking check out of my code. Currently, if you want to compare how many lines of my code I removed you can check out in this commit f4998dcdb63e6e9f996a607f3e797f7d97e04262 with comment lines and you can check without comment lines on this commit e7a35dbada5ee00e6933241eb98e7251442d2f9f. I really want to explain you the magic of this @Inject but before that, I want to show you one more magic of this keyword. For example, my project manager gave me a new requirement, so I need to add one more dependency into my this code. I am going to create a one simple class with name FormatString.
public class FormatString {
}
As you saw that is an empty class. Now I need to add this dependency into our HomeActivity. As we know the simple formula AppCode->Compoent->Module.
In AppCode:
In Component:
Oops we already removed all our methods from component and now I have only one method which is void inject(...) in my component. So the question is, am I doing something wrong? Basically, I can choose my old strategy but now I want to write less code and want to give an order to annotation processor of Dagger 2, write code for me. So due to that I will not add any new method in component instead I will add magic keyword @Inject on my field as shown below.
Yes, we achieved the first step. AppCode ->Component. You can trust on me this will work. Now next I need to do second step Component -> Module.
@Module
public class HomeActivityModule {
....
@Provides
public FormatString formatString(){
return new FormatString();
}
}
The second step is done. That is awesome. I never open the Component file to tell him I have one more dependency. In this way, I can add thousands of dependencies without adding any new method. Next benefit is if there is any change in Construction of any dependency, that never impacts on my HomeActivty class. For example due to any reason I need to add one string argument into our FormateString constructor.
public class FormatString {
public FormatString(String s) {
}
}
Now you can imagine I only need to change my module code because that is the only code who is responsible for the creation of objects. Due to that my AppCode never breaks. WOW.
@Module
public class HomeActivityModule {
....
@Provides
public FormatString formatString(){
return new FormatString("Hello Guys");
}
}
Now it's time to reveal how @Inject magic keyword works. Truly saying that is not important in the learning curve of Dagger 2. If you only know what we discuss that is enough but a great developer always know what he is writing and what annotation processor or auto genration libs are writing for him. :)
I also know if you are the one who is new and fighting with Dagger 2 learning curve. For you guys, I have one advice, try to play with @Inject a lot without taking tension how that is working behind the scene.
I am going to start from Component. As we know I added one new method in our component.
@Component(modules = HomeActivityModule.class)
public interface HomeActivityComponent {
void inject(HomeActivity homeActivity);
}
In simple words, you can say. void is giving one indicator to Dagger 2 about field injection or you can say I want to use field injection so please take care of all my dependencies on your own and then Dagger 2 asked us, ok I will but please confirm me. In which class you want I will take care of dependncies. For that, we gave our HomeActivity class context as an argument. This is really simple. Now what we did next. Any body knows? Yes, you are right, all those fields or objects which are dependent on Component we are appending keyword @Inject and removed private specifier as shown below.
public class HomeActivity extends AppCompatActivity {
...
@Inject
GitHubRepository gitHubRepository;
@Inject
HomeAdapter homeAdapter;
@Inject
RecyclerView.LayoutManager layoutManager;
@Inject
FormatString formatString;
...
}
After that, we initialize our Dagger 2 Component in our HomeActivity class on Create method as shown below.
public class HomeActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);DaggerHomeActivityComponent.builder()
.homeActivityModule(new HomeActivityModule(this, App.getApp().getGlide()))
.build().inject(this);
...
}
...
}
So here we are giving our HomeActivty class context to Dagger 2 by giving inject(this);.
Now I have a feeling you can see behind the scene how Dagger 2 is working. As we compile, Dagger 2 will take this context of HomeActivity and figure it out how many fields have the @Inject keyword. As he got all those fields he will go to the module and ask for the creation of all these objects and we already gave all creation code in the module. May be you are confused like how Dagger 2 know which field required which object reference. Like how that will inject LayoutManager against LayoutManager. For that, I will explain you more but before that. I am expecting there is no more magic between @Inject keyword in your minds. Every thing is crystal clear. We are giving proper information to Dagger 2. [Again].
Dagger 2 you will get @Inject fields in a class, of which context I am giving you in void inject(this) method.
Next how Dagger 2 decide the relationship between object type. For that again we are giving information into the module. Dagger 2 first determines all fields in a class with the @Inject keyword. After that Dagger 2 check their data type like in my class I have one field with LayoutManager. So Dagger 2 will ask for the module, give me LayoutManager object and Module know how to create that object because we are mentioning return type in the module with the @Provide keyword as shown below.
@Module
public class HomeActivityModule {private final Context context;
private final Glide glide;public HomeActivityModule(Context context, Glide glide) {
this.context = context;
this.glide = glide;
}@Provides
public RecyclerView.LayoutManager layoutManager() {
return new LinearLayoutManager(context);
}@Provides
public HomeAdapter homeAdapter() {
return new HomeAdapter(new ArrayList<>(), glide);
}@Provides
public GitHubRepository gitHubRepository() {
return App.getApp().getGitHubRepository();
}@Provides
public FormatString formatString(){
return new FormatString("Hello Guys");
}
}
So this is the simple Dagger 2 strategy. We can use the same strategy without Dagger 2 but then we need to write a lot of boilerplate code. There are many other benefits which I will show in next posts. I think for today that is enough. Only before the finish, I am going to show you Dagger 2 code which that generated for us.
This is simple and if you are the one who is not feeling comfortable with this code. Don't take tension. That is use less. Who cares. So forgot this last code image :). In the end, I am showing you simple graph how Dagger 2 work in our code.
Dagger 2 ( Caution: PLEASE TRY AT HOME ) Part 4
Conclusion:
As you remember we already discuss there are three types of Injections. Construction Injection, Method Injection and Field Injection. Constructor and method injection we already saw in part 1. Today we saw the Field Injection. I have a feeling after completion of this part you can use easily Dagger 2 in your project. I know there are some things which I used in a wrong way when I am discussing with you about the different concepts of Dagger 2 but the great news is these bad practices when I will do refactor in next post for new concepts they will not impact on our AppCode. Abstraction between AppCode and the creation code of the object is the one biggest benefit of Dagger 2. Hope you guys enjoy this post. Have a nice weekend. BYE :)