google / dagger

A fast dependency injector for Android and Java.
https://dagger.dev
Apache License 2.0
17.42k stars 2.01k forks source link

Add AndroidInjection.inject(View) #720

Closed rakshakhegde closed 7 years ago

rakshakhegde commented 7 years ago

We have inject for Activity, Fragment, Receivers, etc., but one is required for custom views too.

ronshapiro commented 7 years ago

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.

trevjonez commented 7 years ago

it might be possible to use https://github.com/InflationX/ViewPump in a way that allows you to use normal constructor injection for custom views as well, thus making special constructs for injecting a view unneeded at least in the context of dagger-android.

ZakTaccardi commented 7 years ago

it's not fully clear to us that injecting views is the right thing to do

while I would like to agree, fragments have their own issues, which has let to the rise of using custom views

JakeWharton commented 7 years ago

Custom views are not a replacement for fragments. You still need a coordination layer which is responsible for inflating, transitioning, and managing the stack. It should be that component's responsibility for injecting any views (or at the very least, providing the injector via the context hack).

ZakTaccardi commented 7 years ago

Custom views are not a replacement for fragments.

agreed, I was just pointing out the reason for the use case

sevar83 commented 7 years ago

I currently inject Square's Coordinators via constructor and and I bind the coordinators to the custom views from inside the Activity. So the Activity becomes just a container for custom views and a coordination layer. Any opinions - is that correct?

jemshit commented 6 years ago

Would anyone elaborate the context hack approach?

ronshapiro commented 6 years ago

Not directly related, but discussed this a bit with @digitalbuddha here (and here's the gist)

autonomousapps commented 6 years ago

I'm open to being told this is bad design, but here's my situation and why I'd like the ability to easily inject Views.

My app's monetization strategy is based around subscriptions, and we have three tiers of membership -- call them Gold, Bronze, Silver.

I have a custom view, TierLayout, and a custom style attribute,

    <declare-styleable name="TierLayout">
        <!-- Gold | Silver | Bronze -->
        <attr name="tier" format="string"/>
    </declare-styleable>

I can set the tier on a TierLayout in XML.

In my custom view, I take a peak at the AttributeSet to see what the XML says and set the appropriate tier and set all the correct values relating to that programmatically. Of course, a Tier is a complex beast, so I get one via a TierFactory, which has an @Inject constructor.

In the current prod version of my app, I have a static reference to the relevant @Subcomponent, and I can call, e.g. DaggerUtil.INSTANCE.getUpgradeSubcomponent().inject(this /* TierLayout */). This gives me my TierFactory, from which I can call tierFactory.fromString(tierName /* from attrs */). I can do this right in the view's constructor.

I am in the fortunate (?) position of rewriting my app from scratch, and I've adopted dagger.android as the approach. This means I no longer have an easy static reference to that subcomponent, since I let the lib instantiate it for me.

@ContributesAndroidInjector abstract fun upgradeActivity(): UpgradeActivity

Please note, the existing prod code was written in Java, works well, and I'm basically just trying to port it over as-is to the new app (and in Kotlin, but that's an aside).

What is the recommended approach for getting an injectable object into my custom view?

Zhuinden commented 5 years ago

Time for us to just wait for Jake Wharton to solve this problem for us in https://github.com/square/AssistedInject 's inflation-inject 😛

ghahramani commented 4 years ago

Any news on this?

Zhuinden commented 4 years ago

You can inject anything you want with Dagger-Android as long as you create the dagger injection module just like AndroidInjectionModule.class for your target iirc

ghahramani commented 4 years ago

I am a bit new with dagger, I searched a lot for an example of a custom view with injection but no luck. It would be great if you could refer me to an example or article for injection in a custom view.

Zhuinden commented 4 years ago

@ghahramani if you check the source code, AndroidInjectionModule.class looks like this

@Module
public abstract class AndroidInjectionModule {
  @Multibinds
  abstract Map<Class<?>, AndroidInjector.Factory<?>> classKeyedInjectorFactories();

  @Multibinds
  abstract Map<String, AndroidInjector.Factory<?>> stringKeyedInjectorFactories();

  private AndroidInjectionModule() {}
}

So what you need to do is get a Class<?>, AndroidInjector.Factory<?> in for a given custom view.

It might even work just by providing @ContributesAndroidInjector for a custom view.

If not, this factory looks like this:

@Module(
  subcomponents =
      MySubcomponent.class
)
public abstract class MyFragmentModule {
  private MyFragmentModule() {}

  @Binds
  @IntoMap
  @ClassKey(MyFragment.class)
  abstract AndroidInjector.Factory<?> bindAndroidInjectorFactory(
      MyFragmentSubcomponent.Factory builder);

  @Subcomponent
  @PerScreen
  public interface MyFragmentSubcomponent
      extends AndroidInjector<MyFragment> {
    @Subcomponent.Factory
    interface Factory extends AndroidInjector.Factory<MyFragment> {}
  }
}

So if @ContributesAndroidInjector didn't work, you need to replace MyFragment with MyView and it would theoretically work.

You can check https://stackoverflow.com/questions/53889327/dagger-android-for-custom-class-possible which worked with Conductor Controllers, the idea should be the same.

I hope you won't need to worry about DispatchingAndroidInjector, I'm not sure when that comes into the picture.

ghahramani commented 4 years ago

Wow, @ContributesAndroidInjector works. Thank you so much. I was literally looking for this around a week. I found out about Mortar from Square guys, what do you think about it? It works on top of Dagger2 https://github.com/square/mortar

Zhuinden commented 4 years ago

Wow, @ContributesAndroidInjector works. Thank you so much.

That's pretty cool, I guess the 2.20 update made the library much more powerful.


Mortar works independently of Dagger2, its development has been dead for 3 years, and I was happy to rip the BundleServiceRunner out of our code and replace it with simple-stack a while ago.

Mortar itself is not based on Dagger2 at all btw, it holds Map<String, Any> in a Map<String, Map<String, Any>> where the String keys identify a scope, and in that scope they stored a Dagger component in some samples. The only trick is that this map is in Application, so it doesn't die with the Activity.

ghahramani commented 4 years ago

Great. Thank you for the explanation. In that case I won't go with it as you mentioned it is a dead project (3 years no development :-1: )

CamiloVega commented 4 years ago

@ghahramani , @Zhuinden , sorry but I am not quite following how you got the @ContributesAndroidInjector to work with a custom view, is there a code example you can point me at please?

ghahramani commented 4 years ago

@CamiloVega I'll post the example tomorrow, Also I'm almost finishing a medium related to that.

ghahramani commented 4 years ago

@CamiloVega Here is an example of it

The Module:

@Module
internal abstract class CustomViewDiModule {

    @ContributesAndroidInjector
    abstract fun bindDictionaryTextView(): DictionaryTextView

}

And here is the DictionaryTextView

class DictionaryTextView : XXXText {

    @Inject
    protected lateinit var viewModel: XXXViewModel

    init {
        application.androidInjector().inject(this)
    }

    @Suppress("USELESS_CAST")
    override val application: DaggerApplication by lazy {
        val ctx = context.applicationContext
        if (ctx is DaggerApplication) {
            return@lazy ctx as DaggerApplication
        }
        throw IllegalStateException("Application context does not extend DaggerApplication: $context")
    }
}

Now XXXViewModel is injected

Hope it helps, let me know if you need any help

CamiloVega commented 4 years ago

@ghahramani I have no way to thank you, this has saved me so much work and time, and now my code definitely looks cleaner. Thank you!

ghahramani commented 4 years ago

@CamiloVega Glad that could help, I have finished the medium post, please check it for further information

https://medium.com/@ghahremani/android-custom-view-lifecycle-with-dependency-injection-as-a-bonus-4a55217e15d8?sk=b62089ab35a5d0d0f379e194bbd2ae30