Hilt website hilt documentation

The background,

Most of the time we write programs to create objects with new Object (), and when the Object needs to be created in more than one place, we might encapsulate factory methods. Is there a more elegant way to do this? Dependency injection I believe everyone can say one or two, but in the Android side few people will go to use, the following through a small example, to introduce the use of dependency injection, hope to bring you a little help. Let me introduce you to two nouns.

1.1 inversion of Control and Dependency Injection

IOC (Inversion of Control) : Inverse of Control, which is an idea. This means giving the third party control over how the object is created.

DI (Dependency Injection) : The full name is Dependency Injection. Objects are created by Injection. Is a concrete implementation of IOC.

Whereas traditional programs create objects that are created by the caller through Obj Obj = new Obj(), dependency injection leaves the creation of objects and the management of their life cycle in the hands of the container, defining annotations such as: Inject Obj Obj, the container will automatically create an instance for us based on the annotation at the appropriate time, we use Obj directly in the application.

The purpose of dependency injection is to decouple

For example

When using MVVM mode to make network requests,ViewModel relies on the Repository layer, which relies on Remote Data Source and Room. MainApi as a Remote Data Source, the following I respectively use the common writing method, generic reflection writing method, hilt writing method for examples

2.1. Ordinary writing method

// Define the network interface
interface MainApi {
     @GET("goods/list")
     List<String> requestList(a)
}

/ / warehouse
class MainRepo {
    private MainApi api;
    public MainRepo(MainApi api) {
        this.api = api;
    }
     List<String>  requestList(a) {
        // Call the specific interface
       returnapi.requestList(); }}/ / the ViewModel layer
class MainViewModel extends ViewModel {
   private MainRepo repo = new MainRepo(new MainApi() {});

