Restoring fragment state when changing fragments through bottom navigation bar
Asked Answered
N

6

24

I have bottom navigation bar on click of item in navigation bar i am replacing fragments. I have 3 fragments A,B,C so on click of b item B fragment is loaded and in B i am calling 3-4 APIs. So now if i go to C and then again come to B a new instance of B Fragment is created and again those APIs are called how can i save the fragment instance state and not call APIs again while changing fragments. This is my code.

mBottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            int id = item.getItemId();
            Fragment currentLoaded = fgMan.findFragmentById(R.id.container_body);
            switch (id) {
                case R.id.nearby_fragment:
                    if (!(currentLoaded instanceof SpotFeedMapFragment)) {
                        removeScroll();
                        mNearByFragment = fgMan.findFragmentByTag(NEARBY_FRAGMENT_TAG) != null ? fgMan.findFragmentByTag(NEARBY_FRAGMENT_TAG) : mNearByFragment;
                        fgMan.beginTransaction().setCustomAnimations(R.anim.abc_fade_in, R.anim.abc_fade_out);
                        fgMan.beginTransaction().replace(R.id.container_body, mNearByFragment, NEARBY_FRAGMENT_TAG).commit();
                        fgMan.executePendingTransactions();
                        getSupportActionBar().setTitle(getString(R.string.nearby_fragment));
                    }
                    break;
                case R.id.route_fragment:
                    if (!(currentLoaded instanceof BusLocationsFragment)) {
                        if (!inParent) {
                            mRl.removeView(fixLayout);
                            p.addRule(RelativeLayout.BELOW, toolbar.getId());
                            scrollView.setLayoutParams(p);
                            scrollView.addView(fixLayout);
                            mRl.addView(scrollView);
                            inParent = true;
                        }
                        //mFragment = new BusLocationsFragment();
                        mBusLocFragment = fgMan.findFragmentByTag(BUS_LOC_FRAGMENT_TAG) != null ? fgMan.findFragmentByTag(BUS_LOC_FRAGMENT_TAG) : mBusLocFragment;
                        fgMan.beginTransaction().setCustomAnimations(R.anim.abc_fade_in, R.anim.abc_fade_out);
                        fgMan.beginTransaction().replace(R.id.container_body, mBusLocFragment, BUS_LOC_FRAGMENT_TAG).commit();
                        fgMan.executePendingTransactions();
                        getSupportActionBar().setTitle(getString(R.string.app_name));
                    }
                    break;
                case R.id.newsfeed_activity:
                    if (!(currentLoaded instanceof NewsFeedActivity)) {
                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
                            removeScroll();
                        }
                        mNewsFeedFragment = fgMan.findFragmentByTag(NEWSFEED_FRAGMENT_TAG) != null ? fgMan.findFragmentByTag(NEWSFEED_FRAGMENT_TAG) : mNewsFeedFragment;
                        fgMan.beginTransaction().setCustomAnimations(R.anim.abc_fade_in, R.anim.abc_fade_out);
                        fgMan.beginTransaction().replace(R.id.container_body, mNewsFeedFragment, NEWSFEED_FRAGMENT_TAG).commit();
                        fgMan.executePendingTransactions();
                        getSupportActionBar().setTitle(getString(R.string.news));
                    }
                    break;
            }
            return true;
        }
    });

I have already initialized fragments member variables above in onCreateof MainActivity

