Hilt is Google’s latest DEPENDENCY injection framework. It is based on Dagger, but it is different from Dagger. For Android developers, Hilt can be said to be built specifically for Android, providing a standard way to inject the Dagger dependency into Android applications, and creating a standard set of components and scopes that are automatically integrated into each lifecycle of an Android application. To make it easier for developers to get started.

Before you start this article, assume that you already know what dependency injection is, or if you don’t, get to know the concept. Hilt’s goal is to reduce the cost of getting started with the DEPENDENCY injection framework for Android developers, but the basic concept needs to be understood.


Some corresponding notes are as follows:

  • @HiltAndroidApp

    The code generation that triggers Hilt, including base classes applicable to the application, can use dependency injection, and the application container is the application’s parent container, which means that other containers can access the dependencies it provides.

  • @AndroidEntryPoint

    It creates a dependency container that follows the life cycle of the Android class

  • @Inject

    The field used for injection cannot be of type Private

    If you want to tell Hilt how to provide instances of the corresponding type, you need to add @inject to the constructor of the class to be injected.

    Hilt information about how to provide different types of instances is also called binding **. 阿鲁纳恰尔邦

  • @Install(xx)

    Install is used to tell Hilt which component the module will be installed on.


Components (Compenent)

By default, Hitl has the following standard components, which can be injected by adding @AndroidEntryPoint to the class

Compenent Injector for
ApplicationComponent Application
ActivityRetainedComponent ViewModel(please refer to theJetPack – ViewModel extension)
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent View@WithFragmentBindings
ServiceComponent Service

It should be noted that Hilt only supports activities that extend FragmentActivity (such as AppCompatActivity) and fragments that extend the Jetpack library, not fragments from the (now deprecated) FragmentAndroid platform.


The lifecycle of a component (Compenent)

  • It limits the lifetime of the binding between the creation of the component and the generation of the component
  • It indicates the appropriate value to use member injection. (for example: when the @inject field is not null)
Component scope Created at Destroyed at
ApplicationComponent @Singleton Application#onCreate() Application#onDestroy()
ActivityRetainedComponent @ActivityRetainedScope Activity#onCreate()1 Activity#onDestroy()1
ActivityComponent @ActivityScoped Activity#onCreate() Activity#onDestroy()
FragmentComponent @FragmentScoped Fragment#onAttach() Fragment#onDestroy()
ViewComponent @ViewScoped View#super() View destroyed
ViewWithFragmentComponent @ViewScoped View#super() View destroyed
ServiceComponent @ServiceScoped Service#onCreate() Service#onDestroy()

By default, all bindings are unscoped, which means that a new instance of the binding is created each time it is bound.

However, the Dagger allows the binding to scope to a specific component, as shown in the table above, within the specified component scope, instances are created only once, and all requests to that binding share the same instance.

Such as:

@Singletion
class TestCompenent @Inject constructor(a)Copy the code

Where @singleton means that the TestComponent instance is unique across the app and will be shared by any subsequent class that wishes to inject it.


How to use it?

Lead in dependence

implementation 'com. Google. Dagger hilt - android: 2.28 alpha'
kapt 'com. Google. Dagger hilt - android - the compiler: 2.28 alpha'
Copy the code
classpath 'com. Google. Dagger hilt - android - gradle - plugin: 2.28 alpha'
Copy the code

Increase under the corresponding model

apply plugin: 'dagger.hilt.android.plugin'
Copy the code

An 🌰 :

We have a remote data class for our NetDataSource, which we may then need to call in our Activity, as follows


class NetDataSource{
    fun test(a){
        println("I'm just a test method.")}}Copy the code
class MainActivity : AppCompatActivity() {
    lateinit var netDataSource: NetDataSource
    override fun onCreate(savedInstanceState: Bundle?).{... netDataSource = NetDataSource() } }Copy the code


There’s nothing wrong with that, we do that most of the time, and of course you can do by lazy in KT, but it depends on your scenario. But how do you modify the above code with Hilt?

After transforming

class NetDataSource @Inject constructor() {fun test(a){
        println("I'm just a test method.")}}Copy the code
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var netDataSource: NetDataSource
    override fun onCreate(savedInstanceState: Bundle?).{... netDataSource.test() } }Copy the code

Hilt needs to access all modules during code generation, so you must use @hiltAndroidApp to label your base class Application. Something like this:

@HiltAndroidApp
class KtxApplication : Application(a)Copy the code

Also, after adding @hiltAndroidApp, you can use @ApplicationContext anywhere you want




@Module

Modules are used to add bindings to Hlit, in other words, to tell Hlit how to provide different types of instances.

Added the @Module annotation for a class that represents the equivalent of a Module and specifies the component in which container the binding can be installed.

