Jackpack - Hilt

1. Concept

An object used in a class is not instantiated in this class (for example, Activity cannot be instantiated manually), but is injected externally (used after the object is passed in from the outside). This implementation method is called dependency injection Dependency. Injection (referred to as DI).

constructor injection Pass object B to classA through construction. Some objects cannot be used by instantiation, such as Activity.
Field injection Set object C to the field of classA through a function (also called setter injection, property injection). If a class has many types of dependencies and the order must be strictly enforced (for example, before building a car, you need to build the wheels, and to build the wheels, you need to build the screws and tires first), as the project becomes more complex, you will need to write a lot of template code, and the coupling degree will be higher. , manual injection is prone to errors.
method injection Pass object D into the method of classA and use it only in this method.
Factory injection ClassA calls the factory class to produce objects Calling and production are not in the same place, which is not conducive to modification testing.
Singleton injection ClassA calls the singleton class to obtain the object it holds The life cycle of an object is difficult to manage. It usually does not need to exist in the entire APP life cycle, and specifying a specific life cycle requires a lot of judgment.

automatic injection

Reflection-based solution that can connect dependent types at runtime Excessive use of reflection methods will affect the running efficiency of the program, and reflection methods will not generate errors during the compilation stage, so that the correctness of the reflection method can only be verified when the program is running. Dagger developed by Square.
Static solution (via annotations) that generates code that links dependent types at compile time Problems with the use of dependency injection can be discovered at compile time. Google developed Dagger2 and Hilt based on Dagger. Dagger2 is cumbersome to use, while Hilt provides a simpler implementation specifically for Android development and can work better with other Jetpack components.

2. Add dependencies

The latest version

2.1 Project.gradle

plugins {
     id 'com.google.dagger.hilt.android' version "2.44" apply false
}

2.2 app.gradle

plugins {
    id 'com.google.dagger.hilt.android'
}
dependencies {
    implementation 'com.google.dagger:hilt-android:2.44'
    kapt 'com.google.dagger:hilt-compiler:2.44'
}
// Allow references to generated code
kapt {
    correctErrorTypes true
}

3. Configure Application (@HiltAndroidApp)

You must customize an Application and add the @HiltAndroidApp annotation to it, which will trigger Hilt code generation. The generated Hilt component is attached to the life cycle of the Application object and provides it with dependent types. In addition, it is also the parent component of the application, which means that other components can access the dependency types it provides.

4. Configure the Android class (@AndroidEntryPoint)

After using @AndroidEntryPoint to annotate the following Android classes, you can inject dependencies into the fields in them.

  • To add annotations to an Android class, you must add annotations to other Android classes that depend on this class (for example, if you add annotations to FragmentA, you must add annotations to all Activities that use this FragmentA). 
Supported Android classes Annotations used illustrate
Activity @AndroidEntryPoint Only Activities that extend ComponentActivity (such as AppCompatActivity) are supported.
Fragment Only fragments that extend androidx.Fragment are supported, reserved fragments are not supported.
View
Service
BroadcastReceiver
ViewModel @HiltViewModel

5. Field injection (@Inject)

Declare a lateinit var property and add the @Inject annotation.

  • Injected fields cannot be private and will cause compilation errors. 
@AndroidEntryPoint
class LoginFragment : Fragment() {
    //属性未手动初始化,依赖注入提供了实例,所以不会报错
    @Inject lateinit var logBean: LogBean    //不能为private
}

6. Binding dependent types (construction) @Inject

Tells Hilt how to create an instance of the dependent type. Add @Inject annotation to the dependent type's constructor.

  • If the constructor has parameters, the parameter types also need to be bound.
