Dependency injection với Hilt

Hilt là một thư viện trợ giúp dependency injection cho Android giúp giảm bớt việc thực hiện dependency injection thủ công trong dự án của bạn.

Hilt cung cấp một cách đơn giản để sử dụng DI trong ứng dụng của bạn bằng cách cung cấp các container cho tất cả các class Android trong dự án của bạn và tự động quản lý vòng đời của chúng. Hilt được xây dựng dựa trên thư viện DI phổ biến Dagger để hưởng lợi từ độ chính xác của thời gian biên dịch, hiệu suất thời gian chạy, khả năng mở rộng và hỗ trợ Android Studio mà Dagger cung cấp. Để biết thêm thông tin, hãy xem Hilt và Dagger.

Hướng dẫn này giải thích các khái niệm cơ bản về Hilt và các container được tạo ra. Nó cũng bao gồm việc hướng dẫn sử dụng Hilt trong ứng dụng.

1. Thêm các dependency

Đầu tiên, thêm plugin hilt-android-gradle-plugin vào tệp build.gradle gốc trong dự án của bạn:

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

Sau đó, apply Gradle plugin và thêm các phần phụ thuộc này vào tệp app/build.gradle của bạn:

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

Lưu ý: Các dự án sử dụng cả Hiltdata binding yêu cầu Android Studio 4.0 trở lên.

Hilt sử dụng Java 8. Để enable Java 8 trong dự án của bạn, hãy thêm phần sau vào tệp app/build.gradle:

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

2. Hilt application class

Tất cả các ứng dụng sử dụng Hilt phải có một lớp Application gắn với annotation @HiltAndroidApp @HiltAndroidApp kích hoạt việc tạo mã của Hilt, bao gồm một base class cho ứng dụng của bạn, đóng vai trò là dependency container cấp Application.

@HiltAndroidApp
class ExampleApplication : Application() { ... }

Hilt component được tạo này được gắn vào vòng đời của Application và cung cấp các phụ thuộc vào nó. Ngoài ra, nó là thành phần mẹ của ứng dụng, có nghĩa là các thành phần khác có thể truy cập vào các phụ thuộc mà nó cung cấp.

3. Inject dependencies trong các class

Sau khi Hilt được setup trong class Application của bạn và có sẵn thành phần cấp ứng dụng, Hilt có thể cung cấp các phần phụ thuộc cho các class khác với annotation @AndroidEntryPoint.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }

Hilt hiện hỗ trợ các class sau:

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

Nếu một class được gắn với annotation @AndroidEntryPoint, thì bạn cũng phải gán annotation vào các class phụ thuộc vào nó. Ví dụ: nếu bạn gán annotation @AndroidEntryPoint vào một fragment, thì bạn cũng phải gán annotation vào bất kỳ activity nào chưa fragment đó.

Lưu ý: Các quy tắc áp dụng Hilt cho các class:

  • Hilt chỉ hỗ trợ các activity extend từ ComponentActivity, chẳng hạn như AppCompatActivity.
  • Hilt chỉ hỗ trợ các fragment extend từ androidx.Fragment.
  • Hilt không hỗ trợ các retained fragment.

@AndroidEntryPoint tạo một thành phần Hilt riêng lẻ cho từng class trong dự án của bạn. Các thành phần này có thể nhận các phụ thuộc từ các lớp cha tương ứng của chúng như được mô tả trong cấu trúc phân cấp Component hierarchy.

Để lấy các dependency từ một component, hãy sử dụng annotation @Inject:

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var analytics: AnalyticsAdapter
  ...
}

Lưu ý: Các trường được Hilt đưa vào không thể là private. Cố gắng chèn một trường private với Hilt dẫn đến lỗi biên dịch.

Các class mà Hilt inject có thể có các base class khác cũng sử dụng injection. Các class đó không cần annotation @AndroidEntryPoint nếu chúng là abstract.

Để tìm hiểu thêm về vòng đời gọi lại mà một class Android được đưa vào, hãy xem Component lifetimes.