Nickell answered 14/3, 2017 at 8:49 Comment(2)
just an idea i cant provide code since i dont have the dev tools right now. dont use .replace in fragment transaction. this will make the destroy its instance to the backstack. replace it with .add. When you click on a button to change for example fragment a to b and then fragment b is already opened before, you can check youre backstack if fragment b is already there and pop it here is a sample https://mcmap.net/q/186706/-get-the-latest-fragment-in-backstack . Make sure to add the fragment on backstack everytime you visit it to save its instanceConformist
anyone knows is it better to hide/show fragment transaction instead of detach, remove, replace or add? (like this another answer https://mcmap.net/q/583264/-how-to-save-fragment-state-in-android/4074312)Deangelo
H
15

You should use a FragmentPagerAdapter to initiate the fragments so when you want to switch in between them, the state of the fragments will be saved.

CutomViewPager viewPager = (CustomViewPager) findViewById(R.id.viewpager1);
ViewPagerAdapter adapter = new ViewPagerAdapter (MainActivity.this.getSupportFragmentManager());
adapter.addFragment(new SpotFeedMapFragment(), "title");
adapter.addFragment(new BusLocationsFragment(), "title");
adapter.addFragment(new NewsFeedActivity(), "title");
viewPager.setAdapter(adapter);

then in the bottom navigation selected you can set fragment by simple command

viewPager.setCurrentItem(n);

my viewpager class is as follows:

public class CustomViewPager extends ViewPager {

private boolean isPagingEnabled;

public CustomViewPager(Context context) {
    super(context);
    this.isPagingEnabled = true;
}

public CustomViewPager(Context context, AttributeSet attrs) {
    super(context, attrs);
    this.isPagingEnabled = true;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    return this.isPagingEnabled && super.onTouchEvent(event);
}

//for samsung phones to prevent tab switching keys to show on keyboard
@Override
public boolean executeKeyEvent(KeyEvent event) {
    return isPagingEnabled && super.executeKeyEvent(event);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    return this.isPagingEnabled && super.onInterceptTouchEvent(event);
}

public void setPagingEnabled(boolean enabled) {
    this.isPagingEnabled = enabled;
}
}

in the xml instead of a empty layout for fragemnt u need:

<com.package.util.CustomViewPager
    android:id="@+id/viewpager1"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Code for custom FragmentPagerAdapter:

private class ViewPagerAdapter extends FragmentPagerAdapter {
    private final SparseArray<WeakReference<Fragment>> instantiatedFragments = new SparseArray<>();
    private final List<Fragment> mFragmentList = new ArrayList<>();
    private final List<String> mFragmentTitleList = new ArrayList<>();

    ViewPagerAdapter(FragmentManager manager) {
        super(manager);
    }

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

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

    void addFragment(Fragment fragment, String title) {
        mFragmentList.add(fragment);
        mFragmentTitleList.add(title);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        final Fragment fragment = (Fragment) super.instantiateItem(container, position);
        instantiatedFragments.put(position, new WeakReference<>(fragment));
        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        instantiatedFragments.remove(position);
        super.destroyItem(container, position, object);
    }

    @Nullable
    Fragment getFragment(final int position) {
        final WeakReference<Fragment> wr = instantiatedFragments.get(position);
        if (wr != null) {
            return wr.get();
        } else {
            return null;
        }
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mFragmentTitleList.get(position);
    }
}
Highpitched answered 14/3, 2017 at 9:9 Comment(3)
This is awesome, simply keep viewpager and it will take care fragment state, then give page limit, finally remove the viewpager swipe.Water
FragmentPagerAdapter is an abstract class. Code is missing for custom fragment pager adapter class which should extends FragmentPagerAdapter.Delora
@Delora It is a simple implementation. However I added my custom FragmentPagerAdapter if you need it.Highpitched
C
7

To restore/retain a fragment's state you should use ViewPager2 as it is the updated version of ViewPager.

You will get the code on my GitHub repository with three menu items in the Bottom Navigation Bar with more functionality. I am also providing a simple description here with two menu items in the Bottom Navigation Bar.

Step by step guide (to restore/retain an EditText's state as an example):

Step 1:

Add dependencies in your build.gradle (app module) file:

dependencies {

    def nav_version = "2.3.0"
    implementation "androidx.navigation:navigation-fragment:$nav_version"
    implementation "androidx.navigation:navigation-ui:$nav_version"

    implementation 'androidx.viewpager2:viewpager2:1.0.0'

}

Step 2:

Add menu_bottom_navigation.xml to res/menu: (You may also add icons to menu items)

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/menu_first"
        android:checked="true"
        android:title="First"
        app:showAsAction="always" />
    <item
        android:id="@+id/menu_second"
        android:checked="false"
        android:title="Second"
        app:showAsAction="always" />

</menu>

Step 3:

Add activity_main.xml to res/layout: (adding menu to BottomNavigationView and placing ViewPager2)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activityRoot"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="bottom"
    android:orientation="vertical"
    android:animateLayoutChanges="true"
    tools:context=".MainActivity">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewpager2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/bottom_navigation"
        android:layout_alignParentTop="true"
        android:layout_weight="1"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_gravity="bottom"
        android:fitsSystemWindows="true"
        app:itemIconSize="20dp"
        android:background="#A8DD44"
        app:menu="@menu/menu_bottom_navigation" />

</LinearLayout>

Step 4:

Add fragment_first.xml to res/layout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:layout_margin="20dp"
    tools:context="com.example.rough.Fragment.FirstFragment">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="First Fragment"
        android:layout_centerInParent="true"
        android:textSize="30sp" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="Write something &amp; it will stay"
        android:ems="13"/>

</LinearLayout>

Step 5:

Add fragment_second.xml to res/layout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="20dp"
    android:orientation="vertical"
    tools:context="com.example.rough.Fragment.SecondFragment">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Second Fragment"
        android:layout_centerInParent="true"
        android:textSize="30sp" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="Write something &amp; it will stay"
        android:ems="13"/>

</LinearLayout>

Step 6:

ViewPagerAdapter.java:

public class ViewPagerAdapter extends FragmentStateAdapter {
    private final List<Fragment> mFragmentList = new ArrayList<>();

    public ViewPagerAdapter(@NonNull FragmentManager fragmentManager, Lifecycle b ) {
        super(fragmentManager,b);
    }

    public void addFragment(Fragment fragment) {
        mFragmentList.add(fragment);
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getItemCount() {
        return mFragmentList.size();
    }

}

Step 7:

FirstFragment.java:

public class FirstFragment extends Fragment {


    public FirstFragment() {
        // Required empty public constructor
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_first, container, false);
    }

}

Step 8:

SecondFragment.java:

public class SecondFragment extends Fragment {


    public SecondFragment() {
        // Required empty public constructor
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_second, container, false);
    }

}

Step 9:

MainActivity.java:

public class MainActivity extends AppCompatActivity {

    BottomNavigationView bottomNavigationView;

    private ViewPager2 viewPager2;

    FirstFragment firstFragment;
    SecondFragment secondFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        viewPager2 = findViewById(R.id.viewpager2);
        bottomNavigationView = findViewById(R.id.bottom_navigation);

        bottomNavigationView.setOnNavigationItemSelectedListener(
                new BottomNavigationView.OnNavigationItemSelectedListener() {
                    @Override
                    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                        switch (item.getItemId()) {
                            case R.id.menu_first:
                                viewPager2.setCurrentItem(0,false);
                                break;
                            case R.id.menu_second:
                                viewPager2.setCurrentItem(1,false);
                                break;
                        }
                        return false;
                    }
                });

        viewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                super.onPageScrolled(position, positionOffset, positionOffsetPixels);

                switch (position) {
                    case 0:
                        bottomNavigationView.getMenu().findItem(R.id.menu_first).setChecked(true);
                        break;
                    case 1:
                        bottomNavigationView.getMenu().findItem(R.id.menu_second).setChecked(true);
                        break;
                }
            }
        });

        setupViewPager(viewPager2);

    }

    private void setupViewPager(ViewPager2 viewPager) {

        ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager(), getLifecycle());

        firstFragment =new FirstFragment();
        secondFragment =new SecondFragment();

        adapter.addFragment(firstFragment);
        adapter.addFragment(secondFragment);

        viewPager.setAdapter(adapter);
    }

}
Cheerio answered 7/8, 2020 at 18:15 Comment(3)
It is working perfectly I was searching for it from the whole day and now final got this awesome explanation! thanks a lot from the depth of my heartWoolfell
I implemented viewPager2 on the place of viewPager, only for the purpose of disabling the swiping feature, anybody who want same can use this line to achieve the end results :- viewpager2.setUserInputEnabled(false);Woolfell
it has any performance issue or not if it has 4 to 5 fragments and each fragment contains large data ?Strip
I
6

I used bottom navigation bar and I did it by customizing viewpager and I disable the swipe navigation. Each time user clicks bottom item, set relevant fragment in viewpager. Viewpager control state of fragment, so no need control state.

Custom ViewPager

public class BottomNavigationViewPager extends ViewPager {

    private boolean enabled;

    public BottomNavigationViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.enabled = false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (this.enabled) {
            return super.onTouchEvent(event);
        }

        return false;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (this.enabled) {
            return super.onInterceptTouchEvent(event);
        }

        return false;
    }

    /**
     * Enable or disable the swipe navigation
     * @param enabled
     */
    public void setPagingEnabled(boolean enabled) {
        this.enabled = enabled;
    }
}

If you still want to control state of fragment, you can see my answer in this link How to save fragment state in android?

Ioved answered 14/3, 2017 at 8:59 Comment(1)
I believe the link above is the answer that I'm looking for. ThanksDeangelo
H
1

Solution Using Navigation Component

Use these versions, Multi stack support is only available from these versions

versions.fragment = "1.4.0-alpha01"
versions.navigation = "2.4.0-alpha01"

First, you need to create nav graphs for each fragment in the bottom navigation

Filename: first.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/first"
    app:startDestination="@id/homeFragment"
    tools:ignore="UnusedNavigation">

    <fragment
        android:id="@+id/homeFragment"
        android:label="@string/fragment_A_title"
        android:name="com.app.company.HomeFragment"
    >
    </fragment>
</navigation>

create nav graph for all tabs and setup bottom navigation menu using these ids

<item android:title="@string/title_one"
    android:id="@+id/first"
    android:icon="@drawable/ic_icon_24"/>

<item android:title="@string/title_two"
    android:id="@+id/second"
    android:icon="@drawable/ic_icon_24"/>

.
.

.

include these nav graphs inside the main nav graph and use this main graph in FragmentContainerView

<include app:graph="@navigation/first"/>
<include app:graph="@navigation/second"/>
<include app:graph="@navigation/third"/>

Setup the bottom navigation

  val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment_host) as NavHostFragment
        navController = navHostFragment.navController


        val controller = binding.bottonNavigation.setupWithNavController(navController)


        appBarConfig = AppBarConfiguration(
            setOf(
                R.id.homeFragment,
                R.id.secondFragment,
                R.id.thirdFragment
            )
        )
        setupActionBarWithNavController(navController, appBarConfig)

Refer to the following link for more info

Navigation multiple back stacks

Hyperparathyroidism answered 11/8, 2021 at 7:20 Comment(0)
G
0

The simplest solution for that is to override "OnCreate()" method in your Fragment B and call you APIs in "OnCreate()" method instead of "OnCreateView()".

Hope it will work for you!

Griskin answered 26/6, 2017 at 21:20 Comment(3)
but oncreateView() will get called alwaysWater
That's why we are shifting all our code from OnCreateView to OnCreate()Griskin
@Zohaib...That is Fine, Even check the question once what he is asking?. His question is maintaining state to be specific View state, as far as my understanding is, BottomNavigationView doesn't support it as of now.Water
D
0

You shouldn't have to bring ViewPager in to accomplish this. I have one activity with a BottomNavigationView and a fragment container. When a user clicks a navigation tab, I do the following:

final Fragment existing = getSupportFragmentManager().findFragmentByTag(fragmentName);
final FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
getSupportFragmentManager().getFragments().forEach(transaction::hide);
if (existing != null) {
    transaction.show(existing);
} else {
    transaction.add(R.id.new_main_fragment_frame, instantiate, fragmentName);
}
transaction.commit();

I'm handling my own back press at the activity level by overriding onBackPressed and this is working well. The fragments lazy load in and then are re-used if the user is switching between tabs.

Deciduous answered 20/9, 2022 at 16:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.