    void requestList(a){
        // Request the interface through the repoList<String> list = repo.requestList(); }}Copy the code

Question: Because according to the MVVM architecture, every Activity and Fragment depends on a ViewModel, and every ViewModel depends on a Repository, but the ViewModel instance is created using the Google API. Creating the Repo as a new in each ViewModel is repetitive and inelegant code, which can be reduced by reflection.

2.2. Reflection writing

// Define the network interface
interface MainApi {
     @GET("goods/list")
     List<String> requestList(a)
}

// Warehouse abstract class
abstract class BaseRepo<Api> {
    private Api api;

    public Api getApi(a) {
        return api;
    }

    public void setApi(Api api) {
        this.api = api; }}// Home page warehouse
class MainRepo extends BaseRepo<MainApi> {
    void requestList(a) {
        // Call the specific interfacegetApi().requestList(); }}// Abstract the ViewModel layer
abstract class BaseViewModel<R extends BaseRepo> {
    private R repo;

    public BaseViewModel(a) {
        try {
            repo = crateRepoAndApi(this);
        } catch(Exception e) { e.printStackTrace(); }}public R getRepo(a) {
        return repo;
    }
    // Reflection creates the Repo and Api
    public R crateRepoAndApi(BaseViewModel<R> model) throws Exception {
        Type repoType = ((ParameterizedType) model.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        R repo = (R) repoType.getClass().newInstance();
        Type apiType = ((ParameterizedType) repoType.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        String apiClassPath = apiType.getClass().toString().replace("class "."").replace("interface "."");
        repo.setApi(Class.forName(apiClassPath));
        returnrepo; }}/ / the ViewModel layer
class MainViewModel extends BaseViewModel<MainRepo> {
    void requestList(a) {
        // Request the interface through the repogetRepo().requestList(); }}Copy the code

Add BaseRepo and BaseViewModel classes, and define generics. When BaseViewModel is instantiated, get the generics of subclasses, and then reflect to create Repo, and then reflect to create API based on the generics of Repo. Reflection does allow decoupling, and instead of manually creating the Repo and Api in each ViewModel, is there another way to do it?

2.3 Hilt writing method

@HiltViewModel
public class MainViewModel extends ViewModel {
    @Inject
    public MainRepo repo ;
}

class MainRepo extends BaseRepo {
    @Inject
    public MainRepo(a) {}
    
    @Inject
    public MainApi api;
}

@InstallIn(SingletonComponent.class)
@Module
public class ApiModule {
    @Singleton
    @Provides
    public MainApi provideMainApi(a) {
        // Create an API with Retrofit. This is just an example.
        return newMainApi() {}; }}public interface MainApi {
     @GET("goods/list")
     List<String> requestList(a)
}
Copy the code

Hilt writing method is advanced, but it adds a lot of classes at compile time. Modularity development needs to add dependencies in each module’s build.gradle. Kotlin writing method will have less code. As for how to implement it in the real world, I personally prefer a reflection generic implementation, although creating a Repo in a ViewModel is not consistent with the MVVM architecture idea, but it is easy to use and decoupled.

3. Specific steps for using hilt

Before introducing Hilt, let’s talk about the history of dependency injection in Android. Dagger was launched by Square in 2012 and is based on reflection. Later, Google refactored this, called Dagger2, based on Java annotations, and checked for errors at compile time. If the compilation is successful, the project will work properly. It is applicable to Java and Kotlin. Hilt is a dependency injection framework written by Google for Android. Compared with Dagger2, hilt is easy to use and provides an Android API.

3.1. Import packages

1Introduce the CLASspath in build.gralde, the outermost layer of the project'com. Google. Dagger hilt - android - gradle - plugin: 2.37'

2Plugin at the top of the app module"dagger.hilt.android.plugin"
plugin "kotlin-kapt"

3In the app module kapt {// The error correction type is optional
    correctErrorTypes true 
}
android {
 compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
 }
4Adding dependency implementation'com. Google. Dagger hilt - android: 2.37'
 kapt 'com. Google. Dagger hilt - compiler: 2.37'
Copy the code

Add @hiltAndroidApp

You must annotate @hiltAndroidApp on the Application subclass

@HiltAndroidApp
class MyApp : Application() {
    override fun onCreate(a) {
        super.onCreate()
    }
}
Copy the code

Sunflower: This is the official Example from Google Android

Four, the meaning of Hilt commonly used annotations

Before we used the dependency injection framework, we normally created objects in our code with new XXX (). The creation of this object was actually handed over to the virtual machine itself, and we didn’t care how it was created internally. With dependency injection, object creation is in the hands of Hilt. How is hilt internally created? This also introduces the concept of containers.

The container is responsible for creating objects. We don’t need to go to new manually. We just need to add different annotations in the specified location. This is specified by ourselves via @installIn (container.class). The lifetime of objects created by the container is determined by the hilt, singleton classes are created by the singleton, and the lifetime of objects created by the ViewModel container is the same as that of the ViewModel. Objects used in an Activity are created by the Activity container, and the life cycle of objects created by different containers is different.

It should be called components, but I prefer to call them containers, which is more general, haha. Below is the official diagram, I added the ViewModelComponent to make it more comprehensive.

Container/Component (interface) Scope (annotations) Founded in Destroyed in
SingletonComponent @Singleton Application#onCreate() Application#onDestroy()
ActivityRetainedComponent @ActivityRetainedScoped Activity#onCreate() Activity#onDestroy()
ServiceComponent @ServiceScoped Service#onCreate() Service#onDestroy()
ViewModelComponent @ViewModelScoped The ViewModel to create The ViewModel destroyed
ActivityComponent @ActivityScoped Activity#onCreate() Activity#onDestroy()
FragmentComponent @FragmentScoped Fragment#onAttach() Fragment#onDestroy()
ViewComponent @ViewScoped View#super() View destroyed
ViewWithFragmentComponent @ViewScoped View#super() View destroyed

From the above picture, we should understand the following points:

  1. Container/component: @installIn for classes, scope: @Singleton, @Activityscoped for methods, etc.
  2. The SingletonComponent container has the longest lifetime, ServiceComponent and ActivityRetainedComponent SingletonComponent ActivityComponent and ViewModelComponent inherit in inheritance ActivityRetainedComponent, and so on…
  3. The class returned by the method of @ ActivityScoped logo, can undertake the dependency injection in fragments and the View, through the class @ FragmentScoped logo, if is ViewWithFragmentComponent container objects created can also be injected in the View.

Question: How does hilt internally create objects when MainActivity injects a User class? It's important to understand that.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var scope1: User
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
class User @Inject constructor() {}@Module
@InstallIn(ActivityComponent::class)
class UserModule {

    @Provides
    @ActivityScoped
    fun createUser(a): User {
        return User()
    }
 }
Copy the code

Take the code above as an example:

  1. The currently injected class is declared in the entry point Activity, Hilt inside will, in turn, looking for our projects through @ installIn definition ActivityComponent, ActivityRetainedComponent, SingletonComponent three containers. Similarly if injection class is in fragments statement, hilt inside to find FragmentComponent, ActivityComponent, ActivityRetainedComponent, SingletonComponent in turn.

  2. Look for a method in the container to create a User object, and if the method adds @provides, then it hits and is instantiated through that method. If @activityscoped is added to the method, then inject the User class into the Fragment and custom View of the Activity, which is the same as the User object in the Activity.

  3. If the @provides annotation is not added to the method, then Hilt will look for the User class and see if there is an @activityscoped annotation above the User class. If there is, instantiate it through the User constructor. Then inject multiple users into the Activity, all of which are the same object. The User injected into the Fragment and View is the same object as the User in the Activity, because the scope of the User is declared to be the same as the lifecycle of the Activity. Only one User object is created within the scope of an Activity’s onCreate() and onDestory().

  4. So this explains why the ActivityComponent points to the ViewComponent as well as the FragmentComponent, because the object is created by the ActivityComponent container, If you declare an @activityscoped annotation on the method that creates the object, the objects you inject into the Fragment and View will be created through this container.

  5. @xxscoped annotations can be defined on methods and classes. If the object is created through a container, the @xxscoped annotations will be ignored by Hilt even if they are defined on the class.

@HiltAndroidApp

For hilt to take effect, modules that use hilt must declare @hiltAndroidApp in a subclass of Application

@InstallIn

Effect on the name of the class, such as: @ InstallIn (SingletonComponent: : class), identity provide the current class to create objects are created by the container, the specific use see example below

@Module

This is used on the class name, usually with @installin, to identify the current class as a module, and the methods in the class are created by the specified container class. Half is used to create: third class, interface, build pattern construction, etc. Used in conjunction with @installIn to specify the container through which the module is created. See the following example for specific usage

@Singleton

Applied to a method to indicate that the object created by the method is a singleton and that the method is executed only once. See the following example for specific usage

@Provides

The method body tells Hilt how to provide an instance of the corresponding type. Whenever an instance of this type needs to be provided, Hilt executes the method body. See the following example for specific usage.

@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {

    @Singleton
    @Provides
    fun provideRetrofit(a): Retrofit {
        return Retrofit()
    }
}

Copy the code
  1. @installIn (container.class), you can only write the container.class specified above in ()
  2. @Module and @installIn are used together. Compilation fails without @Module
  3. @singleton indicates that the current method will only be executed once and that the object returned by the method is a Singleton
  4. The @provides function on the method is to have the container execute the method when the Retrofit object is created,
@AndroidEntryPoint

Applies to the classes specified below, except Application

Hilt has six entry points: Application Activity Fragment View Service BroadcastReceiver

Activities only support a subclass of ComponentActivity, except that Application is identified by @hiltAndroidApp. AndroidEntryPoint: AndroidEntryPoint: AndroidEntryPoint: AndroidEntryPoint: AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
@HiltAndroidApp
class MyApp : Application() {
    override fun onCreate(a) {
        super.onCreate()
    }
}
// Failed to compile
@AndroidEntryPoint
class User{}
Copy the code
@ActivityScoped

Can be applied to classes and methods. Acts on a class when an Activity is injected


class UnscopedBinding @Inject constructor() {}

@ActivityScoped  
class ScopedBinding @Inject constructor() {}

@Module
@InstallIn(ActivityComponent::class)
class MainModule {
    @Provides
    fun provideUnscopedBinding(a) = UnscopedBinding()

    @Provides
    @ActivityScoped
    fun provideScopedBinding(a) = ScopedBinding()
}

@Inject
lateinit var unscope1: UnscopedBinding

@Inject
lateinit var unscope2: UnscopedBinding

@Inject
lateinit var scope1: ScopedBinding

@Inject
lateinit var scope2: ScopedBinding

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
// MyLinearLayout is a control in Activity_main
class MyLinearLayout : LinearLayout {.@Inject
    lateinit var scope3: ScopedBinding
    @Inject
    lateinit var scope4: ScopedBinding
    
}
Copy the code

Scope1, scope2, scope3, and scope4 are the same object, because @activityscoped is added to provideScopedBinding (), which means that the object created by this method will only be created once if its lifetime is within the scope of the Activity. MyLinearLayout’s context is also MainActivity, so scope1 == scope3 == scope4

Unscope1, unscope2, unscope3, unscope4 are four different @provides objects. The @provides method is used by the container when the object is created. The container creates objects without using the provideScopedBinding () method. @Module and @installIn are used together. Compilation fails without @Module

@ViewModelScoped
@FragmentScoped
@ViewScoped
@ServiceScoped

@xxscoped annotations can be defined on methods and classes. If the object is created through a container, the @xxscoped annotations will be ignored by Hilt even if they are defined on the class. If the definition is on a class and the object is not created through the container, the @xxscoped annotation on the class takes effect.

@Inject

Use @inject to tell Hilt to create an instance of this class, often used to construct non-private fields, non-static methods.

// Action on structure
class User @Inject constructor() {
    
    // Apply to non-static methods
    @Inject
    fun autoCallByHilt(a){
        // This method is automatically called by hilt when the User object is created}}@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // Applies to non-private fields
    @Inject
    lateinit var user: User
    
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
Copy the code

@EntryPoint

Hilt already supports 6 entry points by default: Application, Activity, Fragment, View, Service, BroadcastReceiver. @entrypoint is a custom EntryPoint provided by hilt and must be used in conjunction with @installin.

@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
    Retrofit must be instantiated through the container, otherwise it will not compile
    fun getRetrofit(a): Retrofit
}

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
    @Singleton
    @Provides
    fun provideRetrofit(a): Retrofit {
        return Retrofit()
    }
}

  
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {...fun doSomething(context: Context) {
    val myEntryPoint = EntryPoints.get(context, MyEntryPoint::class.java)
    valretrofit = myEntryPoint.getRetrofit() ... }}Copy the code

@ Binds and @ the Qualifier

Must apply to an abstract method, and the method must return an interface

// Define a fruit module in which the return objects of the methods are created by the Activit container
@Module
@InstallIn(ActivityComponent::class)
abstract class FruitModule {
    // Apple annotated logo
    @AppleAnnotation  
    @Binds  
    abstract fun provideApple(pear: Apple): Fruit

    // Pear annotation logo
    @PearAnnotation
    @Binds
    abstract fun providePear(apple: Pear): Fruit
}
// Define apple notes
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppleAnnotation

// Define the pear annotation
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PearAnnotation

// Define the fruit category
interface Fruit {
    fun getName(a): String
}

class Apple @Inject constructor() : Fruit {
    override fun getName(a): String {
        return The word "apple"}}class Pear @Inject constructor() : Fruit {
    override fun getName(a): String {
        return "Pear"}}@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @AppleAnnotation
    @Inject
    lateinit var apple: Fruit

    @PearAnnotation
    @Inject
    lateinit var pear: Fruit
    
     override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        System.out.println(apple.getName()) / / apple
        System.out.println(pear.getName()) / / pears}}Copy the code
  1. @Binds only to abstract methods, where the parameters must be concrete classes and the return value must be an interface
  2. @Qualifier works on custom annotations, which are shared with @Binding on abstract methods, where Hilt creates concrete instances based on the parameter objects in the method.
  3. If the interface has only one subclass, there is no need to use custom annotations and @Qualifier

How to inject the interface

See the examples above for @Binds and @Qualifier

How to inject the third square class

@Module
@InstallIn(SingleComponent::class)
object NetworkModule {

  @Provides
  @SingleScoped
  fun provideAnalyticsService(a): Retrofit {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
  }
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var retrofit: Retrofit
    ...
} 
Copy the code

When defining the @InstallIn annotation, add @Module. You must add @Provides to the method that creates the third party object, telling hilt to create the object through that method, not through the third party constructor. The @singlescoped annotation indicates that the method will only be executed once and that the object created is a singleton.

How to inject different subclasses of the same interface

See the examples above for @Binds and @Qualifier

8. Default binding for hilt internal components/containers

An injected class, by default, can hold references to different objects in its constructor, which are analyzed based on the current entry point. See the example for details.

// Context, act, frag, view, will be assigned by hilt
class User @Inject constructor(var context: Application)

class UserByAct @Inject constructor(var act: FragmentActivity) 

class UserByFragment @Inject constructor(var frag: Fragment) 

class UserByView @Inject constructor(var view: View) 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var user: User
    
    @Inject
    lateinit var userAct: UserByAct
    
    The current entry point cannot inject a View into the UserByView class
    @Inject
    lateinit var userView: UserByView
    
   // Error writing
    @Inject
    lateinit var userByFrag: UserByFragment
    
    ...
} 

@AndroidEntryPoint
class MyFragment : Fragment() {
    @Inject
    lateinit var user: User
    
    @Inject
    lateinit var userAct: UserByAct
    
    @Inject
    lateinit var userByFrag: UserByFragment
}

@AndroidEntryPoint
class MyView : View {
    @Inject
    lateinit var user: User
    
    @Inject
    lateinit var userAct: UserByAct
    
    / / right
    @Inject
    lateinit var userView: UserByView
    
    // Error writing
    @Inject
    lateinit var userByFrag: UserByFragment
    ...
} 

Copy the code

We can also convert Application to our own MyApplication

@Module
@InstallIn(SingleComponent::class)
object ApplicationModule {
    @Provides
    fun provideMyApplication(context: Application): MyApplication {
        return context as MyApplication
    }
}

@Module
@InstallIn(FragmentComponent::class)
class BaseFragmentModule {
    @Provides
    fun provideBaseFragment(fragment: Fragment): BaseFragment {
        return fragment as BaseFragment
    }
}

class User @Inject constructor(var context: MyApplication) 

class UserByFragment @Inject constructor(var frag: BaseFragment) 

Copy the code

Inject ViewModel


@HiltViewModel
class MyViewModel @Inject constructor( val repo: MyRepo) : ViewModel() 

class MyRepo @Inject constructor(a)@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
   // Note: the ViewModel must be created by the ViewModelProvider
  val vm by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
  
}

Copy the code

Just add @hiltViewModel on top of the ViewModel. The way to create the ViewModel is through the ViewModelProvider

Ten, the end

So that’s pretty much all the common uses of Hilt. Introducing Hilt brings in the dagger library by default. To get started with Hilt quickly, you need to understand how the Hilt container creates objects, how each annotation is used. To use dependency injection in a multi-module application, you also need to use the DAGGER API to do so. Currently, the hilt/Jetpack integration only supports ViewModel and WorkManager. It is believed that Google will provide more Jetpack component support for Hilt in the future.