Dagger: Using Android injector with SDK-level-dependent components.

Created on 6 Feb 2018  Â·  18Comments  Â·  Source: google/dagger

I have a subclass of TileService (an android.app.Service that is only available on 24+).
I want to use the Android injector and @ContributesAndroidInjector abstract MyTileService service().
This creates a service key of MyTileService.class in the generated module, ultimately resulting in the following as a field in my component.

this.mapOfClassOfAndProviderOfFactoryOfProvider =
        MapProviderFactory
            .<Class<? extends Service>, AndroidInjector.Factory<? extends Service>>builder(1)
            .put(MyTileService.class, bindAndroidInjectorFactoryProvider1)
            .build();

On Android versions <24, the reference to the class file tries to load the Android framework class (TileService) that doesn't exist, crashing with a NoClassDefFoundError.

Can Dagger-Android do any clever lazy tricks to avoid this?

Most helpful comment

For those who came across this issue. The solution was introduced in above commit.

You need to put the following argument to enable it. This will use Strings instead of Class references (which then fixes the mentioned crash). -Adagger.android.experimentalUseStringKeys

On Android java-only projects, you do

android {
    defaultConfig {
        javaCompileOptions.annotationProcessorOptions {
            arguments['dagger.android.experimentalUseStringKeys'] = 'true'
        }
    }
}

For Kotlin projects with kapt you would do

kapt {
  arguments {
    arg('dagger.android.experimentalUseStringKeys')
  }
}

Note: More explanation on how this works and its effects on Proguard/R8 is available here: https://github.com/google/dagger/blob/3a089646fb70fb5a193a731c4909d6ccaeeaadd3/java/dagger/android/processor/AndroidProcessor.java#L44-L52

Note 2: Keep in mind that enabling this also breaks incremental annotation processor support on Dagger 5.+

All 18 comments

MyTileService would be unavailable even if you weren't using Dagger. What would your application do in that case? Would it simply not start up that service?

Correct. The Android framework is the only thing that starts MyTileService.

But in a non-Dagger application, how would your application tell Android to start up that service? The manifest file? Or else, would some Java code look up the API version and conditionally start up the service?

In the case of a TileService, you register it in your manifest (with the intent filer action android.service.quicksettings.action.QS_TILE), and only Android starts it when a user adds the tile from the settings app, clicks on the tile in the quick settings drawer, etc.

So would this bug go away if we used something other than class literals as the map key?

@netdpb The problem is that even MyTileService is not started, the app will crash when the object graph is being created with the following code :

DaggerApplicationComponent.builder().application(this).build().inject(this);

The inject will trigger the crash.

So would this bug go away if we used something other than class literals as the map key?

Yes. I can't reference MyTileService.class in the field in my component. Using the FQCN would fix this.

(Note that Ron mentioned that there are concerns about not using the class reference because ProGuard will mangle the names of Fragments. I personally don't use ProGuard or Fragments, much less both with Dagger-Android.)

Before the fix is done in Dagger, might I ask whether there is any way of fix this issue from consumer side?

The problem is that we create a Map (or a MapFactory) with Class literals as the keys - even if it's never injected, creating the map is a problem.

There are a few options - one is to release separate APKs, one of which doesn't include MyTileService
at all (or, at the very least, the @ContributesAndroidInjector method) if the API level isn't high enough.

Another would be something like this:

@Subcomponent(module = ModuleThatIncludes_ContributesAndroidInjector_For_MyTileService.class)
interface Api24OrGreaterServiceComponent {
  DispatchingAndroidInjector<Service> injector();
}

@Component
interface YourRootComponent {
  // ...
  Api24OrGreaterServiceComponent api24OrGreaterServiceComponent();
}

public class YourApplication extends DaggerApplication {
  @Inject YourRootComponent component;

  public AndroidInjector<Service> api24OrGreaterServiceInjector() {
    return component.api24OrGreaterServiceComponent().injector();
  }
}

You may want to define an interface similar to HasServiceInjector and create a parallel to `AndroidInjector.inject(Service) for that as well, depending on how your app is structured.

Basically what this is doing is creating a subcomponent to sit in between the root component and the generated subcomponent for the @ContributesAndroidInjector MyTileService method so that the class literal is never invoked unless you reach the code path that needs it. It won't be part of the root Map<Class<? extends Service>, AndroidInjector.Factory<? extends Service>> binding and instead will only be accessible in Api24OrGreaterServiceComponent.

@ronshapiro Thank you very much for your answer. I had managed fix this issue in my personal project. And I have also created a Dagger tutorial project which demonstrates this fix. Here is the pull request link to show the difference:
https://github.com/TonyTangAndroid/DaggerTutorial/pull/10/files

And again, thank to your suggestion, I finally get to understand the @SubComponent in Dagger. Before Dagger Android module is released, I felt confused to understand the SubComponent and how to use it. So I tried to avoid it. After Dagger Android module has been released, I no longer have to use @SubComponent as the @ContributesAndroidInjector, which is based on @SubComponent and would serve my requirement, so I no longer put much thought about the SubComponent until today. Appreciate all the help.

Separate apks isn't great because users sometimes get Android updates! hehe. And then, they would get crashes from missing the bindings.

@Subcomponent(module = ModuleThatIncludes_ContributesAndroidInjector_For_MyTileService.class)
interface Api24OrGreaterServiceComponent {
  DispatchingAndroidInjector<Service> injector();
}

^ This is brilliant. I would not have thought of overriding the @Inject constructor like that. Thank you!

You may want to define an interface similar to HasServiceInjector and create a parallel to `AndroidInjector.inject(Service) for that as well, depending on how your app is structured.

^ This might not be necessary if you can add an inject(ApplicationOrWhatever) on the new subcomponent that matches the original component (or subcomponent).

@Inject DispatchingAndroidInjector<Service> dispatchingServiceInjector;
{
  if (SDK_INT >= N) {
    component. api24OrGreaterServiceComponent().inject(this);
  } else {
    component.inject(this);
  }
}

I made a working thing here that isn't so bad!

Another quick solution to overcome this issue is use real Components for your service. Not a Subcompoment nor ContributesAndroidInjector.
This way your greater component does not have a map with the Service being the key.
Of course then you would have to create this component yourself by possible accessing the main component manually if you need some dependencies from there.

I've just run into the same issue using a subclass of JobService. Naturally the service is only used on devices running Android 5.x or newer.

As a possible way to add support for this in Dagger, how about using the @RequiresApi annotation?

@ContributesAndroidInjector
@RequiresApi(VERSION_CODES.LOLLIPOP)
JobService jobService();

If this annotation is present, the generated code could be modified to look like this:

private Map<Class<? extends Service>, Provider<AndroidInjector.Factory<? extends Service>>> getMapOfClassOfAndProviderOfFactoryOf3() {

  MapBuilder mapBuilder = MapBuilder.<Class<? extends Service>, Provider<AndroidInjector.Factory<? extends Service>>>newMapBuilder(3)
      .put(FooService.class, (Provider) fooServiceSubcomponentBuilderProvider)
      .put(BarService.class, (Provider) barServiceSubcomponentBuilderProvider);

  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
      mapBuilder.put(JobService.class, (Provider) jobServiceSubcomponentBuilderProvider)
  }

  return mapBuilder.build();
}

