Skip to content

@IntoMap Lazy<T> creates each instance #1116

@TWiStErRob

Description

@TWiStErRob

Below is a test case which we were expecting to pass. In essence we were expecting that the implementation of this method

@Provides
static Map<String, Lazy<? extends X>> xs(Lazy<X1> x1, Lazy<X2> x2) {
	return ImmutableMap.of("x1", x1, "x2", x2);
}

transformed into Dagger would look like this:

@Binds @IntoMap @StringKey("x1") Lazy<? extends X> x1(Lazy<X1> impl);
@Binds @IntoMap @StringKey("x2") Lazy<? extends X> x2(Lazy<X2> impl);

however they behave differently.

In my reading asking for a Lazy<T> as an @Inject field, @Provides parameter, or @Binds parameter should behave the same way. Currently @Binds is an outlier. The problem is somewhere near DaggerLazyTest_CIntoMap.getMapOfStringAndLazyOf calling .get(), but the providers are initialize()d with DoubleCheck.provider and not DoubleCheck.lazy or ProviderOfLazy.


dependencies {
    testImplementation 'com.google.dagger:dagger:2.15'
    testAnnotationProcessor 'com.google.dagger:dagger-compiler:2.15'
}
import javax.inject.*;
import dagger.*;
import dagger.multibindings.*;

import org.junit.*;
import static org.junit.Assert.*;

import java.util.Map;
import com.google.common.collect.ImmutableMap;

public class LazyTest {

	//@formatter:off
	@Scope @interface MyScope {}

	interface X {}

	static class X1 implements X {
		static int ctorCalled;
		@Inject X1() { ctorCalled++; }
	}

	static class X2 implements X {
		static int ctorCalled;
		@Inject X2() { ctorCalled++; }
	}

	private void assertX1NotInstantiated() { assertEquals(beforeX1Count, X1.ctorCalled); }
	private void assertX2NotInstantiated() { assertEquals(beforeX2Count, X2.ctorCalled); }
	private void assertX1Instantiated() { assertEquals(beforeX1Count + 1, X1.ctorCalled); }
	private void assertX2Instantiated() { assertEquals(beforeX2Count + 1, X2.ctorCalled); }
	//@formatter:on

	private int beforeX1Count;
	private int beforeX2Count;

	@Before
	public void setUp() {
		beforeX1Count = X1.ctorCalled;
		beforeX2Count = X2.ctorCalled;
	}

	/**
	 * Use a Lazy directly in implementation: the consumer needs to reference implementations directly.
	 */
	@MyScope
	@Component interface CAtUsage {
		void inject(Use x);

		class Use {
			@Inject Lazy<X1> x1;
			@Inject Lazy<X2> x2;
		}
	}

	@Test
	public void instantiatedOnDemand_lazyInjectedAtUsage() {
		assertX1NotInstantiated();
		assertX2NotInstantiated();
		CAtUsage comp = DaggerLazyTest_CAtUsage.create();
		assertX1NotInstantiated();
		assertX2NotInstantiated();

		CAtUsage.Use use = new CAtUsage.Use();
		comp.inject(use);
		assertX1NotInstantiated();
		assertX2NotInstantiated();

		X x1Instance1 = use.x1.get();
		assertX1Instantiated();
		assertX2NotInstantiated();
		X x1Instance2 = use.x1.get();
		assertX1Instantiated();
		assertX2NotInstantiated();
		assertSame(x1Instance1, x1Instance2);

		X x2Instance1 = use.x2.get();
		assertX1Instantiated();
		assertX2Instantiated();
		X x2Instance2 = use.x2.get();
		assertX1Instantiated();
		assertX2Instantiated();
		assertSame(x2Instance1, x2Instance2);
	}

	/**
	 * Use a Lazy indirectly in module: the consumer only knows about interface.
	 */
	@MyScope
	@Component(modules = CInModule.M.class) interface CInModule {
		void inject(Use x);

		class Use {
			@Inject Map<String, Lazy<? extends X>> xs;
		}

		@Module
		abstract class M {
			@Provides
			@MyScope
			static Map<String, Lazy<? extends X>> xs(Lazy<X1> x1, Lazy<X2> x2) {
				return ImmutableMap.of("x1", x1, "x2", x2);
			}
		}
	}

