Hilt Testing - Replace internal Hilt-Module in separate Android multi-module app - android

I have an Android app where the codebase is split into 2 different modules: App and Domain
Goal: I am attempting to use the Hilt provided testing functionality to replace a Domain internal dependency when creating App tests.
In Domain, I have an internal interface with an internal implementation, like below:
internal interface Database {
fun add(value: String)
}
internal class DatabaseImpl #Inject constructor() : Database {
override fun add(value: String) { ... }
}
The above guarantees that the Database can only be used inside Domain and cannot be accessed from elsewhere.
In Domain I have another interface (which is not internal), for use in App, with an internal implementation, like below:
interface LoginService {
fun userLogin(username: String, password: String)
}
internal class LoginServiceImpl #Inject constructor(database: Database) {
override fun userLogin(username: String, password: String) {
// Does something with the Database in here
}
}
In Domain I use Hilt to provide dependencies to App, like below:
#Module(includes = [InternalDomainModule::class]) // <- Important. Allows Hilt access to the dependencies provided by InternalDomainModule.
#InstallIn(SingletonComponent::class)
class DomainModule {
...
}
#Module
#InstallIn(SingletonComponent::class)
internal class InternalDomainModule {
#Provides
#Singleton
fun provideDatabase() : Database = DatabaseImpl()
#Provides
#Singleton
fun provideLoginService(database: Database) : LoginService = LoginServiceImpl(database)
}
This all works perfectly in isolating my implementations and only exposes a single interface outside of Domain.
However, when I need to provide fake implementations inside App using the Hilt guidelines, I am unable to replace the LoginService as I do not have access to InternalDomainModule (because it is internal to Domain only) and replacing DomainModule does not replace LoginService (as it is provided in another Hilt module, namely InternalDomainModule), like below:
#Module
#TestInstallIn(
components = [SingletonComponent::class],
replaces = [DomainModule::class] // [InternalDomainModule::class] is impossible as inaccessible in **App**
)
class FakeModule {
#Provides
#Singleton
fun provideFakeLoginService() : LoginService = FakeLoginServiceImpl() <- Something fake
}
The above leads to only DomainModule being replaced, not InternalDomainModule, which leads to LoginService being provided twice, which makes Hilt unhappy.
Making things not internal to Domain fixes the issue, but defeats the purpose of having a multi-module Android app with clear separations.

Related

Android + Hilt: Dependency cycle injecting a repo into a service which is going to be used by an activity

I have a Service (which is going to be used in an activity) that needs a Repository to be injected in it. I tried providing it with constructor injection (the preferred way), but I was not able to do it like this because I also need a module with a #Provides for the Service itself, who is going to be used in an activity through field injection (because as far as I know I cannot use constructor injection in Activities), and if I do constructor injection for the repo then, when I need to create the #Provides for the service:
#Provides
#Singleton
fun provideMyService(): MyService {
return MyService()
}
It complains about missing constructor parameters for MyService and, if I do:
#Provides
#Singleton
fun provideMyService(myService: MyService): MyService {
return myService
}
It complains about a "Dependency cycle".
In resume, my activity uses a service (located in one module of my app) and the service needs the repo (located in another module), and I need to find the correct dependency injection way for the whole "chain" to work.
For now, activity gets Service injected -through field injection- > Service gets Repository injected -through an EntryPoint-
One of the. things I tried is a module with the next piece of code:
#Provides
#Singleton
fun provideTrackRepository(trackRepository: TrackRepository): TrackRepository {
return trackRepository
}
#EntryPoint
#InstallIn(SingletonComponent::class)
interface TrackRepositoryInterface {
fun getTrackRepository(): TrackRepository
}
And when building I get the following message:
/myApp/app/build/generated/hilt/component_sources/debug/com/myApp/myApp/app/AWApplication_HiltComponents.java:141:
error: [Dagger/DependencyCycle] Found a dependency cycle: public
abstract static class SingletonC implements
com.myApp.repository.local.Dependencies.TrackRepositoryInterface,
^
com.myApp.repository.local.TrackRepository is injected at
com.myApp.repository.local.Dependencies.provideTrackRepository(trackRepository)
com.myApp.repository.local.TrackRepository is injected at
com.myApp.repository.local.Dependencies.provideTrackRepository(trackRepository)
The cycle is requested via:
com.myApp.repository.local.TrackRepository is requested at
com.myApp.repository.local.Dependencies.TrackRepositoryInterface.getTrackRepository()
But if I do:
#Provides
#Singleton
fun provideTrackRepository(): TrackRepository {
return TrackRepository()
}
#EntryPoint
#InstallIn(SingletonComponent::class)
interface TrackRepositoryInterface {
fun getTrackRepository(): TrackRepository
}
It builds and works correctly, but I'm not sure if this second way is a correct way for "dependency injection class providing" or not.
Some people is redirecting me to
https://developer.android.com/training/dependency-injection/hilt-android#android-classes
But I've read this "poor" and "not very clear" -in my opinion- documentation several times, and I'm still stuck.
They're trying to sell Hilt as a very easy dependency injection library, but IMHO working on a multi-module app with Hilt is a true nightmare.

