Dagger: @BindValue fields cannot be private.

Created on 11 Aug 2020  路  11Comments  路  Source: google/dagger

Hilt is generating my public @BindValue kotlin properties as private and complaining about it:

@HiltAndroidTest
@UninstallModules(
    AuthenticationStorageModule::class, 
)
@RunWith(AndroidJUnit4::class)
//@Config(application = HiltTestApplication::class)  // For Robolectric tests
class MainActivityInstrumentedTest {

    @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this)
    @get:Rule(order = 1) var activityScenarioRule = activityScenarioRule<MainActivity>(null)

    @Before fun init() = hiltRule.inject()

    @BindValue 
    val ftueData = object : FirstTimeUserExperienceData {
        override var hasOnboardingBeenCompleted = false
        override var hasNotificationAllowed = false
        override var hasFindMyDeviceCompleted = false
    }

    @BindValue 
    val authenticationData = object : AuthenticationData {
        override var name = ""
        override var emailAdress = ""
    }

    @Test fun test() = runBlockingTest { }

}

below is the error from Hilt:

.../build/tmp/kapt3/stubs/debugAndroidTest/com/my/app/MainActivityInstrumentedTest.java:31: error: [Hilt]
    private final com.my.app.storage.FirstTimeUserExperienceData ftueData = null;
                                                                                 ^
  @BindValue fields cannot be private. Found: ftueData
  [Hilt] Processing did not complete. See error above for details.warning: File for type 

'com.my.app.MainActivityInstrumentedTest_HiltComponents' created in the last round will not be subject to annotation processing.
warning: File for type 'com.my.app.MainActivityInstrumentedTest_ComponentDataHolder' created in the last round will not be subject to annotation processing.
warning: File for type 'dagger.hilt.android.internal.testing.TestComponentDataSupplierImpl' created in the last round will not be subject to annotation processing.
warning: The following options were not recognized by any processor: '[dagger.fastInit, dagger.hilt.android.internal.disableAndroidSuperclassValidation, kapt.kotlin.generated]'
[WARN] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: android.databinding.annotationprocessor.ProcessDataBinding (DYNAMIC).

Most helpful comment

Hi @Computr0n , for now I think either @BindValue lateinit var or @BindValue @JvmField val will work.

We do have plans to make this work with just @BindValue var, but we haven't gotten to this yet.

All 11 comments

This Kotlin code:

    @BindValue 
    val ftueData = object : FirstTimeUserExperienceData {
        override var hasOnboardingBeenCompleted = false
        override var hasNotificationAllowed = false
        override var hasFindMyDeviceCompleted = false
    }

implements the following interface:

interface FirstTimeUserExperienceData {
    var hasOnboardingBeenCompleted: Boolean
    var hasNotificationAllowed: Boolean
    var hasFindMyDeviceCompleted: Boolean
}

and in production code, it its @Provide ed by a module.
In the test, I uninstall it at the top of my test-code in the first post.
I want to replace it with the anonymous object for the duration of the test, so I @BindValue it as a kotlin test class property.

Here is the java code that was generated from my

    @org.jetbrains.annotations.NotNull()
    @dagger.hilt.android.testing.BindValue()
    private final com.my.app.android.storage.FirstTimeUserExperienceData ftueData = null;

I am not sure why the generated code makes this into a private field, or why it is then complaining about it.

Hi @Computr0n , for now I think either @BindValue lateinit var or @BindValue @JvmField val will work.

We do have plans to make this work with just @BindValue var, but we haven't gotten to this yet.

Excellent feedback, fast and accurate.
I think it may be a good idea to add @BindValue @JvmField val to the Kotlin testing documentation since I figure there are many developers who are more familiar with the Kotlin idioms and may not know to put@JvmFieldon their val

Currently the Android developers site has this:
Screen Shot 2020-08-11 at 4 39 20 PM

Link to doc

I believe that is not even valid Kotlin syntax -- I think it may have been hastily copied from the Java example.

Thanks for the heads up on the documentation! That should be fixed now.

The @BindValue @JvmField val approach works correctly.
The @BindValue lateinit var compiles correctly but it doesn't fulfill the dependency that was uninstalled.

The @BindValue lateinit var compiles correctly but it doesn't fulfill the dependency that was uninstalled.

@ziglee What's the error that you get?

When using like this:

@BindValue
lateinit var repository: KittensRepository

 @Before
 fun setUp() {
    repository = mockk()
    hiltRule.inject()
}

I get:

java.lang.NullPointerException: Cannot return null from a non-@Nullable @Provides method
at dagger.internal.Preconditions.checkNotNull(Preconditions.java:48)
at net.cassiolandim.kittychallenge.ui.main.MainFragmentTest_BindValueModule_ProvidesRepositoryFactory.providesRepository(MainFragmentTest_BindValueModule_ProvidesRepositoryFactory.java:36)

In this case the lateinit var is working as expected, i.e. the binding is correctly recognized by Dagger at compile time.