For each Android class that Hilt can inject, there is an associated Hilt Component, for example, The Application container is associated with ApplicationComponent, and the Fragmenet container is associated with the FragmentComonent, which is the component lifecycle we started with

An 🌰 :

Create a module:
@InstallIn(ApplicationComponent::class)
@Module
object TestModule {
	
  // Each time is a new instance
  @Provides
  fun bindBook(a):Book{
    	return Book()
  }
  
  // Reuse the same instance globally
  @Provides
  @Singleton
  fun bindBook(a):BookSingle{
    	return Book()
  }
}
Copy the code

Install is used to tell hilt which component the module will be installed on.

A common misconception is that all bindings declared in a module apply to the component on which the module is installed. But that’s not the case. Binding declarations annotated using scope annotations only will be scoped.


So when do you add the injection scope?

Scoping a binding has a cost in terms of generated code size and runtime performance, so use scoping with caution. A general rule for determining whether a binding should be scoped is to only scope a binding if code correctness requires it. If you think bindings are scoped only for performance reasons, first verify that there are performance issues, then consider using them@ReusableRather than component scope.

Note: In Kotlin, a module containing only @Provides can be an Object class. In this way, the provider can be optimized and almost inlined in the generated code.




Use @Provides to tell Hilt how to get specific instances

To tell Hilt how to provide types that cannot be injected by constructors

The function body of the annotated function is executed whenever Hilt needs to provide an instance of this type. @provides is often used in modules

An 🌰 :

Common use of room

We use Room, which has a database table and a corresponding Dao

@Entity(tableName = "book")
class Book(val name: String) {
	 @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}

@Dao
interface BookDao {
    @Insert
    suspend fun insertAll(vararg books: Book)

    @Query("SELECT COUNT(*) FROM book")
    suspend fun queryBookAll(a): Int
}
Copy the code

Next, we have an AppDataBase and a singleton of roomDatabase

@Database(entities = [Book::class], version = 1)
abstract class AppDataBase : RoomDatabase() {
    abstract fun bookDao(a): BookDao
}

object RoomSingle {
    lateinit var context: Context
    val roomDatabase by lazy {
        Room.databaseBuilder(
            context,
            AppDataBase::class.java,
            "logging.db"
        ).build()
    }
}
Copy the code

When we use it, we usually write it as follows:

class MainActivity : AppCompatActivity() {
    val bookDao by lazy {
        RoomSingle.roomDatabase.bookDao()
    }
}
Copy the code

How to use Hilt transformation?

We create a BookModule and use @Model to indicate that it is a module, and @InstallIn to declare that the module’s lifetime is APP level

@InstallIn(ApplicationComponent::class)
@Module
object BookModule {
 	@Provides
    @Singleton
    fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
        return Room.databaseBuilder(
            appContext,
            AppDatabase::class.java,
            "book.db"
        ).build()
    }
  
    @Provides
    fun provideBookDao(database: AppDatabase): BookDao {
        return database.bookDao()
    }
}
Copy the code

We added @Provides to the provideBookDao, which tells Hilt that database.bookdao () needs to be executed when providing an instance BookDao. Since we have the AppDatabase passing dependency, we also need to tell Hilt how to provide an instance of this type.

Since AppDatabase is generated by Room and is therefore another class that the project does not own, we simply copy the original method, and the @singleton flag here indicates that the method will only be called once, like a Singleton.

Change the code in MainActivity as follows:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var bookDao: BookDao
}
Copy the code

So far, we’re done, so we can get the BookDao anywhere. It also eliminates manual construction




Use @Binds to provide an injection for the interface

In the case of the interface, we cannot use the constructor for injection, and we need to tell Hilt which implementation to use. That is what Binds.

Attention should be paid to the following conditions:

  • Binds must annotate an abstract function whose return value is the interface to which we provide the implementation. An implementation is specified by adding a unique parameter that has an interface implementation type.

An 🌰 :

We have an IBook interface for storing and querying book data

interface IBook {
    suspend fun saveBook(name: String)

    suspend fun getBookAllSum(a): Int
}
Copy the code

And then if we wanted to get that interface object somewhere else, the normal way to do that would be to have one of your concrete implementation classes implement it, and then val iBook=xxxImpl() where we need to use it

How about Hint? To continue the code demonstration

Next we have a concrete implementation class, BookImpl, where we use constructor injection and inject a BookDao to work with the concrete data store. Here we use a suspend function, which for those of you who are not familiar with this area, is like a bit of a marker that tells the compiler that there might be a time-consuming operation on this area, so a suspend function is a logical process. Specific understanding can refer to the throwing line and other big guy’s explanation, here do not do too much explanation.

class BookImpl @Inject constructor() : IBook {
    @Inject
    lateinit var dao: BookDao
    override suspend fun saveBook(name: String) {
        // There may be some processing of their own...
        dao.insertAll(Book(name))
    }
  
   override suspend fun getBookAllSum(a): Int {
        return dao.queryBookAll()
    }
}
Copy the code

Then we need a new module to implement the injection of the IBook interface

@InstallIn(ApplicationComponent::class)
@Module
abstract class BookModel {
 @Binds
    abstract fun getIBook(impl: BookImpl): IBook
   }
Copy the code

When used in an Activity (Demo examples, actual development we prefer data processing in a Repository, managed by the ViewModel) :

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var iBook: IBook

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        btnSave.setOnClickListener {
            lifecycleScope.launch(Dispatchers.IO) {
                iBook.saveBook("Android art")
                val sum = iBook.getBookAllSum()
                Log.e("petterp"."Current length -----$sum")}}}}Copy the code




Qualifiers(limited)

In our example above, we have only one concrete implementation class, but often in actual development, we have multiple concrete implementations. Some of them may be used only by an Activity, while others may be used globally. How do we solve this problem?

We can define different modules for the two concrete implementations and use the Qualifers specification.

An 🌰 :

The above code continues. Now there’s another implementation that wants to implement storage with special conditions.

The other implementation class is BookConditionImpl

class BookConditionImpl @Inject constructor() : IBook {
    @Inject
    lateinit var dao: BookDao
    override suspend fun saveBook(name: String) {
        if (name == "Android art") {
            dao.insertAll(Book("Petterp"))}}override suspend fun getBookAllSum(a): Int {
        return dao.queryBookAll()
    }

}
Copy the code

Then we change the BookModel to add the BookConditionModel module

@InstallIn(ApplicationComponent::class)
@Module
abstract class BookModel {
    @Binds
    @Singleton
    abstract fun getIBook(impl: BookImpl): IBook
}

@InstallIn(ActivityComponent::class)
@Module
abstract class BookConditionModel {
    @Binds
    @ActivityScoped
    abstract fun getConditionIBook(impl: BookConditionImpl): IBook
}
Copy the code

Next, add two annotations, where @Qualifier is used

@Qualifier
annotation class BookModelSingle

@Qualifier
annotation class BookModelCondition

@InstallIn(ApplicationComponent::class)
@Module
abstract class BookModel {
    @BookModelSingle
    @Binds
    @Singleton
    abstract fun getIBook(impl: BookImpl): IBook
}

// The module scope is Activity
@InstallIn(ActivityComponent::class)
@Module
abstract class BookConditionModel {
    @Binds
    @ActivityScoped
    @BookModelCondition
    abstract fun getConditionIBook(impl: BookConditionImpl): IBook
}
Copy the code

Then change our Activity

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @BookModelCondition
    @Inject
    lateinit var iBookCondition: IBook

    @BookModelSingle
    @Inject
    lateinit var iBook: IBook
   
}
Copy the code




With the JetPack

As a recommended Dependency injection component by Google, Hilt can now be used in conjunction with ViewModel

Import dependence

    implementation 'androidx. Hilt: hilt - lifecycle - viewmodel: 1.0.0 - alpha02'
    kapt 'androidx. Hilt: hilt - compiler: 1.0.0 - alpha02'
		
		
		// The data of the viewModel can be restored without import, here is only for demonstration
    implementation "Androidx. Lifecycle: lifecycle - viewmodel - savedstate: 2.2.0." "
		// Easy to use the ViewModel-ktx extension
    implementation 'androidx. Activity: the activity: 1.1.0'
    implementation 'androidx. Fragments: fragments - KTX: 1.2.5'
Copy the code

An 🌰

We create a MainActivty

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel by viewModels<TestViewModel>()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        viewModel.test()
    }
}
Copy the code

ViewModel

class TestViewModel @ViewModelInject constructor(
    private val repository: TestRepository,
    @Assisted val savedState: SavedStateHandle
) : ViewModel() {
    fun test(a) {
        repository.test()
    }
}
Copy the code

TestRepository

@ActivityScoped
class TestRepository @Inject constructor() {
    fun test(a) {
        Log.e("petterp"."A test method")}}Copy the code

Supplement – Inject ViewModel into Java

public class TestViewModel extends ViewModel {
    @ViewModelInject
    public TestViewModel(a) {}public void test(a){
        Log.e("petterp"."I am the test method."); }}Copy the code

Up to now, Hilt related content is basically finished. Currently, there is little information about Hilt, and it is in alpha. If you want to use it in a real project, it is recommended to try and read more documents before making a decision.

References, by priority :(ladder required for the following)

new JetPack

Hilt- Code LABS

DaggerDev

Google-iosched