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

Viewed 5311

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?

4 Answers

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().

Related