10

I'm trying to add a singleton Manager into an Android Service. The problem is that the injected manager is not the same as the one in the ViewModel.

Dagger Component

@Singleton
@Component(modules = {ApplicationModule.class, AppScreenModule.class, ServiceModule.class})
public interface AppComponent {

    void inject(App application);

    void inject(OpportunisticService opportunisticService);

    @Component.Builder
    interface Builder {
        AppComponent build();

        Builder applicationModule(ApplicationModule applicationModule);
    }
}

Modules

@Module
class ApplicationModule {    
    private final App mApp;

    ApplicationModule(App app) {
        mApp = app;
    }

    @Provides
    @Named("ApplicationContext")
    Context provideContext() {
        return mApp.getApplicationContext();
    }

    @Provides
    App provideApplication() {
        return mApp;
    }

    @Provides
    PeersManager providePeersManager() {
        return new PeersManager();
    }
}

@Module
abstract class ServiceModule {

    @ContributesAndroidInjector
    abstract OpportunisticService bindOpportunisticService();
}

Initialization

public class Components {
    private static AppComponent sComponent;

    public static void initialize(App app) {
        // Initialize the AppComponent
        Preconditions.checkState(sComponent == null, "AppComponent already initialized");
        sComponent = DaggerAppComponent.builder()
                                       .applicationModule(new ApplicationModule(app))
                                       .build();
    }

    public static AppComponent app() {
        if (BuildConfig.DEBUG)
            Preconditions.checkState(sComponent == null, "AppComponent not initialized");
        return sComponent;
    }
}

public class App extends Application implements HasActivityInjector, HasServiceInjector {
    @Inject
    DispatchingAndroidInjector<Activity> mActivityDispatchingAndroidInjector;

    @Inject
    DispatchingAndroidInjector<Service> mServiceDispatchingAndroidInjector;

    @Override
    public void onCreate() {
        super.onCreate();

        // Initialize the AppComponent
        Components.initialize(this);
        Components.app().inject(this);
    }

    @Override
    public AndroidInjector<Activity> activityInjector() {
        return mActivityDispatchingAndroidInjector;
    }

    @Override
    public AndroidInjector<Service> serviceInjector() {
        return mServiceDispatchingAndroidInjector;
    }
}

Declaring PeerManager:

@Singleton
public class PeersManager {

    @Inject
    public PeersManager() {
    }
}

Using PeersManager:

public class ViewModel {
    private final PeersManager mPeersManager;

    @Inject
    public ViewModel (PeersManager peersManager) {
        mPeersManager = peersManager;
    }
}

public class OpportunisticService extends Service {
    @Inject
    PeersManager mPeersManager;

    @Override
    public void onCreate() {
        super.onCreate();
        AndroidInjection.inject(this);
    }
}

The problem is that the ViewModel.mPeersManager is different than OpportunisticService.mPeersManager, and I would expect them to be the same given that the PeersManager is marked as singleton.

I expect this to happen because of the different AndroidInjectors for Activity/Fragment and for Service.

tasegula
  • 889
  • 1
  • 12
  • 26
  • 1
    You need to scope an object or it will be recreated every time. – David Medenjak Jul 30 '17 at 21:37
  • 1
    Isn't it scoped as singleton with `@Singleton`, or am I missing something? – tasegula Jul 31 '17 at 09:28
  • You need to scope almost everything. In this case `providePeersManager()` is missing the `@Singleton` annotation. E.g. `provideApplication()` should also be scoped in theory, but you will always return the same value since the module holds on to it. – David Medenjak Jul 31 '17 at 09:30
  • I think you have to call the `AndroidInjection.inject(this);` before `super.onCreate();`. – Blehi Aug 04 '17 at 15:28

1 Answers1

0

You've annotated the class as @Singleton as a class-level annotation, but Dagger disregards that annotation in favor of the more-explicit @Provides binding. Used properly, this would allow you to override any scoping that comes from the class itself, because Dagger assumes that regardless of the "default" that comes with the class, you explicitly list out the modules and bindings on your Component. Dagger can't see inside the @Provides method itself, so it doesn't know whether the instance comes from a constructor, a static method return value, a Module instance field, or some other more complicated logic. Here, Dagger sees an unscoped @Provides with an unknown implementation and trusts you to manage the scoping yourself regardless of any scope annotations or @Inject constructor annotations.

As a result, you can choose one of the following two options:

  • Mark @Singleton on your @Provides PeersManager providePeersManager() method in AppModule. This allows you to explicitly call the PeersManager constructor as you're doing here. At that point you can optionally remove the @Singleton annotation on the PeersManager class itself, because Dagger won't read it: it's explicitly deferring to your Module for its scope configuration.

  • Remove your @Provides PeersManager method entirely and add an @Inject-annotated constructor to PeersManager. At that point, Dagger is responsible for creating PeersManager, so it will observe the @Singleton scoping annotation on the PeersManager class.

    If you have an interface-impl split, such as an interface PeersManager and implementation class DefaultPeersManager, then you could express this as a @Binds binding in an abstract @Module class with abstract @Binds PeersManager providePeersManager(DefaultPeersManager impl). In that case, Dagger sees that you are redirecting PeersManager to a DefaultPeersManager, but Dagger knows how to provide DefaultPeersManager with its @Inject constructor and @Singleton annotation.

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251