ᴛʜᴇᴘᴀᴛᴇʟ
ᴛʜᴇᴘᴀᴛᴇʟ

Reputation: 4656

Loading Fragment UI on-demand

Problem:

I am currently running into a problem where my app is trying to load too many fragments when it opens for the first time.

I have BottomNavigationView with ViewPager that loads 4 fragments - each one of the Fragment contains TabLayout with ViewPager to load at least 2 more fragments.

As you can imagine, that is a lot of UI rendering (10+ fragments) - especially when some of these fragments contain heavy components such as calendar, bar graphs, etc.

Currently proposed solution:

Control the UI loading when the fragment is required - so until the user goes to that fragment for the first time, there is no reason to load it.

It seems like it's definitely possible as many apps, including the Play Store, are doing it. Please see the example here

In the video example above - the UI component(s) are being loaded AFTER the navigation to the tab is completed. It even has an embedded loading symbol.

1) I am trying to figure out how to do exactly that - at what point would I know that this fragment UI need to be created vs it already is created?

2) Also, what is the fragment lifecycle callback where I would start the UI create process? onResume() means UI is visible to the user so loading the UI there will be laggy and delayed.

Hope this is clear enough.


EDIT:
I'm already using the FragmentStatePagerAdapter as ViewPager adapter. I noticed that the super(fm) method in the constructor is deprecated now:

ViewPagerAdapter(FragmentManager fm) {
    super(fm); // this is deprecated
}

So I changed that to:

ViewPagerAdapter(FragmentManager fm) {
    super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}

BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT: Indicates that only the current fragment will be in the Lifecycle.State.RESUMED state. All other Fragments are capped at Lifecycle.State.STARTED.

This seems useful as the onResume() of the Fragment will only be called when the Fragment is visible to the user. Can I use this indication somehow to load the UI then?

Upvotes: 9

Views: 3557

Answers (8)

Mahavir Jain
Mahavir Jain

Reputation: 1561

It depends upon how you initialize your fragment in your activity. May be you are initializing all your fragment in onCreate method of your activity instead of that you can initialize it when BottomNavigation item is selected like below :

Fragment one,two,three,four;

 @Override
    public boolean onNavigationItemSelected(MenuItem item){
       Fragment fragment;
    
        switch(item.getId()){
            case R.id.menu_one:{
            if(one==null)
                one = Fragment()
            fragment = one;
            break;
           }
           
          case R.id.menu_two:{
            if(two==null)
                two = Fragment()
            fragment = two;
            break;
           }


         }
       getFragmentManager().beginTransaction().replace(fragment).commit();
    }

To decide how many page is load in you view pager at one time you can use : setOffscreenPageLimit.

viewPager.setOffscreenPageLimit(number)

Please try this.

Upvotes: 1

Roaim
Roaim

Reputation: 2358

How about you maintain just one ViewPager? Sounds crazy? In that case, you just change the dataset of PagerAdapter when you switch between the bottom tabs. Let's see how you can accomplish this,

As you mentioned, you have 4 fragments, each is assigned to an individual tab of the bottom navigation view. Each performs some redundant work i.e. holding a viewPager with tab layout and setting the same kind of adapters. So, if we can combine these 4 redundant tasks into one then we will be able to get rid of 4 fragments. And as there will be just one viewPager with one single adapter then we will be able to reduce the fragment loading count from ~10 to 2 if we set offScreenPageLimit to 1. Let's see some example,

activity.xml should look like

<LinearLayout>
    <TabLayout />
    <ViewPager />
    <BottomNavigationView />
</LinearLayout>

It's optional but I would recommend to create a base PagerFragment abstract class with abstract method getTabTitle()

public abstract class PagerFragment extends Fragment {
    public abstract String getTabTitle();
}

Now it's time to make our PagerAdapter class

public class SectionsPagerAdapter extends FragmentStatePagerAdapter {

    public Map<Integer, List<PagerFragment>> map = ...; // If you are concerned about memory then I could recommend to store DataObject instead of PagerFragment and instantiate fragment on demand using that data.
    public int currentTabId = R.id.first_bottom_tab_id;

    private List<PagerFragment> getCurrentFragments() {
        return map.get(currentTabId);
    }

    public void setCurrentTabId(int tabId) {
        this.currentTabId = tabId;
    }

    public SectionsPagerAdapter(FragmentManager manager) {
        super(manager);
    }

    @Override
    public Fragment getItem(int position) {
        return getCurrentFragments().get(position);
    }

    @Override
    public int getCount() {
        return getCurrentFragments().size();
    }

    @Override
    public int getItemPosition(@NonNull Object object) {
        return POSITION_NONE;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return getCurrentFragments().get(position).getTabTitle();
    }
}

