Use viewmodel for a fragment called twice - android

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)

Related

Shared ViewModel between Fragments with viewPager2

I have a Fragment0 which contains a ViewPager2 that internally may contain 1 or more child Fragments.
I have created a SharedViewModel and defined it in the Parent fragment like so:
val viewModel by viewModels<SharedViewModel>()
I have also added definition for the viewModel in each of the child Fragments.
private val sharedViewModel by viewModels<SharedViewModel>(
ownerProducer = { requireParentFragment() }
)
I use the NavigationControl to navigate to Fragment4 from each of these Child Fragments viz. Fragment1, Fragment2 and Fragment3. My question is, how can I share the same view Model with Fragment4 which is not directly a child of the Fragment0. Can I use the same approach to share viewModel with Fragment4 as well?
Or is there a better way to handle such a usecase?
You can use by viewModels({requireParentFragment()}) only to share viewmodel with parent and child fragment.
It will not work with fragment4.
you can use by activityViewModels().
but it's not good architecture. i suggest you to create separate viewmodel for Fragment4 and share the data with navigation.
find more from here: https://developer.android.com/guide/fragments/communicate

The Fragment refreshes on back- Android Navigation

I am making an application using the Android Navigation component. But I ran into a very fundamental problem which can cause problems in the whole development of my application.
The Scenario
I have this Fragment where in onViewCreated I am observing a field from my viewmodel.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(EventDetailsViewModel::class.java)
viewModel.init(context!!,eventId)
viewModel.onEventDetailsUpdated().observe(this, Observer {
setEventDetails(it)
})
}
And in the setEventDetails method, I set recyclerviews with the data.
The PROBLEM
This fragment is a long fragment with a scroll. Suppose I scroll long way down to a section and click on a button which takes me to another fragment.
But when I come back to this fragment, it again takes me to the top and does everything that it did on first load.
That can be troubling. It is kind of recreating the whole fragment instead of keeping its old state.
What I tried
I searched a lot of questions. And went through This Github Query, This SO question, Another Git... But I could not solve my problem.
Please help, Thanks in advance.
Yes, Fragment's view will get destroyed whenever you navigate forward to another fragment.
RecyclerView's scroll position should be automatically restored, even when new instance of RecyclerView is created and new Adapter instance is set, as long as you setup everything with the same dataset as before. Also, you need to do it before the first layout pass.
This means that you need your old data and you need to have it ready immediately (no async loads!).
ViewModelProvider should return the same ViewModel instance. That ViewModel holds the data you should be able to synchronously get and display on the UI. Make sure to refactor your viewModel.init method - you don't want to make API call if data is already there in case when going back. A simple boolean isInitialized can work here, or you can even check if LiveData is empty or not.
Also, you have a subtle bug when calling observe on LiveData. onViewCreated can be called many times for the same fragment (each time you navigate forward and back!) - so observe will be called each time. Your Fragment will be subscribed many times to the same LiveData. This means you will get events multiple times (once for each subscription). This can cause issues with RecyclerView state restoration too. Your subscription is tied to Lifecycle owner you passed. You passed Fragment's Lifecycle owner which is tied to Fragment's lifecycle. What you want to do is pass Fragment view's lifecycle owner, so whenever the view is destroyed the subscription gets cleared, and you only have 1 subscription ever and only while the Fragment's view is alive. For this, you can use getViewLifecycleOwner instead of this.
You need to rely on ViewModel to restore the fragment state because ViewModel doesn't get destroyed on fragment change.
In your viewModel, create a variable listState
class HomeViewModel : ViewModel() {
var listState: Parcelable? = null
}
Then in your fragment use below code
class HomeFragment : Fragment() {
private val viewModel by navGraphViewModels<HomeViewModel>(R.id.mobile_navigation)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (viewModel.listState != null) {
list.layoutManager?.onRestoreInstanceState(viewModel.listState)
viewModel.listState = null
}else{
//load data normally
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.listState = list.layoutManager?.onSaveInstanceState()
}
}
You don't have to initialize the view model each time. Just check for null before initializing. Don't know kotlin, still it will be something like:
if(viewModel == null){
viewModel = ViewModelProviders.of(this).get(EventDetailsViewModel::class.java)
viewModel.init(context!!,eventId)
}
try putting this code where you first call your fragment.
ft = fm.beginTransaction();
ft.replace(R.id.main_fragment, yourSearchFragment, "searchFragment");
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commit();
and this when going back to the fragment
ft = fm.beginTransaction();
ft.hide(getFragmentManager().findFragmentByTag("searchFragment"));
ft.add(R.id.main_fragment, yourDetailfragment);
ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commit();

How to have separate instance of ViewModel in each Fragment and have shared data as well between Viewmodels

I am working on Android TV app
And I have a Fragment(main Fragment) and inside fragment have side menu, each menu Item creates new fragment(menu Fragment).
I have Viewmodel and in ViewModel I have config livedata which I am loading when I main Fragment is created.
And Menu Fragment's data is based on data I am getting from API calls and from config data
I have created single instance of viewmodel with the activity lifecycle.
But the problem is when I am navigating from one fragment to another for example from 1-2 fragment and as 1 fragment has already loaded data the livedata is not empty and navigating to 2nd fragment before fetching the second fragment data it observes/displays the livedata from first fragment and then it's own after it fetches it's data
I think each fragment should have it's own instance ,but I also need data which should be shared (the config) between the each instance of viewmodel ?
How can I make this?
private val viewModel: HomeViewModel by lazyViewModelActivityScope()
viewModel.fetch()
viewModel.configData.observe(viewLifecycleOwner, Observer { it ->
loadData(it)
})
it is inside MenuFragment
private val viewModel: HomeViewModel by lazyViewModelActivityScope()
viewModel.fetchMenuPage(menuItem)
viewModel.carouselsWithAssetsData.observe(viewLifecycleOwner, Observer { carouselWithAssets ->
carouselWithAssets.forEach { carouselWithAsset ->
mRowsAdapter.add(createCardRow(carouselWithAsset))
}
})
I got the answer of my question and I want to share it with others
I think generally this is something you'd manage with DI (injecting the same config into both ViewModels)
Or each Fragment would need to pass the activity ViewModel's data to the Fragment's ViewModel. Or you could have a global LiveData that manages itself (loading data in onActive()) and skip the activity ViewModel entirely

Retaining ViewModel in fragments

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)

Support Fragment Manager .replace() and ViewModel

It appears that whenever you use the .replace() method within a transaction with the Support Fragment Manager the ViewModel is recreated. Is this intentional? The Fragment instance itself is not changing and the ViewModel will be (partially) preserved during rotation/configuration changes.
I'm seeing the following scenarios:
Get View model ref (count = 0), update count = 1, rotate, count = 1, onCreate called again and count = 0 (view model recreated).
Call .replace() and view model is recreated (activity and fragment instances unchanged).
Using support library 26.0.0.
The ViewModel is being created in onCreate of my fragment and is scoped to the Fragment:
viewModel = ViewModelProviders.of(this).get(DashboardViewModel::class.java)
Anyone shed some light on if this is normal?
As #CommonsWare mentioned, the viewmodel instance inside fragment should be the same in Activity.
Therefore, inside the Activity , you should do something like this
MyViewModel vm = ViewModelProviders.of(this).get(MyViewModel.class);
Inside the Fragment , you should do something like this
MyViewModel vm = ViewModelProviders.of(getActivity()).get(MyViewModel.class);
In result, they will use the same instance.
However, if you try to use it inside the fragment
MyViewModel vm = ViewModelProviders.of(this).get(MyViewModel.class);
The Viewmodel will be recreated in the fragment when you rotate the device. Since the instance is preserved inside the fragment not the activity, when the activity recreated, the fragment will also be recreated, and the MyViewModel instance.
Try to take a look on the example on master detail fragment (which might be easy to resolve your problem) ViewModel in Android Developer
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends LifecycleFragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// update UI
});
}
}
I have also made the simple Master Detail template on the github
SimpleDetailActivity.java
SimpleDetailFragment.java
replace() is suppossed to be called when you make different fragment. For the same fragment you call update().
replace() method means you can replace your current Fragment with something different fragment, which has something different layout (physical structure). You can't even guarantee that it starts from the same memory that the previous fragment used. ViewModel is kind of layout for whole container. So, for the object with different physical structure and (possibly with different memory -- I am writing possibly because you can replace with the same fragment also), you have to recreate different ViewModel to define it's container. This is because one ViewModel object points one reference Container, next time you have different fragment, your fragment container is defined by ViewModel is somewhere else, so you need another ViewModel object to point that Fragment Container.
But when you do update(), or rotate(), you guarantee the updated fragment's memory space can decrease/increase but still it's starting memory remains same. So no need to create the ViewModel. This is because your old ViewModel object is referencing the same old Fragment's container.
When you do create(), it creates GUI everything, so, obviously, there happen ViewModel creation again.
The tracking of ViewModel count is based on the above explanation.
The ViewModel is being created in onCreate of my fragment and is scoped to the Fragment. This is kind of power delegation to the fragment.

Categories

Resources