24

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.

Rahul
  • 3,293
  • 2
  • 31
  • 43
kolboc
  • 805
  • 1
  • 8
  • 22

3 Answers3

10

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.

Bartek Lipinski
  • 30,698
  • 10
  • 94
  • 132
  • 4
    That's what I expected, I thought maybe I've missed something... It's pretty disappointing then, as it's quite a significant limitation comparing to pure Dagger. – kolboc Aug 05 '20 at 09:59
  • Well, if you want to **enclose** the whole dependency graph within the library and you don't want to expose that outside, then Dagger should be your choice of library. I wouldn't say it's a limitation of Hilt. Its target use-case is just different from yours. – Bartek Lipinski Aug 05 '20 at 10:02
  • How do you make Dagger work in the library? Any references? – Viktor Vostrikov May 19 '21 at 21:07
  • 1
    @BartekLipinski That's very sad.. is there any way to use DI for library and the client side doesn't need to implement anything for DI? – c-an Nov 08 '21 at 07:23
  • @ViktorVostrikov A good example of Dagger in a library is ZenDesk https://github.com/zendesk/connect-android-sdk/blob/master/ConnectSDK/src/main/java/com/zendesk/connect/Connect.java They build the component on initialization, and use that component to drive sdk functionality. It's worth noting that you can do the same with a Hilt setup, I have before, but it was for an app that used Hilt, and a complimentary SDK that used the same modules and dagger components. It adds unnecessary overhead to get view models to resolve and I wish we had just used traditional dagger from the beginning – em_ Jul 08 '23 at 23:44
5

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.

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • 7
    The above example still uses `@HiltAndroidApp` in-app. Is there any way to use hilt without `@HiltAndroidApp` and implementation of hilt in-app.module. Like my android library use like separately. User can get it from consumption by implementation in Gradle with help of transitive depdency. – Arul Apr 16 '21 at 07:08
  • 4
    Is there any solution that we make library with Hilt and the client company doesn't need to use 'hilt' in their app project? – c-an Nov 08 '21 at 07:15
0

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.

Serhii Hrabas
  • 401
  • 1
  • 5
  • 12