Skip to content

Instantly share code, notes, and snippets.

@ronshapiro
Last active April 24, 2024 00:19
Show Gist options
  • Save ronshapiro/af99799fe9dd7ec412f2d4e45b843370 to your computer and use it in GitHub Desktop.
Save ronshapiro/af99799fe9dd7ec412f2d4e45b843370 to your computer and use it in GitHub Desktop.
dagger.android for views in ~10 minutes

1. You can implement View.getActivity() like this:

public interface ViewWithActivity {
  // Using android-gradle-plugin 3.0, which has the desugar step for default methods on interfaces
  default Activity getActivity() {
    // https://android.googlesource.com/platform/frameworks/support/+/03e0f3daf3c97ee95cd78b2f07bc9c1be05d43db/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java#276
    Context context = getContext();
    while (context instanceof ContextWrapper) {
      if (context instanceof Activity) {
        return (Activity)context;
      }
      context = ((ContextWrapper) context).getBaseContext();
    }
    throw new IllegalStateException("Context does not stem from an activity: " + getContext());
  }
}

2. Create a parallel to the Has*Injector classes

interface HasViewInjector {
  AndroidInjector<View> viewInjector();
}

3. Define a new activity base class:

public abstract DaggerAppCompatWithViewActivityNamingIsFun
    extends DaggerAppCompatActivity
    implements HasViewInjector {
  @Inject DispatchingAndroidInjector<View> viewInjector;
  
  @Override
  public AndroidInjector<View> viewInjector() {
    return viewInjector;
  }
}

4. Create a module

@Module
interface ViewInjectionModule {
  @Multibinds
  abstract Map<Class<? extends View>, AndroidInjector.Factory<? extends View>>
      viewInjectorFactories();
}

// ...

@Component(
  modules = {
    AndroidSupportInjectionModule.class,
    ViewInjectionModule.class, 
    // ...
  })
interface AppComponent

5. ViewInjection.inject

public final class ViewInjection {
  public static void inject(ViewWithActivity view) {
   checkNotNull(view, "view");
    Activity activity = view.getActivity();
    if (!(application instanceof HasViewInjector)) {
      throw new RuntimeException(
          String.format(
              "%s does not implement %s",
              activity.getClass().getCanonicalName(),
              HasViewInjector.class.getCanonicalName()));
    }

    AndroidInjector<View> viewInjector =
        ((HasViewInjector) activity).viewInjector();
    checkNotNull(viewInjector, "%s.viewInjector() returned null", activity.getClass());

    viewInjector.inject(view);
  }
}

6. Equivalent of @ContributesAndroidInjector MyView

7. Call ViewInjection.inject(this)

I'm not sure where this should happen yet, perhaps onFinishInflate or onAttachedToWindow()?

@digitalbuddha
Copy link

I think 1 could work well through getSystemService.

Not sure what 6 is since its blank maybe I'm just having trouble grokking it all until I start playing with it

@ronshapiro
Copy link
Author

@ContributesAndroidInjector doesn't work for Views yet. we could make it work, but until then I meant #6 as being the handwritten equivalent of what @cai generates

@jemshit
Copy link

jemshit commented Nov 17, 2017

if (!(application instanceof HasViewInjector)) should be if (!(activity instanceof HasViewInjector)) ?

@yolapop
Copy link

yolapop commented Nov 20, 2017

@ronshapiro Could you give a clue on how to write #6?

@dyyao
Copy link

dyyao commented Feb 15, 2018

@ronshapiro wondering about #6 as well, am trying to solve a problem that requires injecting a "configurator" into custom views but i'm having trouble getting it working with dagger-android 2.14

@autonomousapps
Copy link

viewInjector.inject(view); doesn't work because it expects an android.view.View, while we actually have a ViewWithActivity

@autonomousapps
Copy link

I have sinned against the gods of Dagger2, and their punishment is perpetual stack overflow errors. Can someone tell me where I went wrong?

// ViewInjection.java
public final class ViewInjection {
    public static void inject(View view) {
        checkNotNull(view, "view");
        Activity activity = getActivity(view);
        if (!(activity instanceof HasViewInjector)) {
            throw new RuntimeException(
                String.format(Locale.US,
                    "%s does not implement %s",
                    activity.getClass().getCanonicalName(),
                    HasViewInjector.class.getCanonicalName()
                )
            );
        }

        AndroidInjector<View> viewInjector = ((HasViewInjector) activity).viewInjector();
        checkNotNull(viewInjector, activity.getClass() + ".viewInjector returned null");
        viewInjector.inject(view);
    }

