-->

How do I use AndroidInjection class in custom view

2019-01-22 20:46发布

问题:

My issue with the Android-specific pattern is, if you use their AndroidInjection class, there is no way to members inject other objects besides Activities/Fragments/custom views/adapters, except with the Application Component. This is because you cannot get a reference the the Subcomponent (AndroidInjector) used to inject Activities/Fragments. This makes injecting Dialogs (if you use DialogFragments).

The AndroidInjection class seems to support just the core Android types.

回答1:

What follows is not an answer to your question, but an explanation why you shouldn't be asking this question at all.

You should avoid injections into custom Views in general. The reasons for this are listed in this article.

Advantages of using Method Injection in this case [injection into custom Views] are:

  • Dependencies will need to be propagated from top level component (Activity or Fragment)
  • Method Injection does not open door to Single Responsibility Principle violation
  • No dependency on the framework
  • Better performance

The first advantage might come as a surprise because propagation from top level component is harder than adding annotation to fields, and involves more boilerplate code. This is surely a bad thing, right?. Not in this case. In fact, there are two good aspects associated with such a propagation of dependencies. First of all, the dependencies will be visible at the top level component. Therefore, just by looking at e.g. Fragment‘s fields, the reader of the code will immediately understand that this Fragment shows images. Such optimizations for readability makes the system more easily maintainable in the long term. Secondly, there are not many use cases in which sub-classes of View need additional dependencies. The fact that you need to actually work in order to provide these dependencies will give you a bit of time to think about whether providing them is a good design decision to start with.

The second advantage is related to collaborative construction. You might be very experienced software engineer yourself, but you’ll probably have also less experienced teammates. Or it is possible that you’ll leave the project one day, and the guy who will take over will not be as good as you. By injecting one single dependency using a framework, you basically open a door for other injections. Imagine that some data from SharedPreferences becomes required in custom View in order to e.g. fix a bug. One of the less experienced developers might decide that it is a good approach to inject SharedPreferences into custom View directly. Doing this violates Single Responsibility Principle, but that developer might not even be aware of such a concept. Therefore, in the long term, such injection “backdoors” can reduce design quality and lead to long debug sessions.

The third advantage of using Method Injection with custom Views is that you don’t couple the View to dependency injection framework. Just imagine that few years from now you (or some other poor guy) need to replace the framework. The fact that you’ll probably have tens of Activities and Fragments to start with will make your life miserable. If you’ll have additional tens or hundreds of custom Views to handle, then it might bring you into suicidal mood.

The last (but not least) advantage is performance. One screen can contain one Activity, several Fragments and tens of custom Views. Bootstrapping this number of classes using dependency injection framework might degrade application’s performance. It is especially true for reflection based frameworks, but even Dagger carries some performance cost.

In addition, I advice to avoid the new injection method that involves AndroidInjection class. It is discussed in this video tutorial.



回答2:

First, you should think over Vasily's answer.

But let's think for a moment how we did this before Dagger Android? We built a subcomponent from the component that was taken from the Application class. Later, we could use this subcomponent in order to inject fields, for example, of a custom view.

So, we'll try to do the exact same thing now.

Suppose, our aim is to inject MyAdapter class into a MyButton:

public class MyButton extends AppCompatButton {

    @Inject MyAdapter adapter;

    public MyButton(Context context) {
        super(context);

        ...
    }

}

And let's make the adapter have a dependency on the activity Context, not application Context:

public class MyAdapter {

    @Inject
    public MyAdapter(@Named("activity") Context context) {
    }

}

Let's start with the custom Application class.

MyApplication.java

public class MyApplication extends DaggerApplication {

    @Inject
    DispatchingAndroidInjector<Activity> dispatchingActivityInjector;

    public static MySubcomponent mySubcomponent;

    @Override
    protected AndroidInjector<? extends DaggerApplication> applicationInjector() {

        return DaggerAppComponent.builder()
                .create(this);
    }

}

AppComponent.java:

@Component(modules = {AndroidSupportInjectionModule.class, ActivityBindingModule.class, AppModule.class})
@Singleton
public interface AppComponent extends AndroidInjector<MyApplication> {

    @Component.Builder
    abstract class Builder extends AndroidInjector.Builder<MyApplication> {
    }
}

AppModule.java

@Module
abstract class AppModule {

    @Binds
    @Singleton
    @Named("app")
    abstract Context providesContext(Application application);
}

ActivityBindingModule.java

@Module(subcomponents = MySubcomponent.class)
public abstract class ActivityBindingModule {

    @Binds
    @IntoMap
    @ActivityKey(MainActivity.class)
    abstract AndroidInjector.Factory<? extends Activity>
    bindMainActivityInjectorFactory(MySubcomponent.Builder builder);
}

AndroidSupportInjectionModule.java is shipping with dagger itself. If you do not use classes from support package (i.e. android.support.v4.app.Fragment instead of android.app.Fragment), then use AndroidInjectionModule.java.

MySubcomponent.java

@ActivityScope
@Subcomponent(modules = {SubcomponentModule.class/*, other modules here, if needed */})
public interface MySubcomponent extends AndroidInjector<MainActivity> {

    void inject(MyButton button);

    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<MainActivity> {
        public abstract MySubcomponent build();
    }
}

SubcomponentModule.java

@Module
abstract class SubcomponentModule {

    @Binds
    @ActivityScope
    @Named("activity")
    abstract Context toContext(MainActivity activity);

}

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Inject
    MySubcomponent subcomponent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Will inject `subcomponent` field
        AndroidInjection.inject(this);

        // Saving this component in a static field
        // Hereafter you are taking responsibility of mySubcomponent lifetime
        MyApplication.mySubcomponent = subcomponent;

        super.onCreate(savedInstanceState);
        setContentView(new MyButton(this));
    }
}

Having all of these, now here's how MyButton will look like:

public class MyButton extends AppCompatButton {

    @Inject MyAdapter adapter;

    public MyButton(Context context) {
        super(context);

        MyApplication.mySubcomponent.inject(this);
    }

}

I admit that this looks hacky and certainly not an approach to stick to. I'm happy to see a better approach.



回答3:

This is because you cannot get a reference the the Subcomponent (AndroidInjector) used to inject Activities/Fragments.

You can always just inject the component itself. Just add a field for the component to your Activity / Fragment and let Dagger inject it along with the rest.

// will be injected
@Inject MainActivitySubcomponent component;


回答4:

The issue of whether the dagger-android classes like AndroidInjector should support injection inside Views or not has been discussed in the following Github issue:

https://github.com/google/dagger/issues/720

Quoting from one of the library authors:

There is both a philosophical point and logistical/implementation point to be made here.

First, it's not fully clear to us that injecting views is the right thing to do. View objects are meant to draw, and not much else. The controller (in a traditional MVC pattern) is the one which can coordinate and pass around the appropriate data to a view. Injecting a view blurs the lines between fragments and views (perhaps a child fragment is really the appropriate construct instead?)

From the implementation perspective, there is also a problem in that there isn't a canonical way to retrieve the View's parent Fragments (if any), or Activity to retrieve a parent component. There have been hacks suggested to build in that relationship, but so far we haven't seen anything that seems to suggest that we could do this correctly. We could just call View.getContext().getApplicationContext() and inject from there, but skipping the intermediate layers without any option for something in between is inconsistent with the rest of our design, and probably confusing to users even if it works.

This reinforces the opinion expressed in Vasily's answer.

To add further, people often seem to want to inject model-layer dependencies inside their custom views. This is a bad idea as it goes against the software engineering principle of separation of concerns.

The correct solution for associating a view and a model is to write an adapter like the adapters for RecyclerView and ListView. You can inject the model-layer dependency at the Fragment or Presenter level and set the adapter there.