Dagger hilt: Difference between annotating a class #Singleton and a provides function #Singleton

My question is pretty simple and straightforward: What is the difference between the two annotations / examples:
Example one
#Singleton
class MySingletonClass() {}
#Module
#InstallIn(FragmentComponent::class)
abstract class MyFragmentModule {
#Provides
fun provideMySingletonClass() = MySingletonClass()
}
Eaxmple two
class MySingletonClass() {}
#Module
#InstallIn(FragmentComponent::class)
abstract class MyFragmentModule {
#Singleton
#Provides
fun provideMySingletonClass() = MySingletonClass()
}
The only difference I know is, that the second example gives me the following error:
error: [Dagger/IncompatiblyScopedBindings] FragmentC scoped with #dagger.hilt.android.scopes.FragmentScoped may not reference bindings with different scopes:
Does that mean, that the #Singleton annotation in example one is simply ignored?
In Example One, your #Singleton annotation is ignored, but only because you are calling the constructor yourself in your #Provides method. Because Dagger doesn't interact with your MySingletonClass constructor, it cannot read or use the annotation.
If your #Singleton class MySingletonClass had an #Inject constructor—even an empty one—then Dagger would be able to interact with it directly as long as you also delete the #Provides fun that would override the constructor detection. Once you've done that, the behavior of #Singleton would be the same in either syntax.
Regarding the error message "error: [Dagger/IncompatiblyScopedBindings] XXX scoped with #YYY may not reference bindings with different scopes": #Andrew The real problem here is that in Example Two you're trying to declare a #Singleton binding in a Module that you install in your FragmentComponent. #Singleton bindings can only happen in a #Singleton component, which in Hilt is SingletonComponent. I don't remember for sure, but I think your Example One (with the edits I described) would work with singleton behavior and without an error, because Dagger would automatically select the appropriate component in the hierarchy to install your MySingletonClass.

Android dagger how to hide generated factory classes from consuming module

Is there a way to hide dagger generated factory classes from consuming app?
I have a library module with its own component and consuming app obvisouly can see all the dagger generated class(e.g. LibraryData_factory.java) from the library module. But I just want to provide some public class to the consuming app and hide the rest of them, for example:
internal interface Person
internal class Adult #Inject constructor(): Person
internal class Car(val driver: Person): Vehicle
#Module
object LibraryModule {
#Provides fun provideCar(person: Person): Vehicle {
return Car(person)
}
}
How can I hide the generated Adult_factory class from consuming app?

How to pass in parameters to a dagger module from a activity or fragment at runtime