    private static Activity getActivity(View view) {
        Context context = view.getContext();
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                return (Activity) context;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
        throw new IllegalStateException("Context does not stem from an activity: " + view.getContext());
    }
}

// ViewInjectionModule.java
@Module(subcomponents = ViewSubcomponent.class)
public abstract class ViewInjectionModule {

    // This differs from the gist because I was trying to figure out how to actually get the proper factories injected into my DispatchingAndroidInjector
    @Binds @IntoMap
    @ClassKey(TierLayout.class)
    abstract AndroidInjector.Factory<? extends View> bindViewInjectorFactory(ViewSubcomponent.Builder builder);
}

// ViewSubcomponent.java
@Subcomponent(modules = ViewInjectionModule.class)
public interface ViewSubcomponent extends AndroidInjector<View> {

    void inject(TierLayout tierLayout);

    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<View> { }
}

// HasViewInjector.java
public interface HasViewInjector {
    AndroidInjector<View> viewInjector();
}

// MyActivity.kt
class MyActivity : AppCompatActivity(), HasViewInjector {
    @Inject lateinit var viewInjector: DispatchingAndroidInjector<View>
    override fun viewInjector() = viewInjector

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        ...
    }
}

Before my changes to the ViewInjectionModule, it actually compiled and ran, but when I tried to inject my view, it rightly complained I hadn't provided any factories. Honestly, I just don't know how to do that. The docs are pretty opaque.

Error:

:app:kaptDebugKotlin
w: warning: Supported source version 'RELEASE_7' from annotation processor 'org.jetbrains.kotlin.kapt3.ProcessorWrapper' less than -source '1.8'

e: [kapt] An exception occurred: java.lang.StackOverflowError
	at sun.reflect.annotation.AnnotationInvocationHandler.<init>(AnnotationInvocationHandler.java:48)
	at sun.reflect.annotation.AnnotationParser$1.run(AnnotationParser.java:306)
	at sun.reflect.annotation.AnnotationParser$1.run(AnnotationParser.java:303)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.reflect.annotation.AnnotationParser.annotationForMap(AnnotationParser.java:303)
	at com.sun.tools.javac.model.AnnotationProxyMaker.generateAnnotation(AnnotationProxyMaker.java:87)
	at com.sun.tools.javac.model.AnnotationProxyMaker.generateAnnotation(AnnotationProxyMaker.java:79)
	at com.sun.tools.javac.code.AnnoConstruct.getAnnotation(AnnoConstruct.java:179)
	at dagger.internal.codegen.MapKeys.unwrapValue(MapKeys.java:84)
	at dagger.internal.codegen.MapKeys.mapKeyType(MapKeys.java:93)
	at dagger.internal.codegen.KeyFactory.bindingMethodKeyType(KeyFactory.java:222)
	at dagger.internal.codegen.KeyFactory.forBindingMethod(KeyFactory.java:179)
	at dagger.internal.codegen.KeyFactory.forBindsMethod(KeyFactory.java:145)
	at dagger.internal.codegen.DelegateDeclaration$Factory.create(DelegateDeclaration.java:73)
	at dagger.internal.codegen.ModuleDescriptor$Factory.create(ModuleDescriptor.java:180)
	at dagger.internal.codegen.ComponentDescriptor$Factory.create(ComponentDescriptor.java:502)
	at dagger.internal.codegen.ComponentDescriptor$Factory.create(ComponentDescriptor.java:519)
	at dagger.internal.codegen.ComponentDescriptor$Factory.create(ComponentDescriptor.java:519)

...forever and ever...


:app:kaptDebugKotlin FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:kaptDebugKotlin'.
> Compilation error. See log for more details

@hijamoya
Copy link

Not work after Dagger 2.17...

@jkaan
Copy link

jkaan commented Sep 12, 2019

@hijamoya This actually broke for me after upgrading to Dagger 2.20, in 2.19 it still works. If you want to fix it you have to do two changes:

  1. Change @ViewKey for @ClassKey.
  2. Change the return type of the AndroidInjector.Factory to AndroidInjector.Factory

@ronshapiro
Copy link
Author

ronshapiro commented Sep 12, 2019 via email

@davidliu
Copy link

In recent dagger releases, a lot of this has been made deprecated I think. HasAndroidInjector was introduced in 2.23, which can take any arbitrary type. The only thing that you'll need is some way to locate the relevant AndroidInjector now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment