Dagger 2 create singleton instance

2019-05-30 08:25发布

问题:

Consider the scenario that I am calling Webservice at Presenter A and holding the response data at the same Presenter. I want to utilize the same response data at Presenter E. But I cant pass the response object to each presenter B, C, D. So, I tried to store my response object at separate Holder class with getter & setter. I initialized Holder class using Dagger Inject constructor annotation and tried to consume it at Presenter E . But I got Empty response instead of my datas . Can any one suggest me to handle this scenario in best way . Thanks in advance

回答1:

You have a lot of fundamental problems with how you're using Dagger2, but my time is limited, so I'll answer for the time being regarding Mortar - all I can say is that on some devices, getSystemService() is called sooner in Application than onCreate(), which means you should initialize your root mortar scope like this:

@Override
public Object getSystemService(String name) {
    if(rootScope == null) {
        rootScope = MortarScope.buildRootScope()
                .withService(InjectorService.TAG, new InjectorService(this))
                .build("Root");
    }
    if(rootScope.hasService(name)) { // if the additional "Context" service is within Mortar
        return rootScope.getService(name);
    }
    return super.getSystemService(name); // otherwise return application level context system service
}

Personally, I had this in my onCreate()

@Override
public void onCreate() {
    super.onCreate();
    Fabric.with(this, new Crashlytics());
    realmHolder = new RealmHolder();
    ApplicationHolder.INSTANCE.setApplication(this);
    appConfig = new AppConfig(this);
    InjectorService.obtain().inject(this); // <--- this one obtains component
    initializeRealm();
}

And in InjectorService:

public static ApplicationComponent obtain() {
    return ((InjectorService) MortarScope.getScope(ApplicationHolder.INSTANCE.getApplication())
            .getService(TAG)).getComponent();
}

As such, in the worst case scenario, getSystemService() initialized my RootScope either at start-up, or when the singleton dagger component was created.

This solution is currently not multi-process friendly (so Firebase Crash Reporting would kill it by calling onCreate() in CustomApplication twice)

EDIT: Injector Service code

public class InjectorService {
    public static final String TAG = "InjectorService";

    private ApplicationComponent applicationComponent; //dagger2 app level component

    InjectorService(CustomApplication customApplication) {
        AppContextModule appContextModule = new AppContextModule(customApplication);
        RealmModule realmModule = new RealmModule();
        applicationComponent = DaggerApplicationComponent.builder()
                .appContextModule(appContextModule)
                .realmModule(realmModule)
                .build();
    }

    public ApplicationComponent getInjector() { //return the app component to inject `this` with it
        return applicationComponent;
    }

    public static InjectorService get(Context context) {
        //this is needed otherwise the compiler is whining. -_-
        //noinspection ResourceType
        return (InjectorService) context.getSystemService(TAG);
    }

    public static ApplicationComponent obtain() {
        return ((InjectorService) MortarScope.getScope(ApplicationHolder.INSTANCE.getApplication())
                .getService(TAG)).getInjector();
    }
}

As for your initial question, it's because you added @Inject annotation to your constructor, but did not include @Singleton on the class itself

@Singleton
public class Blah {
    @Inject
    public Blah() {
    }
}

EDIT:

I got home from vacation, so initial error is

Error:(40, 5) error: com.hari.daggerpoc.application.App.Component scoped with @com.hari.daggerpoc.frameworks.dagger.DaggerScope may not reference bindings with different scopes: @Singleton class com.hari.daggerpoc.cache.ResponseCache

It refers to this class in App:

@dagger.Component(modules = {Module.class})
@DaggerScope(Component.class)
public interface Component extends AppDependencies {

    void inject(App app);
}

Which inherits from this class:

@Module(includes = {Utils.class, ResponseCache.class})
public interface AppDependencies {

    Utils utils();

    ResponseCache responseCache();

}

...which is totally NOT a module, so that annotation is unnecessary, but hey.

Anyways, the issue now is that while the dependency is scoped, it is from a different scope (I didn't know the singleton scope isn't used), so if you change

@Singleton
public class ResponseCache {
    @Inject
    public ResponseCache(){    
    }

to

@DaggerScope(App.Component.class)
public class ResponseCache {

    @Inject
    public ResponseCache(){

    }

Then if in ScreenA you change

    public Callback<WeatherResponse> configServiceCallback = new Callback<WeatherResponse>() {
        @Override
        public void onResponse(Call<WeatherResponse> call, Response<WeatherResponse> response) {
            Log.d("ScreenA","Response data -->"+response.body().toString());
            Flow.get(context).setHistory(History.single(new ScreenB()), Flow.Direction.FORWARD);
            responseCache.setWeatherResponse(response.body());
        }

to

    public Callback<WeatherResponse> configServiceCallback = new Callback<WeatherResponse>() {
        @Override
        public void onResponse(Call<WeatherResponse> call, Response<WeatherResponse> response) {
            Log.d("ScreenA","Response data -->"+response.body().toString());
            responseCache.setWeatherResponse(response.body());
            Flow.get(context).setHistory(History.single(new ScreenB()), Flow.Direction.FORWARD);
        }

Then it says

08-28 18:12:48.369 31253-31253/com.hari.daggerpoc D/ScreenA: Response data -->WeatherResponse{endpoint=Endpoint{url='http://www.waynedgrant.com/weather/api/weather.json', version=1.7, githubProject='null', copyright='Copyright © 2016 Wayne D Grant (www.waynedgrant.com)'}}
08-28 18:12:48.369 31253-31253/com.hari.daggerpoc D/ScreenB: Response cache -->WeatherResponse{endpoint=Endpoint{url='http://www.waynedgrant.com/weather/api/weather.json', version=1.7, githubProject='null', copyright='Copyright © 2016 Wayne D Grant (www.waynedgrant.com)'}}


回答2:

I can imagine this solution to your problem.

The point is, you need to remove from your presenter the ApiRequest and use a interactor, the both presenter receives this Interactor injected in his constructor, in this case they will share the same interactor instance (if he was a singleton). The interactor is responsible for doing the cache, if you are using OkHttpClient you can make the cache without using a holder class(up to you), in this solution you will not perform 2 api calls for the same data.

Something like this:

public class PresenterA (){
    private UserInteractor userInteractor;
    private ViewA view;

    public PresenterA(UserInteractor interactor, ViewA view){
        this.interactor = interactor;
        this.view = view;
    }

    public void getUser(){
        interactor.findFirst(new MyCallback(){
            @Override
            public void onSuccess(User user){
                view.loadUserName(user.getName());
            }
        });
    }
}

public class PresenterB (){
    private UserInteractor userInteractor;
    private ViewB view;

    public PresenterA(UserInteractor interactor, ViewB view){
        this.interactor = interactor;
        this.view = view;
    }

    public void getUser(){
        interactor.findFirst(new MyCallback(){
            @Override
            public void onSuccess(User user){
                view.loadAddress(user.getAddress().getLine1());
            }
        });
    }
}

public class UserInteractor (){
    private MyHolderData holder;
    private MyApi api;

    public UserInteractor(MyHolderData holder, MyApi api){
        this.holder = holder;
        this.api = api;
    }

    public User getUser(){
        if(holder.hasCache()){
            return holder.getUser();
        }
        api.requestUser(new MyApiCallback(){
            @Override
            public void onSuccess(User user){
                return user;
            }
        })
    }

}