10

I am following the MVVM pattern - meaning I have a ViewModel for each Fragment.

I added two tabs by using ViewPager2.

My adapter looks like this:

@Override
public Fragment createFragment(int position) {
    switch (position) {
        case 0:
            return new MergedItemsFragment();
        case 1:     
            return new ValidatedMergedItemsFragment();
    }
    return new MergedItemsFragment();
}

The tabs are working. However, I noticed that the ViewModel of my MergedItemsFragment is behaving weirdly. Before I added tabs I navigated to the Fragment like this:

NavHostFragment.findNavController(this).navigate(R.id.action_roomFragment_to_itemsFragment);

When I left that fragment with NavHostFragment.findNavController(this).popBackStack() and later on returned to that fragment I would get a new empty ViewModel. This was intended.

With the new approach I am navigating with return new MergedItemsFragment(). When I leave that fragment and later on return I am getting a ViewModel that contains the old data. That is an issue because the old data is not relevant anymore because the User selected different data in another fragment.


Update #1

I realized that he actually keeps all the old Fragments in memory because the same print statements gets called multiple times. The times it is called increases with the amount of times I leave and return to that screen. So if I leave and return 10 times and rotate my device he will actually execute one line 10 times. Any guesses how to implement Tabs/ViewPagers with Navigation Components in a manner that works with ViewModels?


Update #2

I set my ViewModels like this:

viewModel = new ViewModelProvider(this, providerFactory).get(MergedItemViewModel.class)

I get the same results with:

viewModel = ViewModelProviders.of(this).get(MergedItemViewModel.class);

I bind the ViewModel in the Fragment itself. Therefore, this is the Fragment.

user123456789
  • 301
  • 1
  • 4
  • 15
  • Can you show how you are setting your ViewModels? Also, is there any reason you can't just create a new ViewModel when you get new data? – BlackHatSamurai Feb 18 '20 at 15:26
  • I've updated my question. Isn't the point of a viewmodel that it takes care of that itself? I create it once and it persists for one fragment. How exactly would I recreate it in case I have new data and why didn't I need to do that previously? – user123456789 Feb 18 '20 at 16:42
  • You didn't need to do it before because the fragment was destroyed. Now you are using a ViewPager and It stores the fragment in memory. I would suggest just clearing the data when you need to. You need to manage the data in the VM, rather than the VM itself. – BlackHatSamurai Feb 18 '20 at 16:43
  • The problem I have is that the old VMs are actually still serving old LiveData and feeding the other components old data. So clearing the data won't do because the old VMs keep interfering. Example: I clear a list in the current ViewModel. However the screen still gets the old list. When I debug the ViewModel and check the length of the List it says 0 - since it has been cleared. The only logical explanation are other ViewModels serving old data. – user123456789 Feb 18 '20 at 17:12
  • Are you using the same VM for each of the fragments? Or does each fragment have it's own VM? – BlackHatSamurai Feb 18 '20 at 19:27
  • I am using a distinct VM for the MergedItemsFragment and a shared VM for the ValidatedMergedItemsFragment (scoped to the acitivity). But I think the bug persists even if I remove the second fragment alltogether. – user123456789 Feb 18 '20 at 20:02
  • I was also having this issue, actually, my View pager was inside a fragment and while initializing my Adapter I was passing "activity.supportFragmentManager" but I need to pass "childFragmentManager". Can you confirm this – Rakshit Nawani Feb 19 '20 at 12:28
  • Yes, my ViewPager is within a Fragment. I create my Adapter like this: `viewPager2.setAdapter(new ViewPagerFragmentAdapter(getActivity()));` So I am passing the acitivtiy itself (I am using a single activity pattern). How do I pass the childFragmentManager? The constructor requires a FragmentActivity. Thanks! – user123456789 Feb 19 '20 at 17:35
  • @user123456789 - you should pass in the `Fragment` (i.e., `this`) instead of the `Activity`. That's what uses the `childFragmentManager`. – ianhanniballake Feb 20 '20 at 02:15
  • @user123456789 change your **ViewPagerFragmentAdapter** inside its constructor pass **FragmentManager and int behavior** and while creating adapter pass **childFragmentManager, FragmentPagerAdapter.POSITION_UNCHANGED** This will solve your problem – Rakshit Nawani Feb 20 '20 at 05:09

2 Answers2

6

As per your comment, you are using Fragment and inside that Fragment there is your viewpager. So while creating your Adapter for ViewPager you need to pass childFragmentManager instead of getActivity()

Below is a sample Adapter for your viewPager that you can use

class NewViewPagerAdapter(fm: FragmentManager, behavior: Int) : FragmentStatePagerAdapter(fm, behavior) {
    private val mFragmentList: MutableList<Fragment> = ArrayList()
    private val mFragmentTitleList: MutableList<String> = ArrayList()

    override fun getItem(position: Int): Fragment {
        return mFragmentList[position]
    }

    override fun getCount(): Int {
        return mFragmentList.size
    }

    fun addFragment(fragment: Fragment, title: String) {
        mFragmentList.add(fragment)
        mFragmentTitleList.add(title)
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return mFragmentTitleList[position]
    }
}

and while creating your adapter call it like

   val adapter = NewViewPagerAdapter(
        childFragmentManager,
        FragmentPagerAdapter.POSITION_UNCHANGED
    )

as if you see the documentation for FragmentStatePagerAdapter it states that you should pass (FragmentManager, int) inside your adapter's constructor

I hope this will solve your issue as I was facing the same issue one day.

Happy coding.

Rakshit Nawani
  • 2,604
  • 14
  • 27
  • 1
    Thanks. As ianhanniballake already said, passing in the fragment itself is enough, just make sure that you have a suitable contructor. So both anwers are correct. – user123456789 Feb 20 '20 at 20:17
1
new ViewModelProvider(requireActivity()).get("your_key", YourViewModel.class)

Use requireActivity to obtain a ViewModel not the fragment.

boybeak
  • 417
  • 5
  • 19