5

I'm struggling with the setup of Dagger (1.0.1), in a existing application. It was configured to use ProGuard but I disabled it for this test with -dontobfuscate.

When I enable dagger-compiler it's able to successfully generate a dot file with the dependencies graph, but when I remove the compiler and build the app in Release mode it crashes during startup, complaining that it's unable to create the object graph.

java.lang.RuntimeException: Unable to start activity 
  ComponentInfo{com.corp.myapp/com.corp.myapp.ui.activity.MainActivity}: 
  java.lang.IllegalStateException: Errors creating object graph:

No injectable members on com.corp.myapp.core.services.ConnectionMonitor. Do 
  you want to add an injectable constructor? required by 
  com.corp.myapp.core.services.ConnectionMonitor 
  com.corp.myapp.ui.activity.MyAppBaseActivity.connectionManager

No injectable members on com.corp.myapp.ui.crouton.CroutonManager. Do you want 
  to add an injectable constructor? required by 
  com.corp.myapp.ui.crouton.CroutonManager 
  com.corp.myapp.ui.activity.MyAppBaseActivity.croutonManager

No injectable members on com.corp.core.assembler.ResourceAssembler. Do you want 
  to add an injectable constructor? required by 
  com.corp.core.assembler.ResourceAssembler 
  com.corp.myapp.ui.activity.MyAppBaseActivity.resourceAssembler

I see MyAppBaseActivity and it's dependencies with CroutonManager or ConnectionMonitor being displayed in the generated dot file, so according to this comment I expected this to work. AFAIK if there was something wrong it should be detected by the compiler-enabled build that I used to generate the dot file.


UPDATE:

I previously stated that

In Debug mode it never fails

but it's not really true after further testing: In Debug mode it doesn't fail because ProGuard is disabled, whereas in Release mode it is enabled by default. If I build the app in Release mode but skip ProGuard, I don't get the errors either and the app successfully starts. So the problem is definitely related to my ProGuard configuration.

JJD
  • 50,076
  • 60
  • 203
  • 339
  • Why do you remove the compiler for release mode? The compiler is meant to be used to speed up things since no annotation processing has to take place at runtime. For me it makes no sense to make the app run slower in release by removing the dagger-compiler. – deekay Aug 07 '13 at 14:28
  • Yeah, I know the compiler is there for providing such performance improvement (besides graph validation at compile time). I disabled it because I'm also using ProGuard and according to Square guys the way to avoid problems was to disable the compiler and let Dagger generate all the required code at runtime. – David Santiago Turiño Aug 07 '13 at 14:37

4 Answers4

17

Dagger relies a lot on reflection and class names that are hard-coded and manipulated as strings. This makes the code difficult to shrink/optimize/obfuscate.

The following configuration works for the sample dagger/examples/simple in Dagger 1.1.0:

-keepattributes *Annotation*

-keepclassmembers,allowobfuscation class * {
    @javax.inject.* *;
    @dagger.* *;
    <init>();
}

-keep class **$$ModuleAdapter
-keep class **$$InjectAdapter
-keep class **$$StaticInjection

-keepnames !abstract class coffee.*

-keepnames class dagger.Lazy

The configuration keeps all fields and methods with javax.inject or dagger annotations, and all parameterless constructors. ProGuard might otherwise remove them if they appear unused, but Dagger is actually injecting/accessing them through reflection. This is similar to RoboGuice.

It also has to keep all adapter classes generated by Dagger.

It also has to keep all class names related to these adapter classes, so the names still match. In this sample, those are almost all classes in the package coffee, so the easiest way is to use a wild-card. This line will be different for other applications.

Finally, it also has to keep the name of the class dagger.Lazy, since its name is hard-coded as a string in the generated code.

JJD
  • 50,076
  • 60
  • 203
  • 339