The issue in this case is that at runtime something in your test is requesting access to the repository before you have a chance to set the value of the repository in your @Before set up.

If you post the entire test, and entire stack trace I might be able to tell you what's causing it. However, in general initializing the @BindValue field directly is the way to go because it ensures the value is set before anything requests it.

@HiltAndroidTest
@UninstallModules(KittensModule::class)
@RunWith(AndroidJUnit4::class)
class FavoritesFragmentTest {

    @get:Rule(order = 0)
    var hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    var activityRule = MainActivityTestRule(R.id.navigation_favorites)

    @BindValue
    lateinit var repository: KittensRepository

    @Before
    fun setUp() {
        repository = mockk<KittensRepository>().also {
            coEvery { repository.favoritesLocal() } returns emptyList()
        }
        hiltRule.inject()
    }

    @Test
    fun given_list_of_favorites_When_click_to_unfavorite_Should_delete() {
        onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(RecyclerViewActions.actionOnItemAtPosition<KittenViewHolder>(0,
                PerformClickViewWithIdAction(R.id.deleteLayout)
            ))
    }
}
class MainActivityTestRule(
    private val initialNavId: Int
) : ActivityTestRule<MainActivity>(MainActivity::class.java) {

    override fun getActivityIntent(): Intent {
        return Intent(Intent.ACTION_MAIN).apply {
            putExtra(MainActivity.EXTRA_NAVIGATION_ID, initialNavId)
        }
    }
}
java.lang.NullPointerException: Cannot return null from a non-@Nullable @Provides method
at dagger.internal.Preconditions.checkNotNull(Preconditions.java:48)
at net.cassiolandim.kittychallenge.ui.favorites.FavoritesFragmentTest_BindValueModule_ProvidesRepositoryFactory.providesRepository(FavoritesFragmentTest_BindValueModule_ProvidesRepositoryFactory.java:36)
at net.cassiolandim.kittychallenge.ui.favorites.DaggerFavoritesFragmentTest_HiltComponents_ApplicationC.getKittensRepository(DaggerFavoritesFragmentTest_HiltComponents_ApplicationC.java:67)
at net.cassiolandim.kittychallenge.ui.favorites.DaggerFavoritesFragmentTest_HiltComponents_ApplicationC.access$700(DaggerFavoritesFragmentTest_HiltComponents_ApplicationC.java:50)
at net.cassiolandim.kittychallenge.ui.favorites.DaggerFavoritesFragmentTest_HiltComponents_ApplicationC$ActivityRetainedCImpl$ActivityCImpl.getFavoritesUseCase(DaggerFavoritesFragmentTest_HiltComponents_ApplicationC.java:193)
at net.cassiolandim.kittychallenge.ui.favorites.DaggerFavoritesFragmentTest_HiltComponents_ApplicationC$ActivityRetainedCImpl$ActivityCImpl.access$1700(DaggerFavoritesFragmentTest_HiltComponents_ApplicationC.java:175)
at net.cassiolandim.kittychallenge.ui.favorites.DaggerFavoritesFragmentTest_HiltComponents_ApplicationC$ActivityRetainedCImpl$ActivityCImpl$SwitchingProvider.get(DaggerFavoritesFragmentTest_HiltComponents_ApplicationC.java:390)
at net.cassiolandim.kittychallenge.ui.MainViewModel_AssistedFactory.create(MainViewModel_AssistedFactory.java:38)
at net.cassiolandim.kittychallenge.ui.MainViewModel_AssistedFactory.create(MainViewModel_AssistedFactory.java:15)
at androidx.hilt.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:76)
at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.java:69)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:185)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:54)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelProvider.kt:41)
at net.cassiolandim.kittychallenge.ui.favorites.FavoritesFragment.getViewModel(Unknown Source:6)
at net.cassiolandim.kittychallenge.ui.favorites.FavoritesFragment.onCreateView(FavoritesFragment.kt:71)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2699)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:320)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1199)
at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2236)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2009)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1965)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1861)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2641)
at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2589)
at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:2723)
at androidx.fragment.app.FragmentStateManager.activityCreated(FragmentStateManager.java:346)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1200)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1368)
at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState(FragmentManager.java:1446)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1509)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2637)
at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2589)
at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:247)
at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:541)
at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:210)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1391)
at androidx.test.runner.MonitoringInstrumentation.callActivityOnStart(MonitoringInstrumentation.java:714)
at android.app.Activity.performStart(Activity.java:7157)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:2937)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

@ziglee the problem is that ActivityTestRule calls Activity.onCreate() before @Before is called, which means your activity and fragments try to inject themselves before @Before is called, which creates issues because you don't set the @BindValue until @Before method.

Thanks @bcorso. I will use the approach where I declare the module using an object with a method annotated with @Provides inside the test class. That's the only way I think can circumvent this problem of Activity.onCreate() being called before the test dependencies are correctly set.

Was this page helpful?
0 / 5 - 0 ratings