2

Is there any way to bind with provider which interprets target's annotation value in Google Guice?

Example:

bind(Resource.class)
    .annotatedWith(MyAnnotation.class)
    .toProvider(new MyProvider<MyAnnotation, Resource>{
        public Resource get(MyAnnotation anno){
            return resolveResourceByAnnoValue(anno.value());
        }
    });

I want to initialize field of an Android Activity class by annotated binding. It should have to take multiple resources by it's unique Id.

Original Way:

public class TestActivity extends Activity{
    private TextView textView;
    private Button testButton;

    public void onAfterCreate(...){
        // set UI declaration resource.
        setContentView(R.layout.activity_test);

        // initialize fields, it must be done after setting ui definition.
        textView = (TextView) findViewById(R.id.textView);

        .... initialize other fields, hook them...

    ...
}

I want to bind UI and it's field in declarative way, not pragmatically likes above:

@ResourceID(R.layout.activity_test)
public class TestActivity extends InjectiveActivity{
    @ResourceID(R.id.textView) // Auto generated static resource id constant
    private TextView textView;

    @ResourceID(R.id.testButton)
    private Button testButton;

    ...
}
jeeeyul
  • 3,727
  • 1
  • 24
  • 37

2 Answers2

3

This isn't possible as such.

If @MyAnnotation is a binding annotation, it will be compared using its equals method. @MyAnnotation(5) Resource will be bound to @MyAnnotation(5) Resource, and that will not match at all compared to @MyAnnotation(6) Resource. Check out this SO answer for more. As in that answer, you could loop through your possible annotation values and bind each one individually, if you feel like it.

If @MyAnnotation isn't a binding annotation, you won't be able to access it at all from your provider. As mentioned in this SO answer, it is a rejected feature to add injection-site information to the provider or dependency itself.

Your best bet is to create an @Assisted injection (or manual factory) to accept the parameter:

class MyConsumer {
  final Resource resource;
  @Inject MyConsumer(Resource.Factory resourceFactory) {
    int previouslyAnnotatedValue = 5;
    this.resource = resourceFactory.createWithValue(previouslyAnnotatedValue);
  }
}

You may also consider using Custom Injections, which will let you use an arbitrary annotation other than @Inject, which may use runtime annotation values however you'd like.

Community
  • 1
  • 1
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • Android SDK generates resource id constants, So I tried to inject actual resources into fields through `@MyAnnotation(resourceId)` rather than assign manually using `findViewById()`. Since I can't create resource instance manually because it must be resolved by Android application context, I can't use factory or assisted inject approach. Anyway, Is there no way to interpret annotation value dynamically in provider? I can use looped comparable annotation which you said, But I have to iterate generated resource constants through Java reflection. It smells bed to me. :( – jeeeyul Feb 05 '13 at 08:23
  • I think I should have to write own injection mechanism rather than using Google Guice. I'm so curious why Guice does not have binding configuration which fits my requirements. – jeeeyul Feb 05 '13 at 08:26
  • I'm not sure I understand what you mean by "Since I can't create resource instance manually because it must be resolved by Android application context": You would still need your resource ID to pass as the parameter, right? Why wouldn't it be equally possible to implement your own one-method class (call it ResourceFactory) that returns an Android-created Resource? – Jeff Bowman Feb 05 '13 at 16:17
  • Sorry for my insufficient explanation. I updated my question. I think factory still can't get information about inject target field. – jeeeyul Feb 06 '13 at 00:52
  • True, though you could inject (say) a ResourceInjector you write, which could reflect across the class (resourceInjector.injectActivity(this)) and its fields looking for your annotation. You could then use a fake or mock ResourceInjector in tests. – Jeff Bowman Feb 06 '13 at 16:21
0

Here is an example in Scala (I like using Scala for prototyping, it's Java in a different dress after all) which I came up with after wondering about it myself in Dynamic Google Juice injection depending on value of an annotation

import java.lang.reflect.{Constructor, Parameter}
import java.util.concurrent.atomic.AtomicReference
import javax.inject.{Inject, Named, Provider}

import com.google.inject.matcher.Matchers
import com.google.inject.spi.ProvisionListener.ProvisionInvocation
import com.google.inject.{AbstractModule, Binder, Guice}
import com.google.inject.spi.{DependencyAndSource, ProviderInstanceBinding, ProvisionListener}
import com.typesafe.config.ConfigFactory
import net.codingwell.scalaguice.InjectorExtensions._
import net.codingwell.scalaguice.ScalaModule

import scala.collection.JavaConverters._

object GuiceExperiments extends App {

  val injector = Guice.createInjector(new MyModule())

  val some = injector.instance[Some]

  println(some)

  some.go()
}

trait Some {
  def go(): Unit
}

class Impl @Inject()(
                    @Named("a.a.a") hello: String,
                    @Named("a.a.b") bello: String,
                    @Named("a.b.a") kello: String

                    ) extends Some {
  override def go() = {
    println(hello)
    println(bello)
    println(kello)
  }
}

abstract class DynamicProvider[T >: Null](binder: Binder) extends Provider[T] {

  private[this] val nextValue = new AtomicReference[T]

  binder.bindListener(Matchers.any(), new ProvisionListener {

    private[this] def tryProvide(target: DependencyAndSource): Unit = {
      val dependency = target.getDependency
      val injectionPoint = dependency.getInjectionPoint
      val parameterIndex = dependency.getParameterIndex

      injectionPoint.getMember match {
        case constructor: Constructor[_] =>
          val parameter = constructor.getParameters()(parameterIndex)
          nextValue.set(getFor(parameter))
      }
    }

    override def onProvision[V](provision: ProvisionInvocation[V]): Unit = {

      provision.getBinding match {
        case binding: ProviderInstanceBinding[_] if binding.getUserSuppliedProvider eq DynamicProvider.this =>
          provision.getDependencyChain.asScala.lastOption.foreach(tryProvide)
        case _ => ()
      }
    }
  })

  final override def get(): T = nextValue.getAndSet(null)

  def getFor(parameter: Parameter): T
}

class MyModule extends AbstractModule with ScalaModule {

  override def configure(): Unit = {

    bind[Some].to[Impl]

    bind[String].annotatedWith[Named].toProvider(new DynamicProvider[String](binder) {
      override def getFor(parameter: Parameter): String = {
        if (parameter.isAnnotationPresent(classOf[Named])) {
          parameter.getAnnotation(classOf[Named]).value()
        } else {
          null
        }
      }
    })

  }

}

this only inserts the value of the @Named, but looks like it pretty damn works. so much for not possible.

Community
  • 1
  • 1
scravy
  • 11,904
  • 14
  • 72
  • 127
  • Cool! Do note that `.annotatedWith(Named.class)` is a somewhat deprecated Guice behaviour. If I were you I wouldn't use the binding annotation to also get the value. – Tavian Barnes Apr 17 '16 at 16:20
  • Also I don't think `AtomicReference` gives you the right semantics in the presence of threads, you need a `ThreadLocal` – Tavian Barnes Apr 17 '16 at 16:33