I am trying to write a UI test with Espresso that runs across multiple Fragments. They share a viewModel injected via by activityViewModels().
I am using Android Navigation component, Dagger Hilt injection and a mocked NavController like this:
val navController = Mockito.mock(NavController::class.java)
launchFragmentInHiltContainer<AddRecipeFragment1> {
Navigation.setViewNavController(requireView(), navController)
}
// Here I need to initialize some data in viewModel...
After this I want to navigate to another Fragment that I actually want to test, but neither pressing a button that calls the navigation nor calling navController.navigate(AddRecipeFragment1Directions.toAddRecipeFragment2()) works for me.
An approach using launchFragmentInHiltContainer<AddRecipeFragment>... will restart the whole application and drop the viewModel.
Any ideas?
Meanwhile I found a workaround doing this:
private lateinit var viewModel: AddRecipeViewModel
#Before
fun beforeEach() {
...
navController = Mockito.mock(NavController::class.java)
launchFragmentInHiltContainer<AddRecipeFragment2> {
val viewModelTemp: AddRecipeViewModel by activityViewModels()
viewModel = viewModelTemp
Navigation.setViewNavController(requireView(), navController)
}
}
This way I can at least access the delegated ViewModel directly in my tests.
Related
I am new to writing Android Test Cases. I am trying to write a test case for a project which is already developed. It has so many Activities and Fragments and we are using Navigation Graph for navigating the fragments.
I have started writing test cases from the first fragment and the code is as below:
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
val scenario = launchFragmentInContainer<SplashFragment>()
scenario.onFragment{
// Get Auth Navigation
navController.setGraph(R.navigation.auth_navigation)
Navigation.setViewNavController(it.requireView(), navController)
it.redirectUser()
}
Now my SplashFragment contains few methods which have checks like
fun getAppName(): String {
return (activity as BaseActivity).getAppName()
}
Here, the BaseActivity is the activity which extends AppCompatActivity and it is not defined in my AndroidManifest file.
When I execute the test, I am getting an exception like:
androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity
cannot be cast to com.xxx.ui.base.BaseActivity
Can anyone please help me with this?
In my project I have a splash screen, when it is displayed, my app loading some startup data from server, after loading the data shows another screen.
For splash screen I create a ViewModel, but it stays in memory all the time. How to destroy it correctly?
Thank you for help!
#HiltViewModel
class SplashViewModel #Inject constructor (private val repository: Repository) {
....
}
#Composable
fun SplashScreen(vm: SplashViewModel) {
...
}
#Composable
fun Navigate() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "splash") {
composable("splash") {
SplashScreen(vm = hiltViewModel())
}
composable("main") {
MainScreen(...) // When shows MainScreen, SplashViewModel object still is in memory
}
}
}
Your viewmodel stays in memory because your splash screen is your root destination, and as such it stays always on the stack as the bottom entry.
If you want your splash viewmodel to be automatically destroyed when you leave your splash screen you should pop it from the backstack when you navigate to your main screen, using popUpTo.
Another option you could consider is to make your main screen the root destination and then navigate from that screen to splash if you are starting the app fresh.
Using hiltViewModel and scoping the viewmodel to the nav graph destination as you do will ensure the viewmodel is destroyed when the user leaves that screen, provided it's not in the backstack.
It is not explicitly supported in Android as far as I know. However, you could create a method named onViewModelCleared() inside the viewmodel itself and pass null to all the nullable objects, and something lightweight to non-null objects.
I'm currently new to testing, so I decided to start off with some basic stuff.
I handle all my navigations from a DrawerLayout that is connected to an Activity.
So for my testing I launch an ActivityScenarioRule, create a testNavController object and then I set this testNavController to the current view that handles the navigation (The container fragment).
So the test consists on opening the drawer, clicking on menu item(Will navigate to a fragment) and therefore check if navigated to the fragment.
Then I check if that happened, but the testNavController stays on the same destination which is weird because it performs the click, so I decided to check the navController (The real one inside the activity), and it shows me that navigated to the correct fragment.
Here's the needed code:
#LargeTest
#RunWith(AndroidJUnit4::class)
class MapsActivityTest {
#get:Rule
var activityScenarioRule = ActivityScenarioRule(MapsActivity::class.java)
#Test
fun clickOnDrawerMaps_NavigateToAboutAppFragment() {
//Create TestNavHostController
val testNavController = TestNavHostController(ApplicationProvider.getApplicationContext())
UiThreadStatement.runOnUiThread { // This needed because it throws a exception that method addObserver must be called in main thread
testNavController.setGraph(R.navigation.nav_graph)
}
val scenario = activityScenarioRule.scenario
var navcontroller : NavController? = null
scenario.onActivity {mapsActivity ->
navcontroller = mapsActivity.navController //Get the real navController just to debug
mapsActivity.navController = testNavController //Set the test navController
Navigation.setViewNavController(mapsActivity.binding.containerFragment, testNavController)
}
onView(withId(R.id.drawerLayout)).perform(DrawerActions.open()).check(matches(isOpen()))
onView(withId(R.id.aboutAppFragment)).perform(click())
assertThat(testNavController.currentDestination?.id).isEqualTo(R.id.aboutAppFragment)
}
}
In the example they use a Fragment, which they set the fragment.requireView() on the launch of the fragment, but I think it's exactly the same.
What am I doing wrong here?
When you use ActivityScenario (or ActivityScenarioRule), your activity is brought all the way up to the resumed state before any onActivity calls are made. This means that your real NavController has already been created and used when you call setupWithNavController. This is why your call to setViewNavController() has no effect.
For these types of integration tests (where you have a real NavController), you should not use TestNavHostController.
As per the Test Navigation guide, TestNavHostController is designed for unit tests where you do not have any real NavController at all, such as when testing one fragment in isolation.
I have an app with one activity and about 29 Fragment in the nav_graph, two of these fragments are used for authentication, and I need to share data between these two fragments using a shared view model but not with the other fragments.
So, I created my ViewModel and instantiated it in both fragments using the viewModels() which is a part of fragment-ktx library.
private val viewModel: AuthViewModel by viewModels()
However, once I navigate to the second fragment using the findNavController().navigate() I lose all the data in the AuthViewModel
AuthViewModel.kt
class AuthViewModel #ViewModelInject constructor(
private val authRepository: AuthRepository
) : BaseViewModel()
Is there any additional step I'm missing here?
EDIT
I'm accessing the data from the onViewCreated method
When you create a viewmodel by viewmodels() yout get a reference to the ViewModel scoped to the current Fragment.
So in your case you would be using private val viewModel: AuthViewModel by viewModels() in both the fragment which gives you two different instance of viewmodel tied to each fragment.
The concept of Shared Viewmodel need a Shared Scope like Activity or NavGraph.
Using Activity
Just change
private val viewModel: AuthViewModel by viewModels()
to
private val viewModel: AuthViewModel by activityViewModels()
Using NavGraph
Create another nav graph where you have two fragments which are used for authentication.
<navigation android:id="#+id/authenticationNavGraph"
app:startDestination="#id/chooseRecipient">
<fragment
android:id="#+id/authentication1Fragment"
android:name="com.example.AuthFragment1"
android:label="Fragment 1">
</fragment>
<fragment
android:id="#+id/authentication2Fragment"
android:name="com.example.AuthFragment2"
android:label="Fragment 2" />
</navigation>
Now If you want to use same viewmodel AuthViewModel then you can create a viewmodel using:
private val viewModel: AuthViewModel by navGraphViewModels(R.id.authenticationNavGraph)
I'm collaborating with ViewModel and fragments, and would like to retain my ViewModel for my fragment on rotation change. When passing my Fragment into ViewModelProviders.of() it does not get retained, but when I pass the Activity that the fragment belongs to, it is retained. So is passing the activity how it is supposed to be used?
Calling ViewModelProviders.of(this) in Fragment won't retain my ViewModel. Is that expected behavior?
class MainFragment : Fragment() {
private lateinit var viewModel: MainViewModel
fun OnXXXXXXXXX {
// This _will NOT_ retain ViewModel
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
// This _will_ retain ViewModel
viewModel = ViewModelProviders.of(activity).get(MainViewModel::class.java)
}
}
Yes, its expected behaviour, Look at this content
Fragments can share a ViewModel using their activity scope to handle this communication
If you want to share same ViewModel, use same context. For Example, multiple fragments on same activity:
ViewModelProviders.of(activity)