//无参
class LogBean @Inject constructor() {}
//有参
data class LogBean @Inject constructor(
    val userName: String,    //又依赖了String类型,String也要进行绑定依赖
    val time: TimeBean       //又依赖了TimeBean类型,TimeBean也要进行绑定依赖

7. Bind dependency type (module) @Module

We cannot add annotations to the constructor of the dependent type (unconstructed interface type, type that does not belong to itself such as String, and must use the builder mode to create an instance such as Retrofit), so you need to manually create a module class through @Module and @InstallIn (XXX::class) Load the module into the specified Hilt component (different Android classes have corresponding Hilt components), and provide instances through functions @Binds @Provids, so that the module can provide dependencies for the corresponding Android classes (object's creation, injection, destruction).

Annotation module class @Module Tells Hilt how to provide instances of this type.
@InstallIn Tells the Hilt component (for which Android class) the Hilt module will be loaded into.
annotation function @Binds Provide an interface instance. Abstract functions must be annotated so the class is also abstract. The return value tells which instance of the interface type is provided, and the parameters tell the implementation type of the interface (which also requires a construction annotation).
@Provides Provide examples. You can annotate the class. It is more efficient to define only the @provides function as an object. The return value tells what type of instance is provided, the parameters tell what other types the provided instance depends on (these types also require construction annotations), and the function body tells how to create the instance (the function body is executed every time an instance needs to be provided).

7.1 Providing a single instance

7.1.1 Provide interface instance @Binds

  • The type of function parameters, that is, the implementation class of the interface, must also be bound and dependent.
interface IWork
class WorkImpl @Inject constructor(): IWork {}

@Module
@InstallIn(ActivityComponent::class)
abstract class WorkModule {
    @Binds
    abstract fun bindIWork(workImpl: WorkImpl): IWork    //又依赖了WorkImpl类型,即实现类也要进行绑定依赖
}

7.1.2 Providing a single instance @Provides

  • If the function has parameters, the parameter types must also be bound and dependent.
@Module
@InstallIn(ActivityComponent::class)
object RetrofitModule {
    @Provides
    fun provideRetrofit(okHeepClient: OkHttpClient): Retrofit {    //又依赖了OkHttpClient类型,OkhttpClient也要进行绑定依赖
        return Retrofit.Builder()
            .client(okHeepClient)
            .baseUrl(ApiService.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}

7.2 Provide multiple examples of different implementations

In actual development, you may need to create multiple objects of the same type with different implementations, such as Student("Zhang San") and Student("李思"), String("A") and String("B"). The above method can only provide objects of the same implementation for the target type, and distinguish different implementations by using qualifiers.

7.2.1 Using @Named

You only need to use the @Named annotation for functions annotated with @Binds or @Provids, and pass in a unique tag to distinguish them. When using, you must also add the corresponding tag to let Hilt select the corresponding instance when injecting.

@Module
@InstallIn(ActivityComponent::class)
object StringModule {
    @Provides
    @Named("One")
    fun providesOneString() = "One"
    @Provides
    @Named("Two")
    fun providesTwoString() = "Two"
}

@AndroidEntryPoint
class DemoFragment : Fragment() {
    @Inject @Named("One") lateinit var oneString: String
    @Inject @Named("Two") lateinit var twoString: String
}

7.2.2 Use custom annotation @Qualifier

Using the @Named method can only be hard-coded, because the characteristics of the annotation cannot be penetrated into a static String, and it is easy to make mistakes or miss them in later reconstruction. First define the annotation according to the required classification, use @Qualifier to declare that the function is to inject different instances of the same type, and use @Retention to declare the scope of the annotation (AnnotationRetention.BINARY means that the annotation will be retained after editing). The usage is similar to @Named.

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OneString

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TwoString

@Module
@InstallIn(ActivityComponent::class)
object StringModule{
    @Provides
    @OneString
    fun providesOneString(): String = "One"
    @Provides
    @TwoString
    fun providesTwoString(): String = "Two"
}

@AndroidEntryPoint
class DemoFragment : Fragment() {
    @Inject @OneString lateinit var oneString: String
    @Inject @TwoString lateinit var twoString: String
}

8. Generate components for Android classes

Injecting an Android class will generate a corresponding Hilt component class (component). The module is loaded into the specified component through @InstallIn(XXX::class). The component can provide dependencies for the corresponding Android class (object creation, injection, destruction ).

  • No component is generated for Broadcast, the broadcast receiver is injected directly from the SingletonComponent. 
  • ActivityRetainedComponent (recommended) will not be destroyed when the Activity is rotated, while ActivityComponentd will regenerate the component every time.
Android class Generated Hilt component class Specifiable scope Default bound dependency type Create actual~destroy timing
Application SingletonComponent @Singleton Application Application:onCreate()~Destroyed
not applicable ActivityRetainedComponent @ActivityRetainedScope Application Activity:onCreate()~onDestroy()
ViewModel ViewModelComponent @ViewModelScope SavedStateHandle ViewModel: Created ~ Destroyed
Service ServiceComponent @ServiceScoped Application、Service Service:onCreate()~onDestroy()
Activity ActivityComponent @ActivityScoped Application、Activity Activity:onCreate()~OnDestroy()
View ViewComponent @ViewScoped Application、Activity、View View:super()~Destroyed
Fragment FragmentComponent @FragmentScoped Application、Activity、Fragment Fragment:onAttach()~onDestroy()
@WithFragmentBindings annotated View ViewWithFragmentComponent @ViewS coped Application、Activity、Fragment、View View:super()~Destroyed

8.1 Component life cycle

Components have the same life cycle as the corresponding Android class, otherwise there will be memory leaks.

  • ActivityRetainedComponent survives configuration changes, so it is created the first time Activity#onCreate() is called and destroyed the last time Activity#onDestroy() is called.

8.2 Scope of components

By default, all bindings in Hilt have no scope, that is, a new instance will be created every time the code accesses a field. When you need to share an instance, you need to limit the scope of the binding, that is, the provided instance will be in the corresponding The Android class is a singleton (the singleton is maintained in the same Activity, and the instances in different Activity are different).

  • The specified scope must be consistent with the scope of the component it is loaded into, otherwise an error will be reported.  
@ActivityScoped    //指定作用域
class Demo @Inject constructor() {...}

@Module
@InstallIn(ActivityComponent::class)
object StringModule {
    @ActivityScoped    //指定作用域
    @Provides
    fun providesOneString() = "One"
}

8.3 Component hierarchy

When the scope of a dependent type is the entire APP, it can definitely be accessed in the Activity. There is an inclusion relationship in the scope, that is, there is a hierarchy of components. When a module is loaded into a component, the dependency types that the module is bound to can also be used to bind subcomponents below that component hierarchy.

  • ViewComponent can use the dependency types bound in ActivityComponent. If you also need to use bindings in FragmentComponent and the view is part of Fragment, use the @WithFragmentBindings annotation together with @AndroidEntryPoint.

8.4 Dependency types bound by component by default

Each component has a default bound dependency type, so it can be used directly without manually binding dependencies. In addition, @ApplicationContext and @ActivityContext can be used to obtain context binding.

class Demo @Inject constructor(
    val activity: Activity,
    @ActivityContext val context: Context
)

Use with other Jetpack libraries

.1 ViewModel

        Add @HiltViewModel to ViewModel and use @Inject on constructor. In Activity/Fragment with @AndroidEntryPoint you can use ViewModelProvider or by viewModels() to get instance.

        Instances are provided by ViewModelComponent, which have the same life cycle as the ViewModel and therefore survive configuration changes. If you need to get the same instance every time you access it, use @ViewModelScope to limit the scope.

@HiltViewModel
class DemoViewModel @Inject constructor(
    private val avedStateHandle: SavedStateHandle,
    private val repository: DemoRepository
) : ViewModel() {

}

@AndroidEntryPoint
class DemoActivity : AppCompatActivity() {
    private val viewModel: DemoViewModel by viewModels()
}

.2 Navigation

If the ViewModel is scoped to the navigation graph, use hiltNavGraphViewModels( ), which works with Fragments with @AndroidEntryPoint.

implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0'
val viewModel: ExampleViewModel by hiltNavGraphViewModels(R.id.my_graph)

Guess you like

Origin blog.csdn.net/HugMua/article/details/132544090