My software specifications are as follows:
Android Studio 3.4
dagger-android 2.16
I have the following class that passes a MapboxGeocoder that will execute and return a response.
class GeocodingImp(private val mapboxGeocoder: MapboxGeocoder) : Geocoding {
override fun getCoordinates(address: String, criteria: String): AddressCoordinate {
val response = mapboxGeocoder.execute()
return if(response.isSuccess && !response.body().features.isEmpty()) {
AddressCoordinate(
response.body().features[0].latitude,
response.body().features[0].longitude)
}
else {
AddressCoordinate(0.0, 0.0)
}
}
}
However, the MapboxGeocoder is generated in a dagger module at compile time. So I have to specify the string for the address and TYPE_ADDRESS.
#Reusable
#Named("address")
#Provides
fun provideAddress(): String = "the address to get coordinates from"
#Reusable
#Provides
#Named("geocoder_criteria")
fun provideGeocoderCriteria(): String = GeocoderCriteria.TYPE_ADDRESS
#Reusable
#Provides
fun provideMapboxGeocoder(#Named("address") address: String, #Named("geocoder_criteria") geocoderCriteria: String): MapboxGeocoder =
MapboxGeocoder.Builder()
.setAccessToken("api token")
.setLocation(address)
.setType(geocoderCriteria)
.build()
#Reusable
#Provides
fun provideGeocoding(mapboxGeocoder: MapboxGeocoder): Geocoding =
GeocodingImp(mapboxGeocoder)
my component class:
interface TMDispatchMobileUIComponent {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: TMDispatchMobileUIApplication): Builder
fun build(): TMDispatchMobileUIComponent
}
fun inject(application: TMDispatchMobileUIApplication)
}
In the main activity I would use this like this as the user can enter in a different address or change the criteria to something else. But as the module are compiled I cannot pass any parameters to them at runtime:
presenter.getAddressCoordinates("this should be the actual address", GeocoderCriteria.TYPE_ADDRESS)
For my injection into the Activity I use the following:
AndroidInjection.inject(this)
Is there any solution to this problem?
The problem you have can be solved using "Assisted injection" approach.
It means that you need a class to be built both using dependencies provided from the existing scopes and some dependencies from the instance's creator, in this case, your main activity. Guice from Google has a nice description of what it is and why it is needed
Unfortunately, Dagger 2 does not have this feature out from the box. However, Jake Wharton is working on a separate library that can be attached to Dagger. Moreover, you can find more details in his talk on Droidcon London 2018, where he dedicated a whole talk section for this question:
https://jakewharton.com/helping-dagger-help-you/
You can recreate your whole component at runtime if you wish, where you'd then pass in the parameters to your module as a constructor parameter. Something like:
fun changeAddress(address: String) {
val component = DaggerAppComponent.builder() //Assign this to wherever we want to keep a handle on the component
.geoModule(GeoModule(address))
.build()
component.inject(this) //To reinject dependencies
}
And your module would look like:
#Module
class AppModule(private val address: String) {...}
This method may be wasteful though, if you're creating many different objects in your component.
A different approach compared to the already given answers would be to get a "Factory" via dagger dependency injection called GeoModelFactory which can create new instances of GeoModel for you.
You can pass the address and type to the factory which creates your instance. For optimization you can either store references for all different address/types that have already been requested (can result in a memory leak if there are a lot of different ones if old ones are not removed) or it could also be enough if you store only the latest instance and in other parts of the code to simply ask the factory to provide you with the GeoModel that has been created last.
The MapboxGeocoder are dynamically constructed at runtime, in this case, dagger doesn't help much as its objective is to help you construct the object graph at compile time like you hand write the code.
So in my opinion, you should create a MapboxGeocoder inside getCoordinates().

Using dependencies from multiple modules dagger 2.11 android

