I followed single activity pattern for my app. My MainActivity has a bottom navigation with 4 fragments.
nav_home and nav_video have same fragment so I want to HomeViewModel and VideoViewModel keep their states within these graphs. As you can see below.
nav_home.xml
nav_video.xml
On my fragment I use hiltNavGraphViewModels extension to keep viewmodel states.
#AndroidEntryPoint
class CourseDetailFragment : Fragment(R.layout.fragment_course_detail),
LoadingDelegate by LoadingDelegateImpl(), NavigationDelegate by NavigationDelegateImpl() {
private val binding by viewBinding(FragmentCourseDetailBinding::bind)
private val viewModel: CourseDetailViewModel by viewModels()
private val categoryViewModel: CategoryViewModel by hiltNavGraphViewModels(R.id.nav_video)
private val homeViewModel: HomeViewModel by hiltNavGraphViewModels(R.id.nav_home)
If I reach this fragment from home, categoryViewModel throws exception and it says:
java.lang.IllegalArgumentException: No destination with ID 2131362218
is on the NavController's back stack. The current destination is
Destination(com.example.app:id/courseDetailFragment)
label=CourseDetailFragment
class=com.example.app.ui.course.CourseDetailFragment
On the other hand, if I reach this fragment from video, homeViewModel throws same exception.
Any help would be most welcomed. Thanks.
Maybe you can use this:
private val sampleViewModel: SampleViewModel by hiltNavGraphViewModels(findNavController().graph.id)
However in my case, the vm is lazy initialized even before onCreate() and findNavController() is not yet available at that time.
Here's what I did:
fun provideVM(): SampleVM =
hiltNavGraphViewModels<SampleVM>(findNavController().graph.id).value
provideVM() is called during onViewCreated()
Related
i have 2 fragments on the screen, how can i notify the 2nd that something happened on the 1st ?
now I am using this solution:
companion object {
private val onFullScreenPressedEvent = SingleLiveEvent<Boolean>()
val onFullScreenPressed: LiveData<Boolean> = onFullScreenPressedEvent
}
and observe onFullScreenPressed from second fragment
Your best bet would be to use a ViewModel tied to the Activity of the Fragments (so shared between fragments), and modify the liveData there.
example Viewmodel
class MyActivityViewModel : ViewModel(){
private val onFullScreenPressedEvent = SingleLiveEvent<Boolean>()
val onFullScreenPressed: LiveData<Boolean> = onFullScreenPressedEvent
fun onFullScreen(){
onFullScreenPressedEvent.call()
}}
For example, by using implementation "androidx.fragment:fragment-ktx:1.4.0" you can do in fragment A
private val viewModelA: MyActivityViewModel by activityViewModels()
and call the code that will change the event viewModelA.onFullScreen()
Then in Fragment B
private val viewModelB: MyActivityViewModel by activityViewModels()
and observe the liveData you trigger in Fragment A viewModelB.onFullScreenPressed.observe(viewLifecycleOwner, myObserver)
I have a parent Fragment A with Fragment B and Fragment C inside a ViewPager. I want to share Fragment's A ViewModel within Fragment B and Fragment C.
This was my approach which seemed intuitive at first:
In parent Fragment A which contains ViewPager, I create my viewModel like this:
val viewModel by viewModels<SharedViewModel>()
...
#HiltViewModel
class SharedViewModel #Inject constructor(
stateHandle: SavedStateHandle
) {
val myArgument = stateHandle.get<Boolean>("myArgument")
...
}
My ViewPager Fragment's B and C are trying to access same SharedViewModel instance like this:
private val sharedViewModel by viewModels<SharedViewModel>(
ownerProducer = { requireParentFragment() }
)
Everything works fine, except when the process is destroyed, my savedStateHandle in the SharedViewModel does not retain arguments passed by navigation component. Doing this in SharedViewModel returns null and savedStateHandle is empty.
val myArgument = stateHandle.get<Boolean>("myArgument")
Does someone know why is this happening? Using activityViewModels seems to fix this, but I do not fully understand why.
I'm using a shared view model like here
But the problem is that when I clear my last fragment, I want to clear the viewmodel, or kill its instance, but somehow it survives when I leave the last fragment that uses it
How can I programatically clear this viewmodel ?
I use it like this
Fragment A
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated() {
model.getTotal().observe(viewLifecycleOwner, Observer { cartTotal ->
total = cartTotal
})
}
From fragment B I sent the total
Fragment B
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated() {
model.setTotal = 10
}
But when leaving Fragment A with that data (doing popBackStack since I'm using navigation components) it does not clear the viewmodel, instead when I open again my fragment , the data stills there
I suspect that the viewmodel is tied with my Container Activity and not the lifecycle of the fragments itself, so
How can I remove the instance or clear my viewmdel when I hit my last fragment ?
Thanks
If you want to get a ViewModel associated with a parent fragment, your inner fragment should follow the by viewModels JavaDoc and use:
val viewmodel: MYViewModel by viewmodels ({requireParentFragment()})
This says to use the parent Fragment as the owner of your ViewModel.
(The parent fragment would use by viewModels() as it is accessing its own ViewModels)
you can also clear viewModelStore manually after Fragment A destroyed.
something like this :
override fun onDetach() {
super.onDetach()
requireActivity().viewModelStore.clear()
}
then your viewModel instance will be cleared. for checking this work you can debug onCleared method of your viewModel.
I'm trying to share a ViewModel between my activity and my fragment. My ViewModel contains a report, which is a complex object I cannot serialize.
protected val viewModel: ReportViewModel by lazy {
val report = ...
ViewModelProviders.of(this, ReportViewModelFactory(report)).get(ReportViewModel::class.java)
}
Now I'm trying to access the viewmodel in a fragment, but I don't want to pass all the factory parameters again.
As stated by the ViewModelProvider.get documentation:
Returns an existing ViewModel or creates a new one in the scope
I want to access the ViewModel instance defined in the activity, so I tried the following but it logically crashes as the model doesn't have an empty constructor:
protected val viewModel: ReportViewModel by lazy {
ViewModelProviders.of(requireActivity()).get(ReportViewModel::class.java)
}
How one should access its "factorysed" ViewModels in a fragment? Should we pass the factory to the fragment?
Thanks!
A little late but I had this question myself. What I found is you can do the following:
In your activity override getDefaultViewModelProviderFactory() like so:
override fun getDefaultViewModelProviderFactory(): ReportViewModelFactory {
return ReportViewModelFactory(report)
}
now in your fragments you can do
requireActivity().getDefaultViewModelProviderFactory()
to get the factory.
Or simply instantiate your viewModel like:
private val viewModel: ReportViewModel by activityViewModels()
I have my Activity MainActivity.kt .
And and one ViewModel MainActivityViewModel.kt
And I want to observe my live data to my 3 different fragments.
class MainActivity{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
But my observer only work on activity not on the fragments.
Hey there you are doing everything greate except one thing you should use requireActivity() instead on this in your fragment class.
Make sure your all fragment are attached to your viewModel holding Activity.
class MainActivity{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(requireActivity(), viewModelFactory).get(MainActivityViewModel::class.java)
}
This will help you solve your issue.
For further detail view this.
The ViewModelProviders.of has 2 different constructors:
of(Fragment fragment, ViewModelProvider.Factory factory)
Creates a ViewModelProvider, which retains ViewModels while a scope of
given fragment is alive.
of(FragmentActivity activity, ViewModelProvider.Factory factory)
Creates a ViewModelProvider, which retains ViewModels while a scope of
given Activity is alive.
Basically when you used this as the first parameter in your activity, you passed the context of the activity and created a viewmodel that will be alive in the scope of the activity, however your second this is the context to your fragment, meaning that the second ViewModel will be alive as long as your fragment is alive (only one fragment).
What instead you should be doing in your fragment is using the context of the activity, since activity is always alive when fragments are attached and swapped. You should change your fragments to:
class MainFragmentOne{
lateinit var mainActivityViewModel: MainActivityViewModel
...
mainActivityViewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(MainActivityViewModel::class.java)
}
or you can use the requireActivity() method that was the previous answer.
To achieve what you are trying to do, you need three things. An activity/fragment that will post the value to the ViewModel, a ViewModel, and an activity/fragment that will retrieve the data from the ViewModel. Lets say your data is stored in an ArrayList, and you want to update and retrieve it from different fragments.
First, we have to implement a ViewModel. It contains the data you want to share between your activities/fragments. You declare the MutableLiveData as an ArrayList then initialize it.
class testviewmodel : ViewModel() {
val list: MutableLiveData<ArrayList<String>> = MutableLiveData()
init {
list.value = arrayListOf()
}
}
Our next step is to access and update the ArrayList using your activity:
val viewmodel = ViewModelProviders.of(this).get(testviewmodel::class.java)
// update the array in Viewmodel
viewmodel.list.postValue(yourarray)
If you are using a Fragment to update it, use this:
activity?.let {
val viewmodel = ViewModelProviders.of(it).get(testviewmodel::class.java)
// update the array in Viewmodel
viewmodel.list.postValue(yourarray)
}
Finally, to retrieve the data from the ViewModel in a fragment, put this inside your onViewCreated:
activity?.let {
val viewmodel = ViewModelProviders.of(it).get(Dbviewmodel::class.java)
observeInput(viewmodel)
}
Put this outside of your onViewCreated:
private fun observeInput(viewmodel: testviewmodel ) {
viewmodel.list.observe(viewLifecycleOwner, Observer {
it?.let {
if (it.size > 5) {
pos = it[5]
//grab it
Toast.makeText(context,pos,Toast.LENGTH_LONG).show()
//display grabbed data
}
}
})
}
Take a look at this docs for more information about ViewModels
Good Luck! I hope this helps :)
That's because you are using the fragment 'this' instance, and not the activity.
Replace
mainActivityViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java)
With
activity?.let { mainActivityViewModel = ViewModelProviders.of(it, viewModelFactory).get(MainActivityViewModel::class.java) }