Dependency injection với Hilt

Tram Ho

Giới thiệu

Vừa qua Google đã giới thiệu một thư viện mới trong Android Jetpack có tên là Hilt. Hilt được xây dựng trên thư viện Dagger, một thư viện khá phổ biến được sử dụng cho Dependency Injection (DI).

Hilt giúp giảm lượng boilerplate code (code mẫu) khi thực hiện manual dependency injection, cũng như giúp cho code của chúng ta dễ dàng trong việc reusability, refactoringtesting.

Trong Android, các thành phần như Activity, Fragment, Service, … được khởi tạo bởi hệ điều hành cho nên chúng ta không thể inject các phụ thuộc trực tiếp từ constructor.

Khi sử dụng Dagger, chúng ta cần một lượng lớn code để làm được điều này. Hilt giúp giảm các boilerplate code liên quan đến sử dụng Dagger trong Android app bằng cách nó sẽ tự động generate và cung cấp:

  • Các component cho việc tích hợp với Android framework classes.
  • Các scope annotation để dùng với các component mà Hilt tạo ra.
  • Các Predefined binding để đại diện cho các Android class như Application hoặc Activity.
  • Các Predefined binding để đại diện cho @ApplicationContext@ActivityContext

Trong bài viết này, mình sẽ giới thiệu với các bạn cách làm thể nào để sử dụng Hilt trong Android app, cũng như demo về mirgate Dagger với Hilt.

Thêm các dependency

Đầu tiên, chúng ta thêm Gradle plugin trong build.gradle (project-level) cho Hilt.

Tiếp theo chúng ta cần apply plugin này và thêm các dependency trong build.gradle (module-level)

Lưu ý rằng, Hilt sử dụng các feature của Java 8, cho nên chúng ta cần enable Java 8 trong project bằng cách thêm vào build.gradle (module level)

Hilt application class

Chúng ta cần annotate Application class với @HiltAndroidApp.

@HiltAndroidAppsẽ tạo các component của Hilt. Các component này sẽ được attach với vòng đời của đối tượng Application và cung cấp các dependency cho nó. Hơn nữa, nó là component chính của của app cho nên các component khác sẽ có thể truy cập được các dependency mà nó cung cấp.

Inject dependencies vào Android class

Với Hilt chúng ta dễ dàng Inject các dependency các Android class bằng cách antotate chúng với annotation @AndroidEntryPoint.

Hilt đang hỗ trợ các Android class như:

  • Application (dùng @HiltAndroidApp)
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

Khi chúng ta annotate một Android class với @AndroidEntryPoint thì các class phụ thuộc vào nó cũng phải được annotate với annotation đó. Ví dụ như SearchFragment được attach vào MainActivity, mặc dù MainActivity không nhận bất kì dependency nào nhưng nó vẫn phải được annotate với @AndroidEntryPoint.

Một vài lưu ý khi sử dụng Hilt với các Android class đó là:

  • Hilt chỉ support các activity kế thừa từ ComponentActivity, như AppCompatActivity.
  • Hilt chỉ supports các fragment kế thừa từ androidx.Fragment.
  • Hilt không support các retained fragment.

Để nhận được các denpendency từ một component chúng ta sẽ dùng annotation @Inject như dưới đây:

Các class mà Hilt inject có thể có các base class cũng dùng injection, nhưng chúng không cần annotate với@AndroidEntryPoint nếu nó là abstract class.

Định nghĩa các Hilt binding

Khi chúng ta thực hiện field injection, Hilt cần biết làm thế nào để cung cấp instance của các denpendency cần thiết từ component tương ứng.

Binding sẽ chứa các thông tin cần thiết để cung cấp một instance của một kiểu như một dependency.

Chúng ta thường sử dụng constructor injection để cung cấp thông tin binding. Như Dagger, chúng ta chỉ cần annotate constructor của class với @Inject.

Các parameter của UserRepository cũng nhận các dependency, cho nên các class như AppExecutors, UserDaoGithubService cũng phải được xác định cách để cung cấp instance của chúng.

Đối với AppExecutors, chúng ta cũng sẽ cung cấp thông tin binding thông qua constructor injection.

Nhưng với UserDaoGithubService chúng ta sẽ cung cấp nó qua một Hilt module.

Hilt modules

Đôi khi, chúng ta không thể dùng constructor injection. Ví dụ như, chúng ta không thể constructor-inject với một interface, hoặc một kiểu hay một class mà chúng ta không định nghĩa (có thể là một external library). Vì thế, với Hilt chúng ta có thể dùng Hilt modules để cung cấp thông tin binding.

Hilt module là một classs với annotation @Module. Giống như Dagger module, chúng ta có thể định nghĩa một dependency bằng annotation @Provides hoặc Binds.

Tuy nhiên, Hilt cần chỉ rõ Android class mà mỗi module được dùng hoặc install in với annotation @InstallIn. Chúng ta sẽ tìm hiểu thêm về @InstallIn trong component scopelifetime.

Binds

Trong ví dụ trên chúng ta có AnalyticsService interface, được implement bởi AnalyticsServiceImpl. Bây giờ, để cung cấp một instance của AnalyticsService chúng ta cần thông qua AnalyticsServiceImpl, cho nên AnalyticsServiceImpl sẽ có một constructor-injected.

Với AnalyticsModule, chúng ta có function bindAnalyticsService(_:AnalyticsServiceImpl) sẽ nhận một AnalyticsServiceImpl và trả về AnalyticsService. Khi nó được annotate với @Binds, nó sẽ nhận instance từ AnalyticsServiceImpl thông qua constructor đã được binding trước đó và ép kiểu thành AnalyticsService để cung cấp cho các component cần dependency này.

@Provides

Chúng ta thường xuyên sử dụng các external library (như Retrofit, OkHttpClient, Room databases, …) trong Android app. Với những thư viện này, chúng ta không thể khai báo một constructor-injected cho nó, cho nên để cung cấp thông tin binding, chúng ta sẽ cần đến annotation @Provides.

Với ví dụ này, chúng ta cần cung cấp một GithubService instance thông qua Retrofit.Builder. Trong AppModule sẽ có function provideGithubService(), sẽ trả về một GithubService. Khi được annotate với @Provides nó sẽ cung cấp instance được trả về từ function này cho các components cần nó.

Khi function hoặc constructor được annotate với @Singleton, thì module hoặc constructor injection sẽ cung cấp một singleton instance cho các component cần denpendency đó.

Provide multiple binding cho các kiểu giống nhau

Trong trường hợp, chúng ta cần cung cấp nhiều dependency có cùng một kiểu thì chúng ta có thể sử dụng qualifiers trong Hilt.

Qualifier là một annotation được dùng đễ nhận dạng cho từng binding cụ thể.

Ví dụ, mình cần cung cấp 2 instance của OkHttpClient trong NetworkModule, với instance thứ nhất sẽ có auth interceptor và instance thứ hai sẽ có một interceptor khác.

Instance thứ nhất sẽ được sử dụng bởi AnalyticsModule cho provideAnalyticsService(_:OkHttpClient)

Lúc này, provideAnalyticsService() sẽ không thể biết được, mình nên nhận instance từ provideAuthInterceptorOkHttpClient() hay từ provideOtherInterceptorOkHttpClient(), do đó sẽ phát sinh lỗi khi thực hiện compile.

Vậy, để xác định được denpendency nào được inject vào provideAnalyticsService(), chúng ta cần có 2 @Qualifier cho từng OkHttpClient được cung cấp.

@Retention(AnnotationRetention.BINARY) xác định rằng annotation này được lưu trữu trên binary output, nhưng invisible với reflection. Xem thêm về Retention.

Sau đó, AnalyticsModuleAppModule sẽ được định nghĩa lại như thế này:

Trong trường hợp, nó là một dependency của constructor-injected class hoặc field injection, chúng ta có thể dùng như sau:

Predefined qualifers

Trong Android app chúng ta thường xuyên cần đến Context class cho việc truy cập resource, content-provider, khởi tạo database, và nhiều thứ khác.

Hilt cung cấp 2 qualifers đó là @ApplicationContext@ActivityContext để làm điều này.

Inject ViewModel objects với Hilt

Hilt cung cấp một cách dễ dàng để inject vào ViewModel bằng annotation @ViewModelInject.

Để sử dụng nó, chúng ta sẽ cần thêm một vài dependencies trong build.gradle (module-level)

Tiếp theo đó là cung cấp một ViewModel với construtor có @ViewModelInject annotation.