Hi I have two modules seperated in domain packages in my project and have firebase insance service class that injects itself manually using DaggerAppComponent.
Then I have two inject dependencies located on the two modules I mentioned.
ModuleOne has the dependency called storage and ModuleTwo has one called delegator.
When atempting to inject both of these inside my firebase service class, it complains that it can't locate and find the delegator injector from ModuleTwo.
However, if I copy the provides method interface of the delegator into ModuleOne it sort of works(now complains that its bound multiple times but can easily fix that by adding naming convention).
This is a hacky way of doing it which I am not keen to do and just want the ability to use any dependencies from different modules. Is this possible?
Below is my firebase service class
class MyInstanceIdService : FirebaseInstanceIdService() {
#Inject
lateinit var delegator: AccountDelegatorContract
#Inject
lateinit var Storage: StorageContract
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onTokenRefresh() {
DaggerApplicationComponent.builder().application(application as MyApplication).build().inject(this)
val refreshToken = FirebaseInstanceId.getInstance().token
// val pianoStorage: PianoStorage = PianoStorage(this)
sendTokenToServer(refreshToken, storage.getUniqueId())
}
private fun sendTokenToServer(refreshToken: String?, uniqueId: String) {
//TODO: Send this new token to server
delegator.sendPushToken(PushTokenRequest(refreshToken!!, uniqueId))
}
Here is moduleOne which represents the main Module that houses dependencies that are used in multiple domain packages in my application.
#Module
abstract class MainModule {
#ContributesAndroidInjector(modules = [AccountModule::class])
#ViewScope
abstract fun bindInstanceIdService(): MyInstanceIdService
#Provides
#Singleton
#JvmStatic
fun provideApplicationContext(application: Application): Context = application
#Provides
#Singleton
#JvmStatic
fun provideStorageData(storage: MainStorage): StorageContract = storage
}
Here is ModuleTwo that is specific to a domain packages
#Module
abstract class AccountModule {
#Module
companion object {
#ViewScope
#JvmStatic
#Provides
fun providesAccountDelegator(accountDelegate: AccountDelegator): AccountDelegatorContract = accountDelegate
#ViewScope
#JvmStatic
#Provides
fun providesAccountControllerContract(accountNetworkController: AccountNetworkController): AccountControllerContract = accountNetworkController
}
}
My app is organised in different packages that represent a part/domain of the app such as accounts, users, vehicle, message etc etc and with each domain has its own module which defines specific dependencies related to that domain.
My issue is that how can I use dependencies above located on different modules?
Edit: my appCOmponent looks like this
#Singleton
#Component(modules = arrayOf(MainModule::class,
AccountModule ::class))
interface ApplicationComponent : AndroidInjector<DaggerApplication> {
fun inject(MyApplication: MyApplication)
fun inject(myInsanceIdService: MyInstanceIdService)
override fun inject(instance: DaggerApplication)
#Component.Builder
interface Builder {
#BindsInstance
fun application(applicaton: Application): Builder
fun build(): ApplicationComponent
}
}
You add you AccountModule (with provision methods of ViewScope sope) to your AppComponent as well as your subcomponent (declared with #ContributesAndroidInjector bindInstanceIdService), so I wonder how/why this compiles at all. So first of all you should remove the AccountModule from your app component, since it has a different scope anyways and won't do you any good there.
Secondly, you call DaggerApplicationComponent.inject(this) in your MyInstanceIdService, although ApplicationComponent does not include a inject(service : MyInstanceIdService) method. Again, I don't see how/why this would compile. As already mentioned above, you registered a subcomponent (#ContributesAndroidInjector bindInstanceIdService) to inject your service, so you should use it. Your app component does not even have the correct scope to provide all of the objects.
Even worse, you create another AppComponent in your MyInstanceIdService, so even if this would work, you now have 2 singleton components providing 2 different instances of your "singleton" objects. Don't recreate components out of their scope. Use MyApplication.getAppComponent() and use that one!
So after removing the AccountModule from the AppComponent you should make your Application implement the HasServiceInjector, injecting/returning the dispatching injector like you do for your activities. Then inject your service by using AndroidInjection.inject(this).
To sum up:
Check your scopes. A #Singleton component can't provide #ViewScope things.
Don't register modules with different scopes on components, they can't use it anyways
Don't inject an object with a component of a different scope than you need
Don't recreate components when creating subcomponents. Use the existing one!
If you use dagger.android then don't create the components yourself, but implement the correct interface and use AndroidInjection.inject()

Categories

Resources