“Jetpack-hilt Dependency Injection”
This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together
I. Inversion of control principle
When it comes to dependency injection, inversion of control IoC has to be mentioned. So what is IoC? Inversion of Control is a principle and idea in object-oriented programming. Its main purpose is to reduce the coupling between modules. Decouple dependencies between modules through third parties or containers.
Take Car as an example. A Car cannot do without its Engine. The usual implementation method can be as follows:
class Car {
private val engine = Engine()
fun start(a) {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
Copy the code
In this case, the Car needs to provide the Engine internally to run, that is, the Car depends on the Engine. There is a strong coupling between them, and the internal implementation must be pushed back to the re-implementation, assuming that a change of engine is needed, such as upgrading from V6 to V8. The strong coupling makes it very inflexible and inconvenient to single test. The idea of introducing an Ioc container to provide engine capabilities to Car within the Ioc container is called inversion of control. DI is an evolving design pattern based on this idea.
Dependency injection in Android
Why is dependency injection needed in a project? What are the benefits of dependency injection?
- Good for code reuse
- Easy to refactor code
- Easy unit testing
The function of a single class is generally designed to be relatively single, and a complete system needs the cooperation between multiple classes and objects to complete. Classes are usually introduced into other classes in several ways:
-
Internally instantiate the desired object as a variable, as in the Car example new Engine().
-
From the container, such as the management class Manager, and so on.
-
Supplied as a parameter, as passed in a constructor; It is passed in through setter functions, and this is commonly known as dependency injection.
Examples of dependency injection:
// The constructor form
class Car(private val engine: Engine) {
fun start(a) {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
// The form of setter functions
class Car {
lateinit var engine: Engine
fun start(a) {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
Copy the code
Manual injection, either in the form of constructors or setter functions. Simpler classes are easy to work with, but in real development, as the project gets more complex, so do the dependencies. This form of manual injection is obviously inadequate. Or the initialization process is long and time complex, and the management of the life cycle and the release of resources must be considered carefully. Third-party libraries can be used to solve such problems. General solutions can be divided into two categories:
A reflection based solution that connects dependencies at run time.
A static solution that generates code to connect dependencies at compile time.
The well-known Dagger is an excellent DEPENDENCY injection library, but few people actually use it. On the one hand, Dagger is so good that it takes time and effort to fully understand it patiently. On the other hand, there are so many rules that developers are basically starting out and giving up. Hilt implements a dependency injection library for Android using a Dagger base, simplifying the process.
3. Use of Hilt
1. The role
Hilt provides a standard way to use DI (dependency injection) in your application by providing a container for each Android class in your project and automatically managing its life cycle.
2. Import dependent packages
In the build.gradle file at the root of your project add:
buildscript {
...
dependencies {
...
classpath 'com. Google. Dagger hilt - android - gradle - plugin: 2.28 alpha'}}Copy the code
Add dependencies to the build.gradle file at app level:
dependencies {
implementation Com. Google. "dagger hilt - android: 2.28 alpha." "
kapt Com. Google. "dagger hilt - android - the compiler: 2.28 alpha." "
}
// Android Studio 4.0 or later is required
Copy the code
4. Field Meanings
1.@HiltAndroidApp
To use Hilt as dependency injection, you must include this annotation in your Application class. The Dagger component is generated (Hilt’s underlying implementation is based on the Dagger), and the generated base class acts as the container responsible for injecting the member into the Android class and instantiating the component at the correct lifecycle point. Something similar is @AndroidEntryPoint.
@HiltAndroidApp
class CustomApplication : Application() {... }Copy the code
Component Once a module is generated, its bindings can be used as dependencies for other bindings in that component, or for other bindings in any child component under that component in the component hierarchy:
2.@AndroidEntryPoint
A marked Class can introduce support for other components, simply by introducing dependent objects. There is no need to create or otherwise instantiate the desired object internally. However, this annotation is required for both dependencies and dependents. Take the relationship between a Fragment and an Activity as an example. A Fragment is attached to an Activity. This annotation @androidEntryPoint is required for both fragments and activities. The current versions of classes that support this annotation are:
-
Application (by using @hiltAndroidapp)
-
Activities that support extending ComponentActivity, such as AppCompatActivity
-
Supports only extensions of fragments on Androidx. Fragment, but does not support reserved fragments
-
Supports View, Service, and BroadcastReceiver
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {... }@AndroidEntryPoint
class LogsFragment : Fragment() {... }//LogsFragment is attached to MainActivity and must be annotated @androidEntryPoint
Copy the code
3.@Inject
Specific instance injection, take the official Demo as an example, simple Log save and display, need logger object to perform the corresponding operation. Dependencies can be added this way (private properties are not supported, that is, public is required to decorate). Inject instance objects by annotating @Inject.
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
}
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log- > >)Unit)
fun removeLogs(a)
}
Copy the code
The LoggerDataSource here is designed as an interface, what is the benefit of that? Interface oriented programming, in order to make the extension more flexible, designed in the form of interfaces, can provide different implementations. For example, store some logs in memory and upload them to the server. Hilt knows that we need a LoggerDataSource object, but we need to tell Hilt the implementation rules for the instance. So add @inject in the constructor of LoggerInMemoryDataSource as well.
// Add @inject to the constructor
class LoggerInMemoryDataSource @Inject constructor() : LoggerDataSource {
private val logs = LinkedList<Log>()
override fun addLog(msg: String) {
logs.addFirst(Log(msg, System.currentTimeMillis()))
}
override fun getAllLogs(callback: (List<Log- > >)Unit) {
callback(logs)
}
override fun removeLogs(a) {
logs.clear()
}
}
Copy the code
4. @ the Module with @ InstallIn
Module injection, as the name suggests, and both comments occur simultaneously. Modular development is no stranger to external, third-party classes when the constructor’s input parameter is an interface. The Hilt does not know the type of object to inject, so it needs to use Module’s help to tell the Hilt the rules and information for injection. Scopes are also required information (@installin), such as network requests, global Toast and other components that are Application unique. Some other components are only used in fragments. So the module definition also needs to tell Hilt whether it should be designed to be global or just page-specific.
@InstallIn(SingletonComponent::class)
@Module
objectDatabaseModule {... }Copy the code
Such as the global database Module, first of all, the whole Application the only instance (@ InstallIn (SingletonComponent: : class), @ the Module field represents a Module components. If an interface is injected, how do you tell Hilt to handle it? This is where @Cursorlies is needed to tell Hilt which specific implementation is required
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log- > >)Unit)
fun removeLogs(a)
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
Copy the code
- The LoggerDataSource is the defined interface, and the LoggingInMemoryModule acts as a module to tell Hilt the rules for providing specific instances.
- The caller is told by @Binds that the specific implementation class is LoggerInMemoryDataSource
This is the injection of the interface, but the interface is still defined by the developer, so we know the implementation. If it is a third party library, such as network requests, database construction, etc., it needs another key @Provides, which is directly constructed in the module and provided. Such as global database construction:
@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
Copy the code
In the case of interfaces with multiple implementations, how does Hilt know which implementation class to provide? The @qualifier Qualifier allows developers to customize attribute tags and compare different implementations to each other. In the case of Log recording, it is divided into local database saving and memory saving. The corresponding Module can be realized as:
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
@InstallIn(SingletonComponent::class)
@Module
abstract class LoggingDatabaseModule {
@DatabaseLogger
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@InMemoryLogger
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
// Add different comments when used
@InMemoryLogger
@Inject lateinit var logger: LoggerDataSource
Copy the code
To summarize, the Hilt module:
-
In the case where the injection parameter is an interface, a module is required to define the concrete implementation of the interface and provide it to the caller, which requires coordination with @Binds
-
To solve this problem, the module needs to implement the specific instance and provide it to the caller. The module needs to cooperate with @Provides
-
The @cursor-function return type tells the Hilt function which instance of the interface it provides
-
The @cursor-function parameter tells Hilt which implementation to provide
-
@provides – The function return type tells Hilt which type the function Provides
-
@provides – The function argument tells Hilt about the corresponding type of dependency
-
@provides – The function body tells Hilt how to provide instances of the corresponding type. Hilt executes the function body whenever it needs to provide an instance of the type
-
Qualifier – Custom attributes that provide tags for different implementations
Hilt component and Android App component scope
Scope and life cycle
Seven, documentation,
Dependency injection DI
Code
Hilt
Github