4. Xác định các ràng buộc Hilt

Để thực hiện injection , Hilt cần biết cách cung cấp các thể hiện của các phụ thuộc cần thiết từ thành phần tương ứng. Một cách để cung cấp thông tin ràng buộc cho Hilt là chèn hàm contructor. Sử dụng annotation @Inject trên phương thức khởi tạo của một class để cho Hilt biết cách cung cấp các thể hiện của class đó:

class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

Lưu ý: Tại thời điểm build, Hilt tạo các component Dagger cho các class. Sau đó, Dagger xem qua mã của bạn và thực hiện các bước sau:

  • Build và xác thực các biểu đồ phụ thuộc, đảm bảo rằng không có phụ thuộc nào không thỏa mãn và không có chu trình phụ thuộc.
  • Tạo các class mà nó sử dụng trong thời gian chạy để tạo các đối tượng thực tế và các phụ thuộc của chúng.

5. Hilt modules

Có một số trường hợp không thể tạo được constructor-inject. Điều này có thể xảy ra vì nhiều lý do. Ví dụ, bạn không thể tạo-chèn một interface. Bạn cũng không thể bổ sung constructor-inject mà bạn không sở hữu,ví dụ như một class từ thư viện bên ngoài. Trong những trường hợp này, bạn có thể cung cấp cho Hilt thông tin ràng buộc bằng cách sử dụng Hilt modules.

Hilt modules là một class có sử dụng annotation @Module. Giống như một modules Dagger, nó cho Hilt biết cách cung cấp các phiên bản của một số loại nhất định. Có phần khác, bạn phải chú thích Hilt modules bằng annotation @InstallIn để cho Hilt biết mỗi modules được sử dụng hoặc cài đặt trong class nào.

Lưu ý: Hilt moduleskhác với Gradle modules. Sự phụ thuộc mà bạn cung cấp trong Hilt modules có sẵn trong tất cả các thành phần được tạo liên kết với class nơi bạn cài đặt Hilt modules.

5.1 Chèn các instances của interface với annotation @Binds

Hãy xem xét ví dụ về AnalyticsService. Nếu AnalyticsService là một interface, thì bạn không thể tạo constructor-inject với nó. Thay vào đó, hãy cung cấp cho Hilt thông tin ràng buộc bằng cách tạo một function trừu tượng được gán annotation @Binds bên trong mô-đun Hilt.

Annotation @Binds cho Hilt biết cách implement nào sẽ sử dụng khi nó cần cung cấp một instance của interface.

Annotated function thông tin sau cho Hilt:

  • Kiểu trả về của function cho Hilt biết instance của interface mà function cung cấp.
  • Tham số function cho Hilt biết việc implement nào cần cung cấp.
interface AnalyticsService {
  fun analyticsMethods()
}

// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

Hilt module AnalyticsModule được gắn annotation @InstallIn(ActivityComponent :: class) vì bạn muốn Hilt đưa sự phụ thuộc đó vào ExampleActivity. Annotation này có nghĩa là tất cả các phần phụ thuộc trong AnalyticsModule đều có sẵn trong tất cả các activity của ứng dụng.

5.2 Inject instances với annotation @Provides

Các interface không phải là trường hợp duy nhất mà bạn không thể tạoconstructor-inject. Cũng không thể chèn constructor-inject nếu bạn không sở hữu class vì nó đến từ thư viện bên ngoài (các lớp như Retrofit, OkHttpClient...) hoặc nếu instance phải được tạo bằng builder pattern.

Hãy xem xét ví dụ trước. Nếu bạn không trực tiếp sở hữu class AnalyticsService, bạn có thể cho Hilt biết cách cung cấp các instance thuộc loại này bằng cách tạo một function bên trong Hilt modules và gán annotation @Provides vào function đó.

Annotated function cung cấp thông tin sau cho Hilt:

  • Kiểu trả về của function cho Hilt biết kiểu mà function cung cấp các instance.
  • Các tham số cho Hilt biết các phụ thuộc của kiểu tương ứng.
  • Phần thân hàm cho Hilt biết cách cung cấp một instance của kiểu tương ứng. Hilt thực thi phần thân hàm mỗi khi nó cần cung cấp một instance của kiểu đó.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    // Potential dependencies of this type
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

5.3 Cung cấp nhiều ràng buộc cho cùng một loại

Trong trường hợp bạn cần Hilt cung cấp các implementation khác nhau của cùng một kiểu làm phụ thuộc, bạn phải cung cấp cho Hilt nhiều ràng buộc. Bạn có thể xác định nhiều ràng buộc cho cùng một loại với các qualifier.

Qualifier là một annotation mà bạn sử dụng để xác định một liên kết cụ thể cho một loại khi loại đó có nhiều liên kết được xác định.

Hãy xem xét ví dụ: nếu bạn cần chặn các cuộc gọi đến AnalyticsService, bạn có thể sử dụng đối tượng OkHttpClient với một trình chặn. Đối với các dịch vụ khác, bạn có thể cần chặn cuộc gọi theo một cách khác. Trong trường hợp đó, bạn cần cho Hilt biết cách cung cấp hai cách implementation khác nhau của OkHttpClient.

Trước tiên, hãy xác định các qualifier mà bạn sẽ sử dụng để chú thích các phương thức @Binds hoặc @Provides:

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

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

Sau đó, Hilt cần biết cách cung cấp một instance của loại tương ứng với mỗi qualifier. Trong trường hợp này, bạn có thể sử dụng Hilt modules với @Provides. Cả hai phương thức đều có cùng kiểu trả về, nhưng các qualifier gắn nhãn chúng là hai liên kết khác nhau:

@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}

Bạn có thể inject loại cụ thể mà bạn cần bằng cách chú thích trường hoặc tham số với qualifier tương ứng:

// As a dependency of another class.
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .client(okHttpClient)
               .build()
               .create(AnalyticsService::class.java)
  }
}

