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)
Related
I have a navigation as follows:
FragmentList -> FragmentDetailA -> FragmentDetailB -> FragmentDetailC
I use a viewModel for the detail FragmentDetailViewModel
private val detailViewModel: DetailViewModel by activityViewModels()
But if I go forward, and then back, the above fragmentDetails are changed. How to have a viewmodel assigned to the fragment without changing the others?
Solution:
First of all, change activityViewModels() for viewModels()
Then, the problem was with setFragmentResultListener. It was called before instantiating the new fragment, then the call was made on Fragment A and not on Fragment B. I decided to pass data between destinations with Bundle objects.
Thanks a lot
With this way that you initialized view model, view model depends on the parent activity you must use this way
private val detailViewModel: DetailViewModel by viewModels()
to your view model instance depends on your fragment instance.
It sounds like you want to link the viewmodel to the fragment.
ViewModelProvider(this,viewModelFactory).get(MyViewModel.class)
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.
How do I get my Fragments to share the same instance of a ViewModel?
In my ViewModel I need application context so I use "AndroidViewModel" but my MainFragment and DialogFragment don't share the same instace of ViewModel.
How can I achive this?
Here is how I initialized my ViewModels in my MainFragment and DialogFragment:
mViewModel = ViewModelProvider.AndroidViewModelFactory.
getInstance(getActivity().getApplication())
.create(MyViewModel.class);
I am using the Navigation Component of Android Jetpack (2.2.0-alpha01).
I wish to use a child NavHostFragment nested inside my main NavHostFragment, equipped with its own child nav graph. Please view the following image for context:
The child nav host is defined like this inside the fragment that is at the front of the MainNavHost's stack:
<fragment
android:id="#+id/childNavHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="false"
app:navGraph="#navigation/child_graph" />
Inside the fragment that is at the front of the CHILD Nav Host Fragment, I am trying to get a ViewModel scoped to the R.navigation.child_graph by using the following code:
private val childGraphScopedViewModel: ChildGraphScopedViewModel by navGraphViewModels(R.navigation.child_graph) {
viewModelFactory
}
When accessing the childGraphScopedViewModel, I am getting a crash with the error message:
java.lang.IllegalArgumentException: No NavGraph with ID 2131689472 is on the NavController's back stack.
I believe the lazy init call by navGraphViewModel() is looking for the navgraph inside the mainGraph.
How can I access a child navHostFragment scoped ViewModel? Thank you for your time.
This works for me without defining aby custom ViewModelProvider (2.2.0):
private val viewModel: ChildGraphScopedViewModel by navGraphViewModels(R.id. child_graph)
An Easy mistake to make is to use R.navigation.child_graph (bad) instead of R.id.child_graph (good)
You can do that by providing the viewModelStore of child NavController
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
val childHostFragment = childFragmentManager
.findFragmentById(R.id.childNavHostFragment) as NavHostFragment
val childNavController = childHostFragment.navController
val childViewModel: ChildGraphScopedViewModel = ViewModelProvider(
childNavController.getViewModelStoreOwner(R.navigation.child_graph)
).get(ChildGraphScopedViewModel::class.java)
}
I wrote a Kotlin Extension for making it easier
inline fun <reified T: ViewModel> NavController.viewModel(#NavigationRes navGraphId: Int): T {
val storeOwner = getViewModelStoreOwner(navGraphId)
return ViewModelProvider(storeOwner)[T::class.java]
}
Usage
val viewModel = findNavController().viewModel(R.navigation.nav)
For some reason creating the nav graph and using include to nest it inside the main nav graph was not working for me. Probably it is a bug.
You can select all the fragments that need to be grouped together inside the nav graph and right-click->move to nested graph->new graph
now this will move the selected fragments to a nested graph inside the main nav graph like this:
<navigation app:startDestination="#id/homeFragment" ...>
<fragment android:id="#+id/homeFragment" .../>
<fragment android:id="#+id/productListFragment" .../>
<fragment android:id="#+id/productFragment" .../>
<fragment android:id="#+id/bargainFragment" .../>
<navigation
android:id="#+id/checkout_graph"
app:startDestination="#id/cartFragment">
<fragment android:id="#+id/orderSummaryFragment".../>
<fragment android:id="#+id/addressFragment" .../>
<fragment android:id="#+id/paymentFragment" .../>
<fragment android:id="#+id/cartFragment" .../>
</navigation>
</navigation>
Now, inside the fragments when you initialize the ViewModel do this
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
If you need to pass the viewmodel factory(may be for injecting the viewmodel) you can do it like this:
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph) { viewModelFactory }
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)