And finally, in Activity

    SectionsPagerAdapter pagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    viewPager.setAdapter(pagerAdapter);
    viewPager.setOffscreenPageLimit(1);
    viewPagerTab.setViewPager(viewPager);

    bottomNavigationView.setOnNavigationItemSelectedListener(menuItem -> {
        pagerAdapter.setCurrentTabId(menuItem.getItemId())
        pagerAdapter.notifyDataSetChanged();
        viewPagerTab.setViewPager(viewPager);
    }

This is the basic idea. You can mix some of your own ideas with it to make a wonderful result. Let me know if it is useful?

UPDATE

Answer to your questions,

  1. I think with my solution you can achieve exactly the same behavior of the video as I already did it in a project. In my solution, if you set offset page limit to 1 then only adjacent fragment's is created in advance. So, fragment creation will be handled by adapter and viewpager you don't need to worry about it.
  2. In my above solution, you should create UI in onCreateView().

Upvotes: -1

Lakhwinder Singh
Lakhwinder Singh

Reputation: 7209

Have you tried the setUserVisibleHint() method of a fragment

override fun setUserVisibleHint(isVisibleToUser: Boolean) {
   super.setUserVisibleHint(isVisibleToUser)
   if(isVisibleToUser){
   // Do you stuff here
   }
}

This will only get called when a fragment is visible to the user

Upvotes: 0

Farid
Farid

Reputation: 2562

The reason your app loads multiple Fragments at the startup is most probably, you're initializing them all at once. Instead, you can initialize them when you need them. Then use show\ hide to attach\ detach from window without re-inflating whole layout.

Simple explanation: You'll create your Fragment once user clicks on BottomNavigationView's item. On clicked item, you'll check if Fragment is not created and not added, then create it and add. If it's already created then use show() method to show already available Fragment and use hide() to hide all other fragments of BottomNavigationView.

As per your case show()/hide is better than add()/replace because as you said you don't want to re-inflate the Fragment when you want show them

public class MainActivity extends AppCompatActivity{ 
    FragmentOne frg1;
    FragmentTwo frg2;

    @Override
    public boolean onNavigationItemSelected(MenuItem item){
        switch(item.getId()){
            case R.id.fragment_one:
                if (frg2 != null && frg2.isAdded(){
                    fragmentManager.beginTransaction().hide(frg2).commit();
                }
                if(frg1 != null && !frg1.isAdded){
                    frg1 = new FragmenOne();
                    fragmentManager.beginTransaction().add(R.id.container, frg1).commit();
                }else if (frg1 != null && frg1.isAdded) {
                    fragmentManager.beginTransaction().show(frg1).commit();
                }
                return true;
            case R.id.fragment_two:
            // Reverse of what you did for FragmentOne
            return true;
        }
    }
}

And for your ViewPager as you can see from the example you're referring to; PlayStore is using setOffscreenPageLimit. This will let you choose how many Views should be kept alive, otherwise will be destroyed and created from start passing through all lifecycle events of the Fragment (in case view is Fragment). In PlayStore app's case that's probably 4-5 that why it started loading again when you re-selected "editor's choice" tab. If you do the following only selected and neighboring (one in the right) Fragments will be alive other Fragments outside screen will be destroyed.

public class FragmentOne extends Fragment{ 
    ViewPager viewPager;
    @Override
    public void onCreateView(){
        viewPager = .... // Initialize
        viewpAger.setOffscreenPageLimit(1); // This will keep only 2 Fragments "alive"
    }
}

Answer to both questions

If you use show/hide you won't need to know when to inflate your view. It will be handled automatically and won't be laggy since it's just attaching/detaching views not inflating.

Upvotes: 1

Sir Codesalot
Sir Codesalot

Reputation: 7293

Just load fragments one by one. Create the main fragment layout with many placeholders and stubs and then just load them in the order you like. Use FragmentTransaction.replace() from the main fragment after it loads.

Upvotes: 0

Bernard
Bernard

Reputation: 3

I would recommend you use BottomNavigationView.setOnNavigationItemSelectedListener to toggle between the fragment UI whenever it is needed.

    navigationView.setOnNavigationItemSelectedListener(item -> {
        switch(item.getItemId()) {
            case R.id.item1:
                // you can replace the code findFragmentById() with findFragmentByTag("dashboard");
                // if you only have one framelayout to hold the fragment
                fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);

                if (fragment == null) {
                    fragment = new ExampleFragment();

                    // if the fragment is identified by tag, add another 
                    // argument to this method: 
                    //   replace(R.id.fragment_container, fragment, "dashboard")
                    getSupportFragmentManager().begintransaction()
                        .replace(R.id.fragment_container, fragment)
                        .commit();
                }
                break;
        }
    }

The idea is simple, when the user swipes or selects a different tab, the fragment that was visible is replaced by the new fragment.

Upvotes: 0

Janusz Hain
Janusz Hain

Reputation: 617

You can try to have Fragments with FrameLayouts only in ViewPager. The actual Fragments could be added to FrameLayout in onResume() (after checking if this Fragment isn't already attached). It should work if BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT works as expected.

Upvotes: 0

Rajan Bhavsar
Rajan Bhavsar

Reputation: 1857

i was worked with the same kind of the Application, There were multiple tabs and also Tabs have multiple inner tabs.

i was used the concept of ViewPager method, In which there is one method of onPageSelected() for that method we were getting the page position.

By the Use of this position we are checking the current Fragment and called their custom method that we created inside that fragment like onPageSelected() defined inside that fragment.

With this custom method onPageSelected() inside the Fragment we checked that weather the list are available or not if list have data then we are not making the call of Api otherwise we are calling the Api and loading that list.

I think you have same kind of requirement to follow if your Tabs have inner Tab or viewpager you can follow same concept inside of that so if your current fragment of viewpager method onpageSelected called at that time your viewpager fragment initialized.

you have to call just initialization like data binding or view initialization need to be called in onCreate() method and other list attachment and api call to be managed by the custom method onPageSelected that will be called based on ViewPager onPageSelected.

let me Know if you need any help for same.

Upvotes: 0

Related Questions