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
@Reusable
Rather 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