// As a dependency of a constructor-injected class.
class ExampleServiceImpl @Inject constructor(
  @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...

// At field injection.
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {

  @AuthInterceptorOkHttpClient
  @Inject lateinit var okHttpClient: OkHttpClient
}

Phương pháp hay nhất là nếu bạn thêm qualifier vào một loại, hãy thêm qualifier vào tất cả các cách có thể để cung cấp sự phụ thuộc đó. Việc rời khỏi cơ sở hoặc triển khai chung mà không có qualifier là dễ xảy ra lỗi và có thể dẫn đến việc Hilt inject sai phần phụ thuộc.

5.4 Các qualifers có sẵn trong Hilt

Hilt cung cấp một số qualifiers có sẵn. Ví dụ: vì bạn có thể cần class Context từ application hoặc activity, Hilt cung cấp các qualifiers @ApplicationContext@ActivityContext.

Giả sử rằng lớp AnalyticsAdapter từ ví dụ cần context của activity. Đoạn mã sau minh họa cách cung cấp ActivityContext cho AnalyticsAdapter:

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

Để biết thêm các qualifiers có sẵn trong Hilt hãy xem thêm trong Component default bindings.

6. Các component có sẵn trong các class

Đối với mỗi class mà bạn có thể thực hiện inject các trường, có một component được liên kết mà bạn có thể tham khảo trong chú thích @InstallIn. Mỗi component chịu trách nhiệm đưa các inject của nó vào class tương ứng. Các ví dụ trước đã chứng minh việc sử dụng ActivityComponent trong các Hilt modules. Hilt cung cấp các component sau:

Hilt component Injector for
ApplicationComponent Application
ActivityRetainedComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent View annotated with @WithFragmentBindings
ServiceComponent Service

Lưu ý: Hilt không tạo component cho broadcast receivers vì Hilt inject broadcast receivers trực tiếp từ ApplicationComponent.

6.1 Vòng đời Component

Hilt tự động tạo và hủy các instance của các class component thành phần được tạo theo vòng đời của các lớp Android tương ứng.

Generated component Created at Destroyed at
ApplicationComponent Application#onCreate() Application#onDestroy()
ActivityRetainedComponent Activity#onCreate() Activity#onDestroy()
ActivityComponent Activity#onCreate() Activity#onDestroy()
FragmentComponent Fragment#onAttach() Fragment#onDestroy()
ViewComponent View#super() View destroyed
ViewWithFragmentComponent View#super() View destroyed
ServiceComponent Service#onCreate() Service#onDestroy()

Lưu ý: ActivityRetainedComponent tồn tại kể cả khi thay đổi configuration, vì vậy nó được tạo ở Activity#onCreate() đầu tiên và bị hủy ở Activity#onDestroy() cuối cùng.

6.2 Component scopes

Theo mặc định, tất cả các ràng buộc trong Hilt đều không được lưu. Điều này có nghĩa là mỗi khi ứng dụng của bạn yêu cầu ràng buộc, Hilt sẽ tạo một instance mới của loại cần thiết. Trong ví dụ này, mỗi khi Hilt cung cấp AnalyticsAdapter dưới dạng phụ thuộc vào một loại khác hoặc thông qua field injection (như trong ExampleActivity), Hilt sẽ cung cấp một instance mới của AnalyticsAdapter. Tuy nhiên, Hilt cũng cho phép một ràng buộc có scoped đến một component cụ thể. Hilt chỉ tạo liên kết theo scoped một lần cho mỗi instance của component mà liên kết đó được xác định phạm vi và tất cả các yêu cầu cho liên kết đó đều chia sẻ cùng một instance.

Bảng dưới đây liệt kê các scope annotation cho từng scope annotation được tạo:

Android class Generated component Scope
Application ApplicationComponent @Singleton
View Model ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
View annotated with @WithFragmentBindings ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

Trong ví dụ này, nếu bạn phân bổ AnalyticsAdapter cho ActivityComponent bằng @ActivityScoped, thì Hilt sẽ cung cấp cùng một instance AnalyticsAdapter trong suốt vòng đời của activity tương ứng:

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

Lưu ý: Việc xác định phạm vi liên kết đến một component có thể tốn kém vì đối tượng được cung cấp vẫn nằm trong bộ nhớ cho đến khi thành phần đó bị phá hủy. Vì vậy nên giảm thiểu việc sử dụng các ràng buộc theo phạm vi trong ứng dụng của bạn.

6.3 Hệ thống phân cấp các Component

Việc cài đặt một module vào một component cho phép các liên kết của nó được truy cập dưới dạng phụ thuộc của các liên kết khác trong thành phần đó hoặc trong bất kỳ thành phần con nào bên dưới nó trong hệ thống phân cấp thành phần:

6.4 Các ràng buộc mặc định của các component

Mỗi thành phần Hilt đi kèm với một tập hợp các ràng buộc mặc định mà Hilt có thể đưa vào làm phụ thuộc vào các ràng buộc tùy chỉnh của riêng bạn. Lưu ý rằng những ràng buộc này tương ứng với activity và fragment chứ không phải bất kỳ lớp con cụ thể nào. Điều này là do Hilt sử dụng một single activity component để đưa vào tất cả các activity. Mỗi activity có một instance khác nhau của component này.

Android component Default bindings
ApplicationComponent Application
ActivityRetainedComponent Application
ActivityComponent Application, Activity
FragmentComponent Application, Activity, Fragment
ViewComponent Application, Activity, View
ViewWithFragmentComponent Application, Activity, Fragment, View
ServiceComponent Application, Service

7. Inject dependencies trong các class không supported bởi Hilt

Hilt hỗ trợ cho các class phổ biến nhất. Tuy nhiên, bạn có thể cần thực hiện injection trong các lớp mà Hilt không hỗ trợ.

Trong những trường hợp đó, bạn có thể tạo một entry point bằng annotation @EntryPoint. Điểm vào là ranh giới giữa mã được Hilt quản lý và mã không được quản lý. Đây là điểm đầu tiên mã đi vào biểu đồ của các đối tượng mà Hilt quản lý. Các điểm đầu vào cho phép Hilt sử dụng mã mà Hilt không quản lý để cung cấp các dependency trong graph.

Ví dụ: Hilt không hỗ trợ trực tiếp content providers. Nếu bạn muốn content provider sử dụng Hilt để nhận một số phụ thuộc, bạn cần xác định một interface được gán annotation @EntryPoint cho từng loại liên kết mà bạn muốn và bao gồm cácqualifiers. Sau đó, thêm @InstallIn để chỉ định component để cài đặt điểm nhập như sau:

class ExampleContentProvider : ContentProvider() {

  @EntryPoint
  @InstallIn(ApplicationComponent::class)
  interface ExampleContentProviderEntryPoint {
    fun analyticsService(): AnalyticsService
  }

  ...
}

Để truy cập một entry point, hãy sử dụng static method thích hợp từ EntryPointAccessors. Parameters phải là instance của component hoặc một object @AndroidEntryPoint hoạt động như component holder. Đảm bảo rằng component bạn truyền dưới dạng parameter và static method EntryPointAccessors đều khớp với class có annotation @InstallIn trên interface @EntryPoint:

class ExampleContentProvider: ContentProvider() {
    ...

  override fun query(...): Cursor {
    val appContext = context?.applicationContext ?: throw IllegalStateException()
    val hiltEntryPoint =
      EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java)

    val analyticsService = hiltEntryPoint.analyticsService()
    ...
  }
}