Bây giờ, userRepositoryrepoRepository sẽ được inject vào UserViewModel khi nó được khởi tạo.

Cuối cùng, activity hoặc fragment có thể lấy instance ViewModel thông qua việc sử dụng ViewModelProvider hoặc by viewModels() trong KTX extensions:

Vậy là chúng ta đã đi qua cơ bản cách để có thể sử dụng Hilt trong Android app của mình. Nếu bạn là người đã dùng Dagger 2, thì có thể tham khảo pull request migrate Dagger sang Hilt của mình, mình đã implement dựa trên project GithubBrowserSample trong architecture-components-samples của Google.

Source đầy đủ bạn có thể tham khảo tại đây.

Lưu ý là branch githubbrowserdaggertohilt nhé 😄

Tiếp theo, chúng ta sẽ tìm hiểu thêm về lifetimescope của component trong Hilt.

Các component đã được tạo cho Android class

Mỗi Hilt component có trách nhiệm inject binding của nó vào Android class tương ứng. Một Android class sẽ liên kết với Hilt component thông qua @InstallIn annotation.

Trong ví dụ trước, chúng ta đã sử dụng ActivityComponent. Ngoài ra, Hilt còn cung cấp mốt số component khác như:

Hilt componentInjector for
ApplicationComponentApplication
ActivityRetainedComponentViewModel
ActivityComponentActivity
FragmentComponentFragment
ViewComponentView
ViewWithFragmentComponentView annotated with @WithFragmentBindings
ServiceComponentService

Hilt không generate component cho broadcast receivers vì Hilt inject các broadcast receivers trực tiếp từ ApplicationComponent.

Component lifetimes

Hilt sẽ tự động tạo và hủy các instance của các component class theo vòng đời của Android class tương ứng.

Generated componentCreated atDestroyed at
ApplicationComponentApplication#onCreate()Application#onDestroy()
ActivityRetainedComponentActivity#onCreate()Activity#onDestroy()
ActivityComponentActivity#onCreate()Activity#onDestroy()
FragmentComponentFragment#onAttach()Fragment#onDestroy()
ViewComponentView#super()View destroyed
ViewWithFragmentComponentView#super()View destroyed
ServiceComponentService#onCreate()Service#onDestroy()

ActivityRetainedComponent sẽ tồn tại qua configuration changes.

Component scopes

Mặc định, tất cả các binding trong Hilt đều là unscoped, có nghĩa là mỗi lần app request binding thì Hilt sẽ tạo một instance mới.

Với scope thì Hilt chỉ tạo một instance và chia sẻ chúng với các request khác trong cùng một scope.

Dưới đây là các scope với Android class và component tương ứng:

Android classGenerated componentScope
ApplicationApplicationComponent@Singleton
View ModelActivityRetainedComponent@ActivityRetainedScope
ActivityActivityComponent@ActivityScoped
FragmentFragmentComponent@FragmentScoped
ViewViewComponent@ViewScoped
View annotated with @WithFragmentBindingsViewWithFragmentComponent@ViewScoped
ServiceServiceComponent@ServiceScoped

Ví dụ, chúng ta có AnalyticsAdapter install in ActivityComponent, sử dụng @ActivityScoped. Hilt sẽ provide một instance AnalyticsAdapter suốt vòng đời của activity tương ứng.

Bây giờ, giả sử chúng ta có AnalyticsService được sử dụng không chỉ trong một activity mà còn ở mọi nơi trong app. Lúc này phạm vi của AnalyticsService phù hợp với ApplicationComponent. Kết quả là, mỗi khi một component cần provide một instance của AnalyticsService, nó sẽ cung cấp cùng 1 instance trong mọi thời điểm.

“Túm” lại

Mình đã giới thiệu với các bạn nội dung cơ bản về Hilt và cách để implement nó trong Android app. Nếu có bất kì vấn đề gì cần trao đổi, hãy để lại comment của bạn phía dưới nhé.

Đây là DemoMigrate Dagger to Hilt pull request của mình.

Thank you and Happy coding =))

Tham khảo

  1. https://developer.android.com/training/dependency-injection/hilt-android
  2. https://medium.com/androiddevelopers/whats-new-in-jetpack-1891d205e136
  3. https://github.com/android/architecture-components-samples/tree/master/GithubBrowserSample
Chia sẻ bài viết ngay

Nguồn bài viết : Viblo