Like attention, no more lost, your support means a lot to me!

đŸ”„ Hi, I’m Chouchou. GitHub · Android-Notebook has been included in this article. Welcome to grow up with Chouchou Peng. (Contact information at GitHub)


preface

  • Dependency injection is a very important means of decoupling project components. Dagger2 and Hilt are the most important dependency injection frameworks in Android.
  • In this article, I will summarize how to use Dagger2, please be sure to like and follow if you can help, it really means a lot to me.

directory


Front knowledge

The content of this article will involve the following pre/related knowledge, dear I have prepared for you, please enjoy ~

  • APT 【 Like 】
  • Java | annotations (including Kotlin)

1. Why dependency injection

Dependency Injection (DI) is not a mysterious concept, and it is often used by accident. Dependency injection applies the principle of inversion of control (IoC), which simply means constructing dependencies outside of a class, using constructors or setters for injection.

Tip: You often inadvertently use the idea of dependency injection.

What are the benefits of using dependency injection?

  • Reuse components: because we construct dependencies outside the class;
  • Component decoupling: When we need to change the implementation of a component, we don’t need to make a lot of changes in the project;
  • Easy to test: We can inject mock implementations of dependencies to the dependencies, which makes testing easier for the dependencies;
  • Lifecycle transparency: Dependencies are not aware of the dependency creation/destruction lifecycle, which can be managed by the dependency injection framework.

2. Android dependency injection framework

Manual dependency injection is simple when there is only one dependency, but becomes increasingly complex as the project grows in size. Dependency injection frameworks can simplify the dependency injection process, and often provide the ability to manage the lifecycle of dependencies. Implementationally, dependency injection frameworks fall into two categories:

  • 1. Reflection based dynamic scheme: Guice, Dagger;
  • Static solutions based on compile-time annotations (higher performance) : Dagger2, Hilt, ButterKnife.

Tip: Dependency injection frameworks do not provide dependency injection capabilities per se, but use annotations to make dependency injection easier.

Within this, Dagger2 and Hilt are the subject of our discussion today.

  • Dagger2: The Dagger name is derived from the Directed Acyclic Graph (DAG), originally developed by Square, while the Dagger2 and Hilt frameworks were developed and maintained by Square and Google.

  • Hilt: Hilt is a secondary wrapper of Dagger2. Hilt is essentially a scenario for the Dagger. It lays down a set of rules for the Android platform that greatly simplifies the use of Dagger2. In Dagger2, you need to manually get the dependency graph and perform the injection, whereas in Hilt, the injection is done automatically because Hilt automatically finds the best places to inject Android components.

Next, let’s discuss Dagger2 and Hilt frameworks respectively. I wasn’t going to cover much about Dagger2 (because we use Hilt directly on Android), but I thought it was worth explaining Dagger2 to really understand what Hilt does for us.


3. Dagger2 usage tutorial

Tip: I also read a lot of articles and official documentation while studying Dagger2. Some authors enumerate the usage of all annotations, while others introduce the usage without explaining the automatically generated code. I was also looking for an easy to understand/accept way of saying it, and I decided that “basic notes” followed by “complex notes”, explaining the usage while explaining the auto-generated code, might be easier to understand. Looking forward to your feedback

In the course of the discussion, let’s start with a simple example: Suppose we have a user data module that depends on two dependencies:

public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; }}Copy the code

First, you can choose not to use dependency injection, and you may end up repeating builds at multiple points in your project, a disadvantage we discussed in the first section.

new UserRepository(new UserLocalDataSource(), new UserRemoveDataSource());
Copy the code

Later, after you’ve started using DEPENDENCY injection, you write a global utility method:

public static UserRepository get() {
    return new UserRepository(new UserLocalDataSource(), new UserRemoveDataSource());
}
Copy the code

This does meet the requirements, however in real projects the dependencies between modules are often much more complex than this example. At this point, if you often manually write dependency injection template code, not only time-consuming and costly, but also prone to error. Next, we’ll start using the Dagger2 helper to write the template code for us.

3.1 @ Component + @ Inject

@Component and @Inject are the two most basic annotations in Dagger2, and these two annotations alone can be used to implement the simplest dependency injection.

  • @Component: Create a Dagger container as an entry point to get the dependency
@Component
public interface ApplicationComponent {
    UserRepository userRepository();
}
Copy the code
  • Inject: Indicate how Dagger instantiates an object
public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } } -------------------------------------------- public class UserLocalDataSource { @Inject public UserLocalDataSource()  { } } -------------------------------------------- public class UserRemoveDataSource { @Inject public UserRemoveDataSource() { } }Copy the code