Trong ví dụ này, bạn phải sử dụng ApplicationContext để truy xuất entry point vì entry point được cài đặt trong ApplicationComponent. Nếu ràng buộc mà bạn muốn truy xuất nằm trong ActivityComponent, thay vào đó bạn sẽ sử dụng ActivityContext.

8. Hilt và Dagger

Hilt được xây dựng dựa trên Dagger, cung cấp một cách đơn giản để kết hợp Dagger vào một ứng dụng Android. Cùng với với Dagger, mục tiêu của Hilt như sau:

  • Đơn giản hóa cơ sở hạ tầng liên quan đến Dagger cho các ứng dụng Android.
  • Để tạo một tập hợp các component và scope tiêu chuẩn để dễ dàng thiết lập, dễ đọc và chia sẻ mã giữa các ứng dụng.
  • Để cung cấp một cách dễ dàng các ràng buộc khác nhau cho các loại bản build khác nhau.

Vì hệ điều hành Android khởi tạo nhiềuframework class của riêng nó, nên việc sử dụng Dagger trong ứng dụng Android yêu cầu bạn phải viết một lượng đáng kể bản soạn sẵn. Hilt giảm mã viết sẵn liên quan đến việc sử dụng Dagger trong ứng dụng Android. Hilt tự động tạo và cung cấp những thứ sau:

  • Các component để tích hợp các class Android mà với Dagger thì bạn cần phải tạo bằng tay.
  • Scope annotation để sử dụng với các component mà Hilt tạo tự động.
  • Các ràng buộc được xác định trước để đại diện cho các class như Application hoặc Activity.
  • Các qualifiers có sẵn để đại diện cho @ApplicationContext@ActivityContext.

Mã Dagger và Hilt có thể cùng tồn tại trong cùng một ứng dụng. Tuy nhiên, trong hầu hết các trường hợp, tốt nhất là bạn nên sử dụng Hilt để quản lý tất cả việc sử dụng Dagger trên Android. Để migrate một dự án sử dụng Dagger sang Hilt, hãy xem migration guidethe Migrating your Dagger app to Hilt codelab.

9. Bổ sung

  1. Samples: Android Architecture Blueprints - Hilt
  2. Codelabs: Codelabs Using Hilt in your Android app Migrating your Dagger app to Hilt
  3. Blogs Dependency Injection on Android with Hilt Adding components to the Hilt hierarchy Migrating the Google I/O app to Hilt

All Rights Reserved