Hilt using in android library

Viewed 5298

I would like to try Hilt DI in the android library.

It is a dependency on another project, with its own submodule. The very first problem I've encountered is the requirement of marking Application with @HiltAndroidApp. Now I do not have anything that extends Application in my library ofc but would like to utilize Hilt and its predefined components.

Is it possible or should I go with Dagger only in such a case? I've found a solution for Dagger, where library dependency injection is made totally independently (the client is unaware of the library's DI): Dagger solution, would love to hear any opinion on that, maybe someone already put a great effort into that issue and can share his insights.

3 Answers

If you're trying to include Hilt in an android library, then you should expect the android app (client of your library) to mark its Application with @HiltAndroidApp.

You should include your whole setup (entry points, modules, dependencies, ... whatever you want to have in your library) in the library module, and make the requirement for the client of the library to use the @HiltAndroidApp to use your library correctly.

You don't need to include @HiltAndroidApp in library module to inject the dependencies in library modules to app module or any dynamic feature modules.

This sample has only core library module, app, and dynamic feature modules. Dynamic feature module implementation is optional.

Result of injecting from core library module to App's Activity and Fragment is as

    Project dependency Structure
 feature_hilt_camera    feature_hilt_photos  (Dynamic Feature Modules)
        |         |          |
        |         ----App----
        |              |
        core(android-library)

enter image description here

In core library module have a dagger module as

@InstallIn(ApplicationComponent::class)
@Module
class CoreModule {

    @Singleton
    @Provides
    fun provideCoreDependency(application: Application) = CoreDependency(application)

    @Provides
    fun provideCoreActivityDependency(context: Application) = CoreActivityDependency(context)

    @Provides
    fun provideCoreCameraDependency(): CoreCameraDependency = CoreCameraDependency()

    @Provides
    fun provideCorePhotoDependency(): CorePhotoDependency = CorePhotoDependency()

    @Provides
    fun provideAnotherDependency() = AnotherDependency()
}

And inject to Activity as

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    /**
     * Injected from [CoreModule] with @Singleton scope
     */
    @Inject
    lateinit var coreDependency: CoreDependency

    /**
     * Injected from [CoreModule] with no scope
     */
    @Inject
    lateinit var coreActivityDependency: CoreActivityDependency

    /**
     * Injected from [MainActivityModule] with no scope
     */
    @Inject
    lateinit var toastMaker: ToastMaker

    /**
     *
     * Injected from [MainActivityModule] with @ActivityScoped
     * * To inject this there should be @Binds that gets Context from an Application
     */
    @Inject
    lateinit var mainActivityObject: MainActivityObject

    /**
     * Injected via constructor injection with no scope
     */
    @Inject
    lateinit var sensorController: SensorController

    /**
     * Injected via constructor injection with @Singleton scope
     *
     * ### Unlike Tutorial 9-2 This can be injected because MainActivity's component does not
     * depend on any component with another scope
     */
    @Inject
    lateinit var singletonObject: SingletonObject

    @Inject
    lateinit var anotherDependency: AnotherDependency

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<TextView>(R.id.tvInfo).text =
                "CoreModule @Singleton coreDependency: ${coreDependency.hashCode()}\n" +
                        "CoreModule no scope coreActivityDependency: ${coreActivityDependency.hashCode()}\n" +
                        "CoreModule no scope anotherDependency: ${anotherDependency.hashCode()}\n" +
                        "MainActivityModule @ActivityScoped mainActivityObject: ${mainActivityObject.hashCode()}\n" +
                        "MainActivityModule no scope toastMaker: ${toastMaker.hashCode()}\n" +
                        "Constructor no scope sensorController: ${sensorController.hashCode()}\n"
        "Constructor @Singleton singletonObject: ${singletonObject.hashCode()}"


    }
}

and it's same for HomeFragment which is in app module

@AndroidEntryPoint
class HomeFragment : Fragment() {


    /**
     * Injected from [CoreModule] with @Singleton scope
     */
    @Inject
    lateinit var coreDependency: CoreDependency

    /**
     * Injected from [CoreModule] with no scope
     */
    @Inject
    lateinit var coreActivityDependency: CoreActivityDependency

    @Inject
    lateinit var homeFragmentObject: HomeFragmentObject

    /**
     * This dependency cannot be injected since this fragment's component does not depend on CoreComponent
     * unlike Tutorial 9-2 counterpart
     */
    @Inject
    lateinit var mainActivityObject: MainActivityObject

    @Inject
    lateinit var fragmentObject: FragmentObject
}

If you also wish to inject to dynamic feature modules you need a provision module in your library module as

/**
 * This component is required for adding component to DFM dependencies
 */
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface CoreModuleDependencies {

    /*
         Provision methods to provide dependencies to components that depend on this component
     */
    fun coreDependency(): CoreDependency

    fun coreActivityDependency(): CoreActivityDependency

    fun coreCameraDependency(): CoreCameraDependency

    fun corePhotoDependency(): CorePhotoDependency

}

and dynamic feature module you will use this interface as dependent component

In camera dynamic feature module have a component like this

@Component(
        dependencies = [CoreModuleDependencies::class],
        modules = [CameraModule::class]
)
interface CameraComponent {

    fun inject(cameraFragment1: CameraFragment1)
    fun inject(cameraFragment2: CameraFragment2)


    fun inject(cameraActivity: CameraActivity)

    @Component.Factory
    interface Factory {
        fun create(coreComponentDependencies: CoreModuleDependencies,
                   @BindsInstance application: Application): CameraComponent
    }

}

and inject it to your dynamic feature fragment with

private fun initCoreDependentInjection() {

    val coreModuleDependencies = EntryPointAccessors.fromApplication(
            requireActivity().applicationContext,
            CoreModuleDependencies::class.java
    )

    DaggerCameraComponent.factory().create(
            coreModuleDependencies,
            requireActivity().application
    )
            .inject(this)
}

Full sample that in image is here, and you check out implementation for both libraries and dynamic feature modules in this sample project.

It's possible to integrate Hilt into your library, but you will have to handle the case where the app is not a Hilt application. You can handle this case by annotating your Activity/Fragment with @OptionalInject and then checking OptionalInjectCheck#wasInjectedByHilt() to check if the Activity/Fragment was injected by Hilt or not.

@OptionalInject
@AndroidEntryPoint
public final class MyFragment extends Fragment {
  ...
  @Override public void onAttach(Activity activity) {
    super.onAttach(activity);  // Injection will happen here, but only if the Activity used Hilt
    if (!OptionalInjectCheck.wasInjectedByHilt(this)) {
      // Get Dagger components the previous way and inject manually
    }
  }
}

Note that doing this will not make your library simpler (it'll actually get more complex since you need to support both Hilt and non-Hilt applications). The main benefit would be to your clients that use Hilt, since they wouldn't need to do any component/module setup to get your library up and running in their app.

Related