I've seen a few things now where you are trying to inject a viewModel using hilt navigation and it is giving errors. I'm using Hilt Navigation with Compose and I'm having problems with doing UI tests.
This question is the most similar.
I have an abstract viewModel which is being inherited by 5 different viewmodels and the viewModel is scoped to the backstack. Everything works fine on the emulator and an real device, just not in UI tests.
Here is the abstract viewModel:
abstract class ProposeNewGoalViewModel : ViewModel() {
private val proposedGoalUiDataInit = ProposedGoalUiData(
targetAmount = null,
startYear = LocalDateTime.now().year.toString(),
singleOrRecurring = SingleOrRecurring.SINGLE,
goalName = null,
wantWishOrNeed = WantWishOrNeed.NEED,
yearCareProvidedStart = LocalDateTime.now().year.toString()
)
open fun resetState() {
......
}
//methods
.......
}
Here is one of the viewModels:
@HiltViewModel
class Category1OneTimeOrRecurringViewModel @Inject constructor() : ProposeNewGoalViewModel() {
override fun resetState() {
super.resetState()
showRecurrenceFormFields.update { false }
onTargetAmountChanged("")
onStartYearChanged("")
onFrequencyChanged("")
onNumberOfOccurrencesChanged("")
}
}
So nothing earth shattering. Here's where I get the error in my androidTest:
@AndroidEntryPoint
class GoalsFragment() {
@AndroidEntryPoint
class GoalsFragment : BaseFragment() {
.......
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireActivity()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
GoalsNav(
goalsViewModel = goalsViewModel,
proposeNewGoalViewModel = proposeNewGoalScreenViewModel,
popBackStack = { activity?.finish() },
navigateToAccounts = {
landingViewModel.performNavigationSubject.onNext(R.id.accounts)
},
navigateToMyTeamMessage = { messageType ->
findNavController().navigateSafe(
R.id.action_goalsLanding_to_myTeamNewMessage,
MyTeamNewMessageFragmentArgs(
messageType
).toBundle()
)
},
fragmentNavController = findNavController()
)
}
}
}
}
@SuppressLint("NavigateSafe")
@Composable
fun GoalsNav(
goalsViewModel: GoalsViewModel,
proposeNewGoalViewModel: ProposeNewGoalScreenViewModel,
navController: NavHostController = rememberNavController(),
popBackStack: () -> Unit,
navigateToAccounts: () -> Unit,
navigateToMyTeamMessage: (MyTeamMessageType) -> Unit,
fragmentNavController: NavController
) {
EJTheme {
NavHost(
navController = navController,
startDestination = GOALS_ROUTE
) {
////////////////////THIS WORKS FINE IN TEST
composable(route = PROPOSE_NEW_GOAL_ROUTE) { backStackEntry ->
ProposeNewGoalScreen(
viewModel = hiltViewModel<ProposeNewGoalScreenViewModel>(backStackEntry),
navigateBack = { navController.popBackStack() },
onNavigateToGoalType = {
navController.navigate(
"$FILL_OUT_PROPOSED_NEW_GOAL_ROUTE/${it.goalType}"
)
},
)}
/////////////////ILLEGAL STATE
composable(route = "$FILL_OUT_PROPOSED_NEW_GOAL_ROUTE/{$PROPOSE_GOAL_TYPE_ARG}") { backStackEntry ->
val goalTypeArg =
backStackEntry.arguments?.getString(PROPOSE_GOAL_TYPE_ARG)?.toInt()
val selectedGoalTypeEnum = goalTypeArg?.let { GoalType.getGoal(it) }
val proposeNewGoal = selectedGoalTypeEnum?.let { ProposeNewGoal.getGoalBasedOnGoalType(it) }
val proposeNewGoalViewModelForForm = when (proposeNewGoal?.category) {
Category.CATEGORY_1_SINGLE_OR_REOCCURRING -> {
hiltViewModel(backStackEntry) as Category1OneTimeOrRecurringViewModel. ///////error happens here
}
Category.CATEGORY_2_EDUCATION -> {
hiltViewModel(backStackEntry) as Category2EducationViewModel
}
Category.CATEGORY_3_SINGLE_OCCURRENCE -> {
hiltViewModel(backStackEntry) as Category3SingleTimeViewModel
}
Category.CATEGORY_4_PROVIDE_CARE -> {
hiltViewModel(backStackEntry) as Category4ProvideCareViewModel
}
Category.CATEGORY_5_BEQUEST -> {
hiltViewModel(backStackEntry) as Category5LeaveBequestViewModel
}
null -> hiltViewModel(backStackEntry) as Category1OneTimeOrRecurringViewModel
}
ProposeNewGoalForm(
viewModel = proposeNewGoalViewModelForForm,
navigateBack = { navController.navigate(PROPOSE_NEW_GOAL_ROUTE) { popUpTo(PROPOSE_NEW_GOAL_ROUTE) { inclusive = true } } },
onNavigateBackToGoals = navigateToGoals,
validateForm = {
proposeNewGoalViewModelForForm.validateForm()
},
selectedGoalType = goalTypeArg,
onNavigateToConfirmation = { navController.navigate("$FILL_OUT_PROPOSED_NEW_GOAL_ROUTE_CONFIRMATION/$goalTypeArg") },
)
}
}
}
I tried making the abstract class an open class with HiltViewModel and no dice. I think it's a bug.
Test SetUp
@HiltAndroidTest
class ProposeNewGoalNavigationTests {
lateinit var navController: TestNavHostController
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context
get() = ApplicationProvider.getApplicationContext()
private val goalsRepository: GoalsRepo = mock()
private val featureTogglesRepo: FeatureTogglesRepo = mock()
private val userDataRepo: UserDataRepo = mock()
private val staticContentRepo: StaticContentRepository = mock()
private val analyticRepository: AnalyticsRepo = mock()
private val documentsRepo: DocumentsRepo = mock()
private val messagesRepository: MyTeamMessagesRepository = mock()
private val documentUseCase = DocumentDownloadUseCase(
documentsRepo,
mock(),
"EJFileAbsolutePath"
)
private val launchDarklyFlagsRepo: LaunchDarklyFlagsRepo = mock()
@get:Rule
val setupRepoRule = SetupRepoRule()
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Mock
val dispatcherProvider: DispatcherProvider = Mockito.mock()
val goalsRepo: GoalsRepo = mock()
@BindValue
val proposeNewGoalScreenViewModel = ProposeNewGoalScreenViewModel()
@BindValue
val confirmVM = ConfirmNewProposedGoalViewModel(
goalsRepository = goalsRepository,
userDataRepo = userDataRepo,
featureTogglesRepo = featureTogglesRepo,
messagesRepository = messagesRepository,
dispatcherProvider = dispatcherProvider
)
@BindValue
val category1FormVM = Category1OneTimeOrRecurringViewModel()
@BindValue
val category2FormVM = Category2EducationViewModel()
@BindValue
val category3FormVM = Category3SingleTimeViewModel()
@BindValue
val category4FormVM = Category4ProvideCareViewModel()
@BindValue
val category5FormVM = Category5LeaveBequestViewModel()
@Before
fun setupNavHost() {
hiltRule.inject()
val goalViewModel = GoalsViewModel(
goalsRepository = goalsRepository,
featureTogglesRepo = featureTogglesRepo,
userDataRepo = userDataRepo,
savedStateHandle = SavedStateHandle(mapOf(GoalsViewModel.ON_TRACK_CALCULATED to false)),
dispatcher = Dispatchers.IO,
analyticsRepository = analyticRepository,
staticContentRepo = staticContentRepo,
documentDownloadUseCase = documentUseCase,
launchDarklyFlagsRepo = launchDarklyFlagsRepo
)
composeTestRule.setContent {
navController =
TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(DialogNavigator())
navController.navigatorProvider.addNavigator(ComposeNavigator())
GoalsNav(
goalsViewModel = goalViewModel,
proposeNewGoalViewModel =
proposeNewGoalScreenViewModel,
navController = navController,
popBackStack = {},
navigateToAccounts = {},
navigateToMyTeamMessage = {},
fragmentNavController = rememberNavController()
)
}
}
}
2025 UPDATE:
app/src/androidTest/.../MyTest.kt
@RunWith(JUnitParamsRunner::class)
@HiltAndroidTest
class MyKtTest {
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1) val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
@Test
@Parameters(value=[
"false, true",
"true, false",
])
fun fooTest(flag1: Boolean, flag2: Boolean) {
...
We need @HiltAndroidTest and two rules instead of just
val composeTestRule = createComposeRule(), and we heed to create HiltComponentActivity in our project.
Fixes the following error: java.lang.IllegalStateException: Given component holder class androidx.activity.ComponentActivity does not implement interface dagger.hilt.internal.GeneratedComponent or interface dagger.hilt.internal.GeneratedComponentManager
app/src/androidTest/.../HiltComponentActivity.java
package...
import androidx.activity.ComponentActivity;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class HiltComponentActivity extends ComponentActivity {
}
It's just a ComponentActivity annotated with @AndroidEntryPoint
app/src/debug/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:icon="@mipmap/ic_myicon"
>
<activity
android:name=".HiltComponentActivity"
android:exported="true"
android:theme="..."
android:configChanges="..."
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Without this, you will get: java.lang.RuntimeException: Unable to resolve activity for: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=....HiltComponentActivity }
app/src/androidTest/.../CustomTestRunner.kt
package...
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
// A custom runner to set up the instrumented application class for tests.
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
app/build.gradle.kts
...
android {
...
defaultConfig {
...
testInstrumentationRunner = "com.mypackagename.CustomTestRunner"
(without this, you will get
junit.framework.AssertionFailedError: No tests found in ...
or
java.lang.IllegalStateException: Hilt test, ..., cannot use a @HiltAndroidApp application but found .... To fix, configure the test to use HiltTestApplication or a custom Hilt test application generated with @CustomTestApplication.
)
and
dependencies {
androidTestImplementation("com.google.dagger:hilt-android-testing:2.56.2")
kspAndroidTest("com.google.dagger:hilt-android-compiler:2.56.2")
Without this, you will get "Unresolved reference 'testing'." at import dagger.hilt.android.testing.HiltTestApplication