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.
Related
I'm kind of new in this Android Architecture, and I'm trying to understand the use of ViewModel.
Currently I have an activity with a fragment embedded in its xml. The fragment just have an edittext whose content I want to persist when the phone rotates. I've implemented the Viewmodel, livedata and observers correctly (I suppose), and attached the ViewModel to the fragment.
The problem comes here, when I rotate the phone the fragment is recreated and the information, gone. But, if I attached the ViewModel to the activity the app works as intended.
So, after some background info, the question is, Why would I want to attach the ViewModel to a fragment if the Viewmodel object is cleared when the fragment is recreated/destroyed?
Thanks
Ps. Here is how I attached the Viewmodel to the activity, this way it works as intended
Fragment
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
...
final MuestrasViewModelFactory mvmFactory = new MuestrasViewModelFactory(new String[]{"hello","world","again"});
final MuestrasViewModel mvm = new ViewModelProvider(requireActivity(), mvFactory).get(MuestrasViewModel.class);
... //here comes the observers
Imagine that you have 2 fragments connected to one (or more) viewModel(s) and inside of activity you'll switch between them. Once you open fragment, viewModel works as expected, so I start listening for changes from onCreate method, code example:
viewModel = new ViewModelProvider(requireActivity(), new InventoryTasksFactory()).get(InventoryTasksViewModel.class);
viewModel.inventoryTasksResponse().observe(this, new Observer<Response<List<InventoryTask>>>() {
#Override
public void onChanged(Response<List<InventoryTask>> listResponse) {
handleResponse(listResponse);
}
});
But when you switching to another fragment and going back, fragment becomes blank. I understand that fragment listening changes inside of viewModel, and you should manually getting value from viewModel and I get value from viewModel inside of onCreateView method, code example:
Response<List<InventoryTask>> inventory = viewModel.inventoryTasksResponse().getValue();
if (inventory!=null){
handleResponse(inventory);
}
Problem is that Response has 3 states: Running, Success, Error, and depends on those states view is updating. So, in first fragment opening, view updating twice and it leads to skipping frames and display blinking.
I was thinking about keeping data inside of fragment, but I want to avoid data duplicating. Besides of that, in case of sharedViewModel, you'll get issues about updating data inside of fragment!
Please, help me!
Observing your data from onViewCreated(View view, Bundle savedInstanceState) might work out.
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();
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
While Navigation component of JetPack looks pretty promising I got to a place where I could not find a way to implement something I wanted.
Let's take a look at a sample app screen:
The app has one main activity, a top toolbar, a bottom toolbar with fab attached.
There are 2 challenges that I am facing and I want to make them the right way.
1. I need to implement fragment transactions in order to allow replacing the fragment on the screen, based on the user interaction.
There are three ways I can think of and have this implemented:
the callbacks way. Having a interface onFragmentAction callback in fragment and have activity implement it. So basically when user presses a button in FragmentA I can call onFragmentAction with params so the activity will trigger and start for example transaction to replace it with FragmentB
implement Navigation component from JetPack. While I've tried it and seems pretty straightforward, I had a problem by not being able to retrieve the current fragment.
Use a shared ViewModel between fragment and activity, update it from the fragment and observe it in the activity. This would be a "replacement" of the callbacks
2. Since the FAB is in the parent activity, when pressed, I need to be able to interact with the current visible fragment and do an action. For instance, add a new item in a recyclerview inside the fragment. So basically a way to communicate between the activity and fragment
There are two ways I can think of how to make this
If not using Navigation then I can use findFragmentById and retrieve the current fragment and run a public method to trigger the action.
Using a shared 'ViewMode' between fragment and activity, update it from activity and observe it in the fragment.
So, as you can see, the recommended way to do navigation would be to use the new 'Navigation' architecture component, however, at the moment it lacks a way to retrieve the current fragment instance so I don't know how to communicate between the activity and fragment.
This could be achieved with shared ViewModel but here I have a missing piece: I understand that fragment to fragment communication can be made with a shared ViewModel. I think that this makes sense when the fragments have something in common for this, like a Master/Detail scenarion and sharing the same viewmodel is very useful.
But, then talking between activity and ALL fragments, how could a shared ViewModel be used? Each fragment needs its own complex ViewModel. Could it be a GeneralViewModel which gets instantiated in the activity and in all fragments, together with the regular fragment viewmodel, so have 2 viewmodels in each fragment.
Being able to talk between fragments and activity with a viewmodel will make the finding of active fragment unneeded as the viewmodel will provide the needed mechanism and also would allow to use Navigation component.
Any information is gladly received.
Later edit. Here is some sample code based on the comment bellow. Is this a solution for my question? Can this handle both changes between fragments and parent activity and it's on the recommended side.
private class GlobalViewModel ():ViewModel(){
var eventFromActivity:MutableLiveData<Event>
var eventFromFragment:MutableLiveData<Event>
fun setEventFromActivity(event:Event){
eventFromActivity.value = event
}
fun setEventFromFragment(event:Event){
eventFromFragment.value = event
}
}
Then in my activity
class HomeActivity: AppCompatActivity(){
onCreate{
viewModel = ViewModelProviders.of(this, factory)
.get(GlobalViewModel::class.java)
viewModel.eventsFromFragment.observe(){
//based on the Event values, could update toolbar title, could start
// new fragment, could show a dialog or snackbar
....
}
//when need to update the fragment do
viewModel.setEventFromActivity(event)
}
}
Then in all fragments have something like this
class FragmentA:Fragment(){
onViewCreated(){
viewModel = ViewModelProviders.of(this, factory)
.get(GlobalViewModel::class.java)
viewModel.eventsFromActivity.observe(){
// based on Event value, trigger a fun from the fragment
....
}
viewModelFragment = ViewModelProviders.of(this, factory)
.get(FragmentAViewModel::class.java)
viewModelFragment.some.observe(){
....
}
//when need to update the activity do
viewModel.setEventFromFragment(event)
}
}