How to migrate missing inject from module with com

2019-01-25 22:55发布

问题:

I have a library project/module that is used by both Android apps and regular java apps. In Dagger 1 this project/module has property complete = false. Within there is an @Inject field that is not satisfied by any class implementation or @Provides method. The idea is to force the "top" module(s) which has complete = true to provide system specific implementation

Just for the sake of example: In the library project I have ActLogin activity that have field @Inject @Named("app version") mAppVersion. The value of this field is used when logging in into a server. ActLogin is used by several apps that use this library. Each app's module has complete = true and provides value with @Provides @Named("app version") provideAppVersion()

Documentation for migration of Dagger 2 (http://google.github.io/dagger/dagger-1-migration.html) states:

Dagger 2 modules are all declared as complete = false and library = true

and in the same time the "main" documentation page (http://google.github.io/dagger/) states:

The Dagger annotation processor is strict and will cause a compiler error if any bindings are invalid or incomplete.

The latter is obviously the correct one because when trying to build with unsatisfied inject error is produced (error: java.lang.String cannot be provided without an @Provides- or @Produces-annotated method).

The question is: is it possible to migrate this approach (deferring providing inject) to Dagger 2 and how?

P.S. Initially I thought as a dirty workaround to provide some dummy values in the library's @Module but then again - you cannot have module overrides in Dagger 2 (which is kind of WTF(!!!). Module overrides were the most useful feature for me when creating unit tests). Probably I am missing something very basic and I hope that someone can point it out :-).

回答1:

It turns out that there is dedicated construct for this but it takes some time to find it out. If you need to have a component that have a module which contains unsatisfied inject(s) - make it @Subcomponent. As documentation clearly states:

That relationship allows the subcomponent implementation to inherit the entire binding graph from its parent when it is declared. For that reason, a subcomponent isn't evaluated for completeness until it is associated with a parent

So in my case, my library project needs to be a dagger subcomponent. When I use it in my app project, my app dagger component have to include the lib subcomponent.

In code:

The library subcomponent:

@Subcomponent(modules = Mod1.class)
public interface MyLibraryComponent {
    void inject(Mod1Interface1 in);
}

The app component:

@Component(modules = Mod2.class)
@Singleton
public interface MyAppComponent {
    void inject(MainActivity act);
    MyLibraryComponent newMyLibraryComponent();
}

Please note the MyLibraryComponent newMyLibraryComponent(); - that is how you tell dagger that your component contains that subcomponent.

Graph instantiation:

    MyAppComponent comp = DaggerMyAppComponent.builder().build();

Please note that contrary to using component composition with dependencies (@Component's property) in this case you don't have to "manually" construct your subcomponent. The component will "automatically" take care for that in case the subcomponent's modules don't need special configuration (i.e. constructor parameters). In case some subcomponent's module requires configuration you do it trough the component instantiation like this:

MyAppComponent comp = DaggerMyAppComponent.builder().
                          mod2(new Mod2SpecialConfiguration()).
                          build();

For android there is a special twist if your library project contains activities because each activity have to be separately injected "on demand" contrary to regular java application where you usually inject the whole application once at start up time.

For the sake of example let's say our library project contains login activity "ActLogin" that we use as common for several applications.

@Subcomponent(modules = Mod1.class)
public interface MyLibraryComponent {
    void injectActLogin(ActLogin act);
    void inject(Mod1Interface1 in);
}

The problem is that in Android we usually create our dependency graph in the Application object like this:

public class MyApplication extends Application {
    private MyAppComponent mAppDependencyInjector;

    @Override
    public void onCreate() {
        super.onCreate();
        mAppDependencyInjector = DaggerMyAppComponent.builder().build();
    }

    public MyAppComponent getAppDependencyInjector() {
        return mAppDependencyInjector;
    }
}

and then in your activity you use it like this:

@Override
protected void onCreate(Bundle savedInstanceState) {
    // ...
    ((MyApplication) getApplication()).getAppDependencyInjector().inject(this);
    // ...
}

but our ActLogin activity is part of the library project (and dagger component) that is not event aware of what application will it be used in so how are we going to inject it?

There is a nice solution but please note that I am not sure it is canonical (i.e. it is not mentioned in the documentation, it is not given as an example by the "authorities" (afaik))

Project's source can be found at github.

First you will have to extend the library dagger component in you app component:

public interface MyAppComponent extends MyLibraryComponent {

That way your app component will contain all the inject methods from the subcomponent so you will be able to inject it's activities too. After all, top component is in fact the whole object graph (more precisely the Dagger generated DaggerMyAppComponent represent the whole graph) so it is able to inject everything defined in itself + in all subcomponents.

Now we have to assure that the library project is able to access it. We create a helper class:

public class MyLibDependencyInjectionHelper {
    public static MyLibraryComponent getMyLibraryComponent(Application app) {
        if (app instanceof MyLibraryComponentProvider) {
            return ((MyLibraryComponentProvider) app).getMyLibraryComponent();
        } else {
            throw new IllegalStateException("The Application is not implementing MyLibDependencyInjectionHelper.MyLibraryComponentProvider");
        }
    }


    public interface MyLibraryComponentProvider {
        MyLibraryComponent getMyLibraryComponent();
    }
}

then we have to implement MyLibraryComponentProvider in our Application class:

public class MyApplication extends Application implements
    MyLibDependencyInjectionHelper.MyLibraryComponentProvider {
    // ...

    @Override
    public MyLibraryComponent getMyLibraryComponent() {
        return (MyLibraryComponent) mAppDependencyInjector;
    }
}

and in ActLogin we inject:

public class ActLogin extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
        super.onCreate(savedInstanceState, persistentState);
        // ...
        MyLibDependencyInjectionHelper.getMyLibraryComponent(getApplication()).
                           injectActLogin(this);
        // ...
    }
}

There is a problem with this solution: If you forget to implement the MyLibraryComponentProvider in your application you will not get an error at compile time but at runtime when you start ActLogin activity. Luckily that can be easily avoided with simple unit test.