The use of Hilt in Android Jetpack

Hilt is a dependency injection library for Android that reduces the boilerplate code required to perform manual dependency injection in your projects. Performing manual dependency injection requires you to manually construct each class and its dependencies, and reuse and manage the dependencies with the help of a container.

Hilt provides a standard way to use DI (dependency injection) in your applications by providing a container for every Android class in your project and automatically managing its life cycle. Hilt is built on the popular DI library Dagger, thereby benefiting from Dagger's compile-time correctness, runtime performance, scalability, and Android Studio support. This article only discusses how to use it. The steps are as follows:

Introduce Hilt into the project.

project/build.gradleAdd the kotlin and hilt plug-ins below

buildscript {
    ext.kotlin_version = '1.5.31'
    ext.hilt_version = '2.40'
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.0.3'
        //kotlin编译插件
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        //hilt编译插件
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

app/build.gradleJoin kotlin and hilt below

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-parcelize'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    compileSdkVersion 31
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.example.android.hilt"
        minSdkVersion 16
        targetSdkVersion 31
        versionCode 1
        versionName "1.0"


        javaCompileOptions {
            annotationProcessorOptions {
                arguments["room.incremental"] = "true"
            }
        }
    }

    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
    implementation 'androidx.recyclerview:recyclerview:1.2.1'

    // Room
    implementation "androidx.room:room-runtime:2.3.0"
    kapt "androidx.room:room-compiler:2.3.0"

    // Hilt dependencies
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}

Use hilt in your project.

Step1: Use @HiltAndroidAppannotations

Create a new class inherited from Applicationand add annotations @HiltAndroidAppto trigger Hilt's code generation, including application base classes that can use dependency injection. The app container is the parent container of the app, which means other containers can access the dependencies it provides.

@HiltAndroidApp 
class LogApplication : Application()

Step2: Use @AndroidEntryPointto inject dependencies into Android classes.

After setting Hilt in the Application class and having application-level components, Hilt can @AndroidEntryPointprovide dependencies for other Android classes with annotations. Hilt currently supports the following Android classes:

  • Application (by using @HiltAndroidApp)
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

If you @AndroidEntryPointannotate an Android class with , you must also annotate the Android classes that depend on that class. For example, if you Fragmentannotate a fragment, you must also annotate all activities that use that fragment.

@AndroidEntryPoint 
class LogsFragment : Fragment() { .... }

Step3: Use hilt for field injection

@InjectAnnotations let Hilt inject instances of different types. In fact, this annotation is used when declaring variables.

@AndroidEntryPoint
class LogsFragment : Fragment() {

    @Inject lateinit var logger: LoggerLocalDataSource
    @Inject lateinit var dateFormatter: DateFormatter

    ...
}

Step4: Hilt provides examples.

step4-condition1: Use the constructor to @Injectobtain the instance.

For @Injectannotated variables, when providing an instance, if it is an instance created through a constructor , we can directly use the annotation on the constructor@Inject to let hilt create an instance of the class for us, such as the following DateFormatter

/**
 * 通过构造器创建依赖
 */
class DateFormatter @Inject constructor() {

    @SuppressLint("SimpleDateFormat")
    private val formatter = SimpleDateFormat("d MMM yyyy HH:mm:ss")

    fun formatDate(timestamp: Long): String {
        return formatter.format(Date(timestamp))
    }
}

Another example is the one Step3in logger. The difference between it and DateFormatter is that its construction parameters have parameters. So for this case, we also need to tell hilt how to get the instance of LogDao. In other words, if LogDao can be built through the constructor, just add @Injectannotations directly. But the logDao here is an interface, and it cannot manually add implementation classes (this is the DAO in the Android room). So we need to use other methods to obtain

@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
    ...
}
step4-condition2: @ProvidesProvide an example with

We can annotate functions in the Hilt module @Provideswith to tell Hilt how to provide types that cannot inject constructors. The hilt module is the use of @Module and @InstallIn annotated classes. You cannot declare the method of providing an object instance by adding an annotation to the constructor to provide an instance via and (specifying the scope). This is a module. We need to use the module to add bindings to Hilt. In other words, we tell Hilt how to provide instances of different types . In the Hilt module, you add bindings for types whose constructors cannot be injected, such as interfaces or classes that are not included in your project. For example OkHttpClient - you need to use its builder to create an instance. Because database operations are actually provided here, the scope should be global, so it is used . There are other components here@Inject@Module@InstallInModuleSingletonComponent

@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
//这个可以是个class,但是在 Kotlin 中,只包含 @Provides 函数的模块可以是 object 类。
//这样,提供程序即会得到优化,并几乎可以内联在生成的代码中。

    /**
     * 用 @Provides 提供实例。我们可以在 Hilt 模块中用 @Provides 注释函数,
     * 以告诉 Hilt 如何提供无法注入构造函数的 类型。
     */
    @Provides
    fun provideLogDao(database: AppDatabase): LogDao {
//
        return database.logDao()
        //Hilt 可从上述代码中得知,在提供 LogDao 的实例时需要执行 database.logDao()。
        //由于我们拥有 AppDatabase 作为传递依赖项,因此我们还需要告诉 Hilt 如何提供这种类型的实例。
    }

    //因为我们一直希望 Hilt 提供相同的数据库实例,所以我们用 @Singleton 注释 @Provides provideDatabase 方法。
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context):AppDatabase{
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "logging.db"
        ).build()
    }

}
step4-condition3: Use to @Bindsprovide the interface.

