Advanced dependency injection for your Android app

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

ComponentScopeAnnotation
SingletonComponentApplication@Singleton
ViewModelComponentViewModel@ViewModelScoped
ActivityComponentActivity@ActivityScoped
FragmentComponentFragment@FragmentScoped
ViewComponentView@ViewScoped
ServiceComponentService@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.

Did you like this article?

Please share it

We are Stefan Fidanov & Vasil Lyutskanov. We share actionable advice about software development, freelancing and anything else that might be helpful.

It is everything that we have learned from years of experience working with customers from all over the world on projects of all sizes.

Let's work together
© 2024 Terlici Ltd · Terms · Privacy