In our recent article on Dependency Injection (DI), I showed you how easy is to use DI in Android with Hilt. You just use a few annotations here and there and everything just works.
However, sometimes this is not enough and it might not cover some of our use cases.
Scoping
By default every injection is unscoped. What?! It means that each time you receive a brand new instance of the class that you have requested. What’s more, if two objects request the same class as their dependency they will receive different instances.
Sometimes, this is not desirable. For example, you might want to reuse the same database class across your entire application. Or you might want to reuse a cache class across objects which are used in the same activity.
Let’s see what scoping can do
public class NumberServiceUnscoped {
private final int number;
@Inject
public NumberServiceUnscoped() {
number = new Random().nextInt();
}
public String getText() {
return "The unscoped number is " + number;
}
}
@FragmentScoped
public class NumberServiceScoped {
private final int number;
@Inject
public NumberServiceScoped() {
number = new Random().nextInt();
}
public String getText() {
return "The scoped number is " + number;
}
}
@AndroidEntryPoint
public class InjectedFragment extends Fragment {
@Inject
public NumberServiceScoped scopedFirst;
@Inject
public NumberServiceScoped scopedSecond;
@Inject
public NumberServiceUnscoped unscopedFirst;
@Inject
public NumberServiceUnscoped unscopedSecond;
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.text.setText(
unscopedFirst.getText() + "\n" +
unscopedSecond.getText() + "\n" +
scopedFirst.getText() + "\n" +
scopedSecond.getText()
);
}
}
When you run this code you will get something like
The unscoped number is 31
The unscoped number is 22
The scoped number is 42
The scoped number is 42
There is only one new thing here the @FragmentScoped
in front of the NumberServiceScoped
.
It means that instances of this class under the same fragment will be the same.
The result confirms it. The unscoped instances return different random numbers, the scope instances return the same because it is the same instance. We could have also just checked their references but using random numbers is more fun.
Above we used @FragmentScoped
but there more options
Component | Scope | Annotation |
SingletonComponent | Application | @Singleton |
ViewModelComponent | ViewModel | @ViewModelScoped |
ActivityComponent | Activity | @ActivityScoped |
FragmentComponent | Fragment | @FragmentScoped |
ViewComponent | View | @ViewScoped |
ServiceComponent | Service | @ServiceScoped |
After seeing this, it is easy to fall under the impression that you might need to use scoping everywhere. Don’t. The Hilt developers suggest to use it sparingly. Otherwise it might generate too much additional code and even decrease performance. Stay with the default unscoped instances as long as you can.
Non-constructor injection
In all the examples so far the injected services or components were constructed with their constructor. You even marked it with @Inject
to be clear.
However, sometimes this is not possible.
Thankfully Hilt has a solution for us.
public class RandomService {
private Random random;
private int max;
public RandomService(long seed, int max) {
random = new Random(seed);
this.max = max;
}
public int nextInt() {
return random.nextInt(max);
}
}
This is the service we would like to inject. No simple constructor. No annotations.
Will this work?
For this particular case we are going to create a module class.
@Module
@InstallIn(SingletonComponent.class)
public class NumberModule {
@Provides
public RandomService providesRandomService() {
return new RandomService(System.currentTimeMillis(), 1000);
}
}
The module class has the @Module
annotation. Then it needs one function to create the service for us.
This function requires the @Provides
annotation. The return type of that function is important.
When you request an instance of the RandomService
Hilt looks for annotated functions in modules with the this particular return type.
There is one more thing here @InstallIn(SingletonComponent.class)
. This is required by Hilt. It requires that the module be scoped, so
that it knows where to create the module.
The binding itself of the RandomService
is unscoped. You can scope it if you need to.
@AndroidEntryPoint
public class InjectedFragment extends Fragment {
...
@Inject
public RandomService randomService;
}
Injecting is simple, just like any other services.
Interfaces
One particular case when it is not really possible to use a constructor injection is when using interfaces. How do you know which class to use?
public interface RandomServiceInterface {
int nextInt();
}
public class RandomService implements RandomServiceInterface {
...
}
The service is the same as above, but this time it implements a simple interface. Just as in the previous case, a module is required, but it has to be a little bit different.
@Module
@InstallIn(SingletonComponent.class)
abstract class AbstractNumberModule {
@Binds
abstract public RandomServiceInterface bindsRandomService(RandomService randomService);
}
The module is an abstract class and the method is an abstract method which returns the interface.
The method needs the @Binds
annotation. Just as above it uses those two things to determine that this is the method to be used.
This time it uses also the input parameter (RandomService randomService)
to determine which implementation it should provide when
the interface is requested.
@AndroidEntryPoint
public class InjectedFragment extends Fragment {
...
@Inject
public RandomServiceInterface randomServiceFromInterface;
}
Injecting the interface is just as simple as always.
Predefined qualifiers and generated bindings
Hilt has some helper stuff ready for us to use almost immediately.
For example, if you need a context in your service
public class SomeService {
private final Context context;
@Inject
public SomeService(@ApplicationContext Context context) {
this.context = context;
}
}
Annotating with @ApplicationContext
is all you need. Just as easy you can use @ActivityContext
.
Wait, there is more.
public class SomeService {
private final Application application;
@Inject
public SomeService(Application application) {
this.application = application;
}
}
There are no annotation, except for @Inject
, and you get the application injected.
Depending on the scope of your classes you might be able to inject a FragmentActivity
, Fragment
, View
or Service
.
Next
As we just saw, DI is not just for simple cases but can do a lot more. Now that you know how to use DI, what can you do with it when testing? We are going to explore it in another article.
You can find all the code on GitHub
P.S. Google have created a small but extremely useful cheatsheet with Hilt annotations.