We cannot use constructor injection for interfaces. @BindsTo tell Hilt what implementation to use for an interface, you can use annotations on functions within the Hilt module . Abstract functions@Binds must be annotated (because the function is abstract, it contains no code, and the class must also be abstract). The return type of the abstract function is the interface for which we want to provide an implementation (i.e. AppNavigator). The implementation is specified by adding a unique parameter with the interface implementation type (i.e. AppNavigatorImpl). For example, in MainActivity, the interface we depend on

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var navigator: AppNavigator
    ....
}

So for this we need to create a new moduleuse @Bindsto get, if the type has a scope, the @Binds method must have a scope annotation

//我们的新导航信息(即 AppNavigator)需要特定于 Activity 的信息
//(因为 AppNavigatorImpl 拥有 Activity 作为依赖项)。
// 因此,我们必须将其安装在 Activity 容器中,而不是安装在 Application 容器中,因为这是有关 Activity 的信息所在。
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {

    @Binds
    abstract fun provideNavigator(impl: AppNavigatorImpl):AppNavigator
    //参数为具体的实现类,所以要告知hilt如何提供实现类的实例。下面的实现类通过构造函数提供实例
}


//======AppNavigatorImpl.ktx========//

//AppNavigatorImpl 会依赖于 FragmentActivity。由于系统会在 Activity 容器中提供 AppNavigator 实例
// (亦可用于 Fragment 容器和 View 容器,因为 NavigationModule 会安装在 ActivityComponent 中),所以 FragmentActivity 目前可用
class AppNavigatorImpl @Inject constructor(private val activity: FragmentActivity) : AppNavigator {

    override fun navigateTo(screen: Screens) {
        val fragment = when (screen) {
            Screens.BUTTONS -> ButtonsFragment()
            Screens.LOGS -> LogsFragment()
        }

        activity.supportFragmentManager.beginTransaction()
            .replace(R.id.main_container, fragment)
            .addToBackStack(fragment::class.java.canonicalName)
            .commit()
    }
}
step4-condition4: use qualifier

To tell Hilt how to provide different implementations (multiple bindings) of the same type, you can use qualifiers. Its definition is actually annotation.

@Qualifier 
annotation class InMemoryLogger 
@Qualifier 
annotation class DatabaseLogger

For example, if you want to provide a memory-based implementation method for log addition and deletion checking, then define an interface

interface LogDataSource {
    fun addLog(msg: String)
    fun getAllLogs(callback: (List<Log>) -> Unit)
    fun removeLogs()
}

The Room-based implementation is as follows. It is actually the implementation mentioned at the beginning, but it only implements the interface.

@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao):LogDataSource {

    private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
    private val mainThreadHandler by lazy {
        Handler(Looper.getMainLooper())
    }

    override fun addLog(msg: String) {
        executorService.execute {
            logDao.insertAll(
                Log(
                    msg,
                    System.currentTimeMillis()
                )
            )
        }
    }

    override fun getAllLogs(callback: (List<Log>) -> Unit) {
        executorService.execute {
            val logs = logDao.getAll()
            mainThreadHandler.post { callback(logs) }
        }
    }

    override fun removeLogs() {
        executorService.execute {
            logDao.nukeTable()
        }
    }
}

The memory-based implementation is as follows

@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor():LogDataSource {
    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() {
        logs.clear()
    }
}

Based on the above introduction, when using the interface we define the implementation class as follows

@Module
@InstallIn(SingletonComponent::class)
abstract class LoggingDatabaseModule {
    @DatabaseLogger
    @Binds
    @Singleton
    abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LogDataSource
}

@Module
@InstallIn(ActivityComponent::class)
abstract class LoggingInMemoryModule {
    @InMemoryLogger
    @Binds
    @ActivityScoped
    abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LogDataSource
}

You can see that we have defined two modulesmodule . The reason why they are not one module is because the scopes of the two implementations are different . @BindsAnd we also added it to the InMemory method @ActivityScoped, which is necessary because the scope is specified in the implementation class . In the same way, we also added custom annotations here InMemoryLogger, which tell hilt which method to choose to provide instances. If no qualifier is added, an error will be reported. The actual use of this interface is as follows:

class ButtonsFragment : Fragment() {
    @InMemoryLogger
    @Inject lateinit var logger: LogDataSource
    ...
}

You can see that the difference from Step 3 is that the type of the variable here is an interface rather than a specific implementation, and secondly, a qualifier is added . In summary, this is the basic use of Hilt

Android study notes

Android performance optimization article: https://qr18.cn/FVlo89
Android vehicle article: https://qr18.cn/F05ZCM
Android reverse security study notes: https://qr18.cn/CQ5TcL
Android Framework underlying principles article: https://qr18.cn/AQpN4J
Android audio and video article: https://qr18.cn/Ei3VPD
Jetpack family bucket article (including Compose): https://qr18.cn/A0gajp
Kotlin article: https://qr18.cn/CdjtAF
Gradle article: https://qr18.cn/DzrmMB
OkHttp source code analysis notes: https://qr18.cn/Cw0pBD
Flutter article: https://qr18.cn/DIvKma
Eight knowledge bodies of Android: https://qr18.cn/CyxarU
Android core notes: https://qr21.cn/CaZQLo
Android interview questions from previous years: https://qr18.cn/CKV8OZ
The latest Android interview questions in 2023: https://qr18.cn/CgxrRy
Android vehicle development position interview exercises: https://qr18.cn/FTlyCJ
Audio and video interview questions:https://qr18.cn/AcV6Ap

Guess you like

Origin blog.csdn.net/maniuT/article/details/132714544