~edit: Updated as per Jake Wharton's comment. Thanks for the correction.

Not a bad idea! It should be @RequiresApi as your want to prevent callers
from invoking that method without the necessary API check as well.

On Wed, Mar 28, 2018 at 11:31 AM Markus Pfeiffer notifications@github.com
wrote:

I've just run into the same issue using a subclass of JobService.
Naturally the service is only used on devices running Android 5.x or newer.

As a possible way to add support for this in Dagger, how about using the
@TargetApi annotation?

@ContributesAndroidInjector
@TargetApi(VERSION_CODES.LOLLIPOP)
JobService jobService();

If this annotation is present, the generated code could be modified to
look like this:

private Map, Provider>> getMapOfClassOfAndProviderOfFactoryOf3() {

MapBuilder mapBuilder = MapBuilder., Provider>>newMapBuilder(3)
.put(FooService.class, (Provider) fooServiceSubcomponentBuilderProvider)
.put(BarService.class, (Provider) barServiceSubcomponentBuilderProvider);

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
mapBuilder.put(JobService.class, (Provider) jobServiceSubcomponentBuilderProvider)
}

return mapBuilder.build();
}

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/google/dagger/issues/1064#issuecomment-376929955, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAEEEYEWbX_RZ3N0foBZx_x8FJeOcvoAks5ti6zggaJpZM4R7gKB
.

That is unfortunately a bit more complicated than one would like to modify the generated code to look like that. Our infratstructure doesn't allow for multiple statements to implement a single binding at the moment. But i'll think about this a bit more

I wish we didn't need to use Class literals to connect the dots between the multibinding and the activity/fragment. It's causing issues for others around class loading. It's the safest option we've been able to come up with in the face of proguarding or without using ErrorProne.

If we had something else to use as the map key, you could include it normally and then map value would never be queried.

Here's a crazy idea that doesn't solve the classloading issue but does solve the android API issue: if the .class literal isn't accessible (because the class itself isn't) from the component that uses the multibinding, we define a proxy method that returns the class. We could define a proxy method whenever there's @RestrictedApi on the multibinding, and if you're on an earlier version, we would return Activity.class. That would cause issues if you're using Guava because we use ImmutableMap.Builder which rejects duplicates... but we could just not use it.

For those who came across this issue. The solution was introduced in above commit.

You need to put the following argument to enable it. This will use Strings instead of Class references (which then fixes the mentioned crash). -Adagger.android.experimentalUseStringKeys

On Android java-only projects, you do

android {
    defaultConfig {
        javaCompileOptions.annotationProcessorOptions {
            arguments['dagger.android.experimentalUseStringKeys'] = 'true'
        }
    }
}

For Kotlin projects with kapt you would do

kapt {
  arguments {
    arg('dagger.android.experimentalUseStringKeys')
  }
}

Note: More explanation on how this works and its effects on Proguard/R8 is available here: https://github.com/google/dagger/blob/3a089646fb70fb5a193a731c4909d6ccaeeaadd3/java/dagger/android/processor/AndroidProcessor.java#L44-L52

Note 2: Keep in mind that enabling this also breaks incremental annotation processor support on Dagger 5.+

Was this page helpful?
0 / 5 - 0 ratings