Eric Lafortune
  • 45,150
  • 8
  • 114
  • 106
  • Thanks for your thorough response Eric! – David Santiago Turiño Sep 10 '13 at 08:25
  • Regarding the keepnames entry for your own application classes, I've reduced them to two possible scenarios so far: 1) whenever you add any binding to a Dagger module for a new dependency via the corresponding provideXXX method, the returned type has to be added to the config. 2) If you have a class with an @Inject annotated constructor that extends another class not already filtered in the ProGuard configuration, that base class has to be added to the config. – David Santiago Turiño Jan 22 '14 at 08:46
  • David, If I use dagger-compiler, I'm under the impression it should not use reflection. in this case, which configurations should I implement in my proguard file? – superjugy Feb 14 '14 at 05:05
  • If like me you don't know ProGuard well and tried to copy the above, adding a second * to coffee.* will cause it to work recursively (coffee.** will match coffee.gui.whatever). Took me a bit to figure out. – Torque May 16 '14 at 20:13
1

Dagger doesn't require @Inject to be on a class to be passed into graph.inject(myActivity) because some activities may not have any injections to make. However, these seem like upstream dependencies, which means that they need to be provided to ComponentInfo, and therefore need to be provisioned by Dagger. It cannot do this if it cannot create these classes, and it can't do so if these are not annotated, unless it provides them via a @Provides method.

So, you either need to create an @Module-annotated class which returns these types from @Provides-annotated methods, or you need to add @Inject to their constructor.

-keep class * extends dagger.internal.Binding

That said, in this case, are you using proguard in "release" mode? And not proguarding in debug mode? If so, I suspect Proguard to be stripping away annotations. You'll need to do some variant of:

-keep class javax.inject.** { *; }

... to ensure that Proguard doesn't remove the annotations.

JJD
  • 50,076
  • 60
  • 203
  • 339
Christian Gruber
  • 4,691
  • 1
  • 28
  • 28
  • Christian I'm testing more configuration alternatives as an attempt to isolate the problem, but so far the only way to avoid the app crashing is by totally skipping ProGuard. I'll think about your recommendation on the inject-related ProGuard config and update the question with more details. Thanks for your feedback! – David Santiago Turiño Aug 07 '13 at 15:24
  • Also, my concern here is also about the fact that Dagger is supposed to work fine with ProGuard if dagger-compiler is disabled so it has to do that stuff at runtime, losing the performance gain of the precompilation. This does not seem to be the case for me :/ – David Santiago Turiño Aug 07 '13 at 15:39
  • 1
    Yeah - dagger is not supposed to work fine in any case if the compiler is disabled. As of 1.0 (I think, or 1.1) code generation is mandatory for Modules (though not for @Inject classes). We are trying to address this by not storing strings explicitly, and being a bit smarter in how we manage that. Keep abreast of the main project, and you should see stuff about that soon. – Christian Gruber Aug 21 '13 at 00:11
  • 1
    Hum, I did not expect that as per Jesse's last comment at https://plus.google.com/117210567825404150882/posts/HGjnJJHbfMj -> "If you remove dagger-compiler from your build, it should be proguard safe". – David Santiago Turiño Sep 06 '13 at 15:57
1

I got the app to start after adding -dontshrink to the ProGuard config file. Having -dontobfuscate at the beginning was not enough.

In fact, if I remove -dontobfuscate it also works.

I definitely need finer control for this but it's a starting point. My current ProGuard setup for Dagger is:

#############
#   Dagger  #
#############

-keep class dagger.** { *; }
-dontwarn dagger.internal.codegen.**
  • 1
    FYI: This works for dagger 1.0.1 but not for 1.1.0, where I get an application initialization error related to obfuscated code while accessing module adapters. – David Santiago Turiño Sep 06 '13 at 15:50
0

I ended up burning a week+ trying to get this to work. In the end I failed and decided to give DexGuard a shot. It worked beautifully right out of the box. Yes its a commercial product but DexGuard has great support and because of such we were able to finally ship. Id definitely recommend DexGuard if you absolutely need to solve this issue.

Donn Felker
  • 9,553
  • 7
  • 48
  • 66
  • DexGuard is written with by the same guy. Do you know what is the difference? I thought they require the same configuration but DexGuard is giving additional optimizations and security. – tasomaniac Dec 30 '15 at 13:15