Easy to use dependency injection for your Android app

Dependency Injection (DI) is not just a good practice. It solves real problems. The more complex your app gets the more your Activities, Fragments, ViewModels and all kinds of services depend on other components to get their job done.

Initializing each dependency is tedious and can be forgotten (yay for NullPointerException). When it comes time for testing you realize that your carefully crafted classes are actually very hard to test properly and independently because of everything they rely on. Can it get worse?

Wouldn’t it be nice if your classes just list what they need and then it auto-magically appears?

Wouldn’t it be even better when during testing you can replace all dependencies with something simple?

Dependency Injection comes to the rescue. I will show you how simple it is, and in my next articles how good it really gets.

We are going to use Hilt, a very nice library (based on another cool library - Dagger) which makes working with DI on Android a breeze.

The Setup

Hilt is a library so you have to add it to your Android project. First you need to update your build.gradle.kts on Project level (kts is when you use the kotlin version of the gradle files) by adding this single line

plugins {
    ...
    id("com.google.dagger.hilt.android") version "2.48" apply false
}

Then in your build.gradle.kts on App level you have to add the following few lines

plugins {
    ...
    id("com.google.dagger.hilt.android")
}

...

dependencies {
    ...

    implementation("com.google.dagger:hilt-android:2.48")
    annotationProcessor("com.google.dagger:hilt-compiler:2.48")

    androidTestImplementation("com.google.dagger:hilt-android-testing:2.48")
    androidTestAnnotationProcessor("com.google.dagger:hilt-compiler:2.48")
}

Sync those changes. A few files will be downloaded and you are ready to go.

But the setup is not done yet.

Everything in Hilt is done with annotations. You are about to see the first one.

@HiltAndroidApp
public class MyApplication extends Application {
}

In case you don’t already have one, you need a custom application class. It doesn’t need anything complicated just the @HiltAndroidApp at the top. That’s it.

Now let’s inform your AndroidManifest.xml about the new application class.

<application
    android:name=".MyApplication"

Now you are ready for the real deal. Wasn’t so complicated so far, was it?

Activities & Dependencies

Let’s create a very simple service.

public class SimpleService {

    @Inject
    public SimpleService() {
    }

    public String getText() {
        return "Simple Service";
    }
}

Notice the @Inject annotation above. Every class that you want to inject somewhere needs one constructor annotated with @Inject. This is how you tell Hilt what to use to create the class.

How do we use that in an Activity?

@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
    @Inject
    public SimpleService simpleService;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        Log.d("MainActivity", simpleService.getText());
    }
}

An activity which wants to use Dependency Injection itself or if its fragments want it, needs to be annotated with @AndroidEntryPoint

The @Inject annotation above the simpleService property informs Hilt that you want that service to be injected into the activity. This is called property injection and it has one requirement. The property must be public.

We used only two annotations and the injection now just works. Magic!

Run the code above and you will see in your logs Simple Service.

The first time I did this I couldn’t believe how simple it is.

Fragments & series of DI

Dependency Injection works very well with fragments and navigation graphs. But first let us build something a little bit more interesting.

We are going to add one more service.

public class AdvancedService {

    @Inject
    public SimpleService simpleService;

    @Inject
    public AdvancedService() {
    }

    public String getText() {
        return simpleService.getText() + " - Advanced Service";
    }
}

It is very much like our first service, except that it actually depends on our first service. It used DI for the SimpleService. This is getting interesting.

Now let’s add a fragment to our graph and a view model for it.

First, we will look into the view model code.

@HiltViewModel
public class InjectedViewModel extends ViewModel {

    private final AdvancedService advancedService;

    @Inject
    public InjectedViewModel(AdvancedService advancedService) {
        super();
        this.advancedService = advancedService;
    }

    public String getText() {
        return advancedService.getText() + " - InjectedViewModel";
    }
}

There are a few things to unpack here. First the whole InjectedViewModel is annotated with @HiltViewModel. This way Hilt knows that it could use this for DI.

Second, we annotate the constructor with @Inject, so that the DI knows which constructor is the right one.

This time, we don’t use property injection like the classes above. This time we use constructor injection. All you have to do is add everything you want injected as parameters of the constructor. Done.

How do we use all of that?

@AndroidEntryPoint
public class InjectedFragment extends Fragment {

    private InjectedViewModel viewModel;

    ...

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(this).get(InjectedViewModel.class);

        binding.text.setText(viewModel.getText());
    }
}

Like an Activity your Fragment is also annotated with @AndroidEntryPoint. However, this time we don’t use property or constructor injection. It just doesn’t work that way for ViewModels. You have to use as usual a ViewModelProvider but then the DI works its magic and provides all the dependencies requested by the view model.

You can run this code and you will get a nice and satisfying Simple Service - Advanced Service - InjectedViewModel showing you that on each step the correct class was injected at the right place and at the right time.

Just a few annotations and it all works.

Next

Today I showed you just the most common things about dependency injection in Android. Next time we will get a little bit deeper with scoping, non-constructor injection, interfaces, application context and more.

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