Reputation: 11419
I'm using Jetpack Compose and Hilt.
My Activity
looks like this one:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
MainContent(
navController = navController,
viewModel = viewModel
)
}
}
}
where MainContent
is a Composable
@Composable
fun MainContent(
navController: NavHostController,
viewModel: MyViewModel
) {
...
}
and MyViewModel
is
@HiltViewModel
class MyViewModel @Inject constructor(
@Named(STATE_FEED) private val _state: MutableStateFlow<Boolean>,
) : ViewModel() {
val state: Flow<MyViewState> = _state
...
}
so it is a ViewModel
with a non-empty constructor.
Everything works fine when I run the app.
When I run the UI automated test though:
@HiltAndroidTest
class MainActivityKtTest {
@get:Rule(order = 0)
val hiltTestRule by lazy { HiltAndroidRule(this) }
@get:Rule(order = 1)
val composeTestRule by lazy { createComposeRule() }
private lateinit var navController: TestNavHostController
private lateinit var context: Context
private lateinit var appMockWebServer: MockWebServer
@Before
fun setUp() {
hiltTestRule.inject()
appMockWebServer = MockWebServer()
appMockWebServer.start(BuildConfig.PORT)
context = InstrumentationRegistry.getInstrumentation().context
}
@After
fun tearDown() {
appMockWebServer.shutdown()
}
@Test
fun given_xxx_WHEN_yyy_THEN_zzz() {
appMockWebServer.enqueueSuccess()
composeTestRule.setContent {
val viewModel: MyViewModel = hiltViewModel()
navController = setTestNavController(context)
MainContent(
navController = navController,
viewModel = viewModel
)
}
…
}
I get an error because Hilt doesn't manage to return an instance of the ViewModel. If I change the ViewModel to be with an empty constructor everything works. Is this:
val viewModel: MyViewModel = hiltViewModel()
the right way to inject the ViewModel?
It looks like the problem might be here:
/**
* Returns an existing
* [HiltViewModel](https://dagger.dev/api/latest/dagger/hilt/android/lifecycle/HiltViewModel)
* -annotated [ViewModel] or creates a new one scoped to the current navigation graph present on
* the {@link NavController} back stack.
*
* If no navigation graph is currently present then the current scope will be used, usually, a
* fragment or an activity.
*
* @sample androidx.hilt.navigation.compose.samples.NavComposable
* @sample androidx.hilt.navigation.compose.samples.NestedNavComposable
*/
@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null
): VM {
val factory = createHiltViewModelFactory(viewModelStoreOwner)
return viewModel(viewModelStoreOwner, key, factory = factory)
}
Returns an existing HiltViewModel or creates a new one scoped to the current navigation graph present on the {@link NavController} back stack. If no navigation graph is currently present then the current scope will be used, usually, a fragment or an activity.
In the UI automated test I'm calling the Composable directly so non of these options are available.
If I use:
@get:Rule(order = 1)
val composeTestRule by lazy { createAndroidComposeRule<MainActivity>() }
then the activity is created but this will trigger the call to setContent
and I will end up calling it twice when I call it from the @Test method
Upvotes: 4
Views: 1043
Reputation: 11419
The only solution I found was to add a different Activity
just for the connected tests to be able to use that one in the rule
. This Activity
doesn't call the setContent
in such a way that the test can do it.
val composeTestRule by lazy { createAndroidComposeRule<MainActivityTest>() }
Upvotes: 1