You need to annotate the dependency constructor with @Inject, and its dependencies, UserLocalDataSource and UserRemoteDataSource, also need to annotate @Inject.

The above code is automatically generated after the build:

DaggerApplicationComponent.java

1, implement ApplicationComponent interface public final class DaggerApplicationComponent implements ApplicationComponent {private DaggerApplicationComponent () {} 2, create dependencies instance @ Override public UserRepository UserRepository () {return new UserRepository(new UserLocalDataSource(), new UserRemoteDataSource()); } 3, Builder mode public static Builder Builder() {return new Builder(); } public static ApplicationComponent create() { return new Builder().build(); } public static final class Builder { private Builder() { } public ApplicationComponent build() { return new DaggerApplicationComponent(); }}}Copy the code

As you can see, the simplest dependency injection template code is already generated automatically. To get the UserReopsitory instance, use the ApplicationComponent entry:

ApplicationComponent component = DaggerApplicationComponent.create();

UserRepository userRepository = component.userRepository();
Copy the code

3.2 @Inject field Injection

Some classes are not initialized with constructors. For example, the Android framework classes Activity and Fragment are instantiated by the system. Instead of using the constructor injection used in Section 3.1, you can use field injection instead, and manually invoke methods to request injection.

Constructor :(X) public class MyActivity {@inject public MyActivity(LoginViewModel viewModel){... }} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - field injection: class MainActivity: AppCompatActivity() { @Inject lateinit var viewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { DaggerApplicationComponent.create().inject001(this) super.onCreate(savedInstanceState) ... } } public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; }}Copy the code

When using an Activity or Fragment, note the component lifecycle:

  • During the recovery phase in super.oncreate (), the Activity attaches bound fragments that may need access to the Activity. To ensure data consistency, inject a Dagger into the Activity’s onCreate() method before calling super.oncreate ().

  • When using a Fragment, you inject a Dagger into the Fragment’s onAttach() method, which can be done before or after calling super.onattach ().

3.3 @ Singleton / @ the Scope

  • @singleton / @scope: Declare Scope, which can constrain the Scope period of dependencies
@Singleton
public class UserRepository {
    ...
}
--------------------------------------------
@Component
@Singleton
public interface ApplicationComponent {
    ...
}
Copy the code

Use the same scoped annotations on ApplicationComponent and UserRepository to indicate that they are in the same scoped cycle. This means that the dependency is provided multiple times by the same Component with the same instance. You can either use the built-in @Singleton directly or use custom annotations:

@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}
--------------------------------------------
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomScope {}
Copy the code

Tip: Using @Singleton or @MyCustomScope, the effect is exactly the same.

The above code is automatically generated after the build:

public final class DaggerApplicationComponent implements ApplicationComponent { private Provider<UserRepository> userRepositoryProvider; private DaggerApplicationComponent() { initialize(); } private void initialize() { this.userRepositoryProvider = DoubleCheck.provider(UserRepository_Factory.create(UserLocalDataSource_Factory.create(), UserRemoteDataSource_Factory.create())); } @Override public UserRepository userRepository() { return userRepositoryProvider.get(); }... }Copy the code

Scope annotation constraints

There are a few constraints on scoped annotations that you should be aware of:

  • If a component has a scoped annotation, it can only be used by classes that provide the annotation or classes that do not have any scoped annotation.
  • A child component cannot use the same scoped annotations as a parent component.

Tip: You can refer to section 3.5 for the concept of subcomponents.

Scoped annotation specification

The Dagger2 framework does not strictly limit the scoped semantics you define, as long as you meet the constraints mentioned above. You can scope by business or you can scope by life cycle. Such as:

In accordance with the business divisions: @ Singleton @ LoginScope @ RegisterScope -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- according to the statement cycle divided into:  @Singleton @ActivityScope @ModuleScope @FeatureScopeCopy the code

However, it is more desirable to divide scopes by life cycle, and scopes should not explicitly indicate what they are intended to achieve.

3.4 + @ @ Module will

  • @Module + @Providers: how can A Dagger instantiate an object, but not in a constructor
public class UserRemoteDataSource {
    private final LoginRetrofitService loginRetrofitService;
    @Inject
    public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) {
        this.loginRetrofitService = loginRetrofitService;
    }
}
--------------------------------------------
@Module
public class NetworkModule {
    @Provides
    public LoginRetrofitService provide001(OkHttpClient client) {
        return new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);
    }
}
--------------------------------------------
@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {

    UserRepository userRepository();

    void inject001(MainActivity activity);
}
Copy the code

The @Module Module provides a different way to provide object instances than @Inject. In the @Module, the @Provides method returns the dependency instance and the parameters are further dependent objects. You also need to apply the module in the @Component argument.

The dependency diagram we have constructed so far looks like this:

3.5 @ Subcomponent

  • Subcomponent: Declare subcomponents. Use the concept of subcomponents to define more detailed scopes

A child component is a component that inherits and extends the parent component’s object graph. Objects in the child component can depend on objects provided by the parent component, but the parent component cannot depend on objects that the child depends on (simple containment, right?). .

Let’s continue with a simple example: Suppose we have a login module LoginActivity that relies on LoginModel. Our requirement is to define a child component whose declaration cycle only exists in a single login process. As mentioned in Section 3.2 (p. 32), activities cannot be injected using constructors, so we use the @inject field syntax for LoginActivity:

@Subcomponent
public interface LoginComponent {
    void inject(LoginActivity activity);
}
Copy the code

However, the LoginComponent defined this way is not really a child of a component. You need to add an additional declaration:

@Module(subcomponents = LoginComponent.class)
public class SubComponentsModule {
}
--------------------------------------------
@Component(modules = {NetworkModule.class,SubComponentsModule.class})
@Singleton
public interface ApplicationComponent {
    UserRepository userRepository();
    LoginComponent.Factory loginComponent();
}
--------------------------------------------
@Subcomponent
public interface LoginComponent {
    @Subcomponent.Factory
    interface Factory{
        LoginComponent create();
    }
    void inject001(LoginActivity activity);
}
Copy the code

Here we need to define a new module SubcomponentModule, and we need to define a child component Factory in LoginComponent, So that ApplicationComponent knows how to create a sample LoginComponent.

The LoginComponent declaration is now complete. To keep the LoginComponent lifecycle the same as the LoginActivity, you should create an instance of the LoginComponent inside the LoginActivity and hold a reference to it:

Public class LoginActivity extends Activity {1, holds a child component reference to ensure the same life cycle LoginComponent LoginComponent; 2. Inject @inject LoginViewModel LoginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); 3, create a component instance loginComponent = ((MyApplication) getApplicationContext ()). AppComponent. LoginComponent (). The create (); Inject loginComponent.inject(this); . }}Copy the code

By step 4, the loginViewModel field is initialized. Here’s a special point to pay attention to. Think about this: If you inject LoginViewModel repeatedly into a Fragment in a LoginActivity, is it an object?

@Subcomponent
public interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    void inject001(LoginActivity loginActivity);
    void inject002(LoginUsernameFragment fragment);
}
Copy the code

It must be a different object, because we haven’t used the @Singleton / @scope Scope annotation mentioned in Section 3.3. Now we add the scope annotation:

@Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope {} @ActivityScope @Subcomponent public interface LoginComponent { ... } @ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; }}Copy the code

The dependency diagram we have constructed so far looks like this:


4. Unit test based on Dagger2

When a project uses Dagger2 or some other dependency injection framework, there is a degree of loose coupling between its components that makes unit testing easy.

On the Dagger2 project you can choose to inject mock dependencies at different levels:

4.1 Object Level

You can define a FakeLoginViewModel and replace it with LoginActivity:

Public class LoginActivity extends Activity {1, holds a child component reference to ensure the same life cycle LoginComponent LoginComponent; Inject @inject FakeLoginViewModel loginViewModel; }Copy the code

4.2 Component Level

You can define two components for the official and beta versions: ApplicationComponent and TestApplicationComponent:

@Singleton
@Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
}
Copy the code

5. To summarize

To summarize the comments we mentioned:

annotations describe
@Component Create a Dagger container as an entry point to get the dependency
@Inject Indicates how Dagger instantiates an object
@Singleton / @Scope Scope, which can constrain the scope period of dependencies
@Module + @Providers Indicates how Dagger instantiates an object, but not as a constructor
@Subcomponent Declare subcomponents, using the concept of subcomponents to define more detailed scopes

The resources

  • 【Dagger · official website 】【Hilt · official website 】【Dagger2 · Github】
  • Dependency Injection in Android Developers (Must-see)
  • Tasting Dagger 2 on Android. By Fernando Cejas
  • From Dagger to Hilt, why Is Google obsessed with dependency Injection? — Throwing objects on the line
  • New member of Jetpack: An article on Hilt and Dependency Injection by Lin Guo
  • Dagger Navigation Update for Android Studio 4.1 — Android Developers
  • Dagger Traps and optimization in Kotlin Android Developers

Creation is not easy, your “three lian” is chouchou’s biggest motivation, we will see you next time!