	@Test
	public void instantiatedOnDemand_lazyInjectedInModule() {
		assertX1NotInstantiated();
		assertX2NotInstantiated();
		CInModule comp = DaggerLazyTest_CInModule.create();
		assertX1NotInstantiated();
		assertX2NotInstantiated();

		CInModule.Use use = new CInModule.Use();
		comp.inject(use);
		assertX1NotInstantiated();
		assertX2NotInstantiated();

		Lazy<? extends X> x1Lazy = use.xs.get("x1");
		assertX1NotInstantiated();
		assertX2NotInstantiated();
		X x1Instance1 = x1Lazy.get();
		assertX1Instantiated();
		assertX2NotInstantiated();
		X x1Instance2 = x1Lazy.get();
		assertX1Instantiated();
		assertX2NotInstantiated();
		assertSame(x1Instance1, x1Instance2);

		Lazy<? extends X> x2Lazy = use.xs.get("x2");
		assertX1Instantiated();
		assertX2NotInstantiated();
		X x2Instance1 = x2Lazy.get();
		assertX1Instantiated();
		assertX2Instantiated();
		X x2Instance2 = x2Lazy.get();
		assertX1Instantiated();
		assertX2Instantiated();
		assertSame(x2Instance1, x2Instance2);
	}

	/**
	 * Same as {@link CInModule}, but using Dagger magic ({@link IntoMap}).
	 */
	@MyScope
	@Component(modules = CIntoMap.M.class) interface CIntoMap {
		void inject(Use x);

		class Use {
			@Inject Map<String, Lazy<? extends X>> xs;
		}

		@Module interface M {
			@MyScope
			@Binds @IntoMap @StringKey("x1")
			Lazy<? extends X> x1(Lazy<X1> impl);
			@MyScope
			@Binds @IntoMap @StringKey("x2")
			Lazy<? extends X> x2(Lazy<X2> impl);
		}
	}

	@Test
	public void instantiatedOnDemand_injectedWithIntoMap() {
		assertX1NotInstantiated();
		assertX2NotInstantiated();
		CIntoMap comp = DaggerLazyTest_CIntoMap.create();
		assertX1NotInstantiated();
		assertX2NotInstantiated();

		CIntoMap.Use use = new CIntoMap.Use();
		comp.inject(use);
		assertX1NotInstantiated(); // fails here
		assertX2NotInstantiated();

		Lazy<? extends X> x1Lazy = use.xs.get("x1");
		assertX1NotInstantiated();
		assertX2NotInstantiated();
		X x1Instance1 = x1Lazy.get();
		assertX1Instantiated();
		assertX2NotInstantiated();
		X x1Instance2 = x1Lazy.get();
		assertX1Instantiated();
		assertX2NotInstantiated();
		assertSame(x1Instance1, x1Instance2);

		Lazy<? extends X> x2Lazy = use.xs.get("x2");
		assertX1Instantiated();
		assertX2NotInstantiated();
		X x2Instance1 = x2Lazy.get();
		assertX1Instantiated();
		assertX2Instantiated();
		X x2Instance2 = x2Lazy.get();
		assertX1Instantiated();
		assertX2Instantiated();
		assertSame(x2Instance1, x2Instance2);
	}

	/**
	 * Blind stab at working around the current behavior and fixing {@link CIntoMap} by wrapping in a provider.
	 */
	@MyScope
	@Component(modules = CProviderIntoMap.M.class) interface CProviderIntoMap {
		void inject(Use x);

		class Use {
			// FIXME doesn't compile:
			// error: java.util.Map<java.lang.String,javax.inject.Provider<? extends dagger.Lazy<? extends com.thetrainline.tech.LazyTest.X>>>
			// cannot be provided without an @Provides- or @Produces-annotated method.
			// java.util.Map<java.lang.String,javax.inject.Provider<? extends dagger.Lazy<? extends com.thetrainline.tech.LazyTest.X>>>
			// is injected at
			// com.thetrainline.tech.LazyTest.CProviderIntoMap.Use.xs
			// com.thetrainline.tech.LazyTest.CProviderIntoMap.Use is injected at
			// com.thetrainline.tech.LazyTest.CProviderIntoMap.inject(x)
//			@Inject Map<String, Provider<? extends Lazy<? extends X>>> xs;
		}

		@Module interface M {
			@MyScope
			@Binds @IntoMap @StringKey("x1")
			Provider<? extends Lazy<? extends X>> x1(Provider<? extends Lazy<X1>> impl);
			@MyScope
			@Binds @IntoMap @StringKey("x2")
			Provider<? extends Lazy<? extends X>> x2(Provider<? extends Lazy<X2>> impl);
		}
	}
}

Potentially related: #218

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions