Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

My fragments keep recreating whenever I reclick or navigate to the next fragment

I have implemented the new architecture component on my android app, but unfortunately handling the states of these fragments has become a nightmare for me. Whenever I press the icon of the fragment, the fragment is recreated every time I navigate. How can I handle this or rather save these fragments states?

Here is my main activity handling the five fragments:

public class MainActivityCenterofInformation extends AppCompatActivity {

    BottomNavigationView bottomNavigationView;

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

        setUpNavigation ();
    }

    public  void setUpNavigation(){
        bottomNavigationView = findViewById (R.id.bottom_nav_cict);
        NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager ()
                .findFragmentById (R.id.nav_host_fragment_cict);
        NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
    }

    //adding animations to the fragment
}

I can't read Kotlin so please direct me to Java, thanks.

like image 371
Kyle Mutta Avatar asked Dec 08 '19 06:12

Kyle Mutta


2 Answers

TL;DR: skip to the JUST SHOW ME THE STEPS ALREADY !!! section

That is the normal behaviour of the fragments. They are suppose to be recreated every time they are removed or replaced and you are suppose to restore their states using onSaveInstanceState.

Here is a nice article that describes how to do it : Saving Fragment States

Other than that you can use View Model which is the part of the following recommended android architecture. They are a great way to retain and restore UI data.

You can learn how to implement this architecture by following this step by step code lab

Android recommended architecture

EDIT : Solution

It took a while but here it is. Solution doesn't uses ViewModels at the moment.

Read carefully because every step is important. This solution covers the following two parts

  • Implement proper navigation on back key press
  • Keeping fragment alive during navigation

Background :

Android Navigation component provides a NavController class that you use to navigate between different destinations. Internally NavController uses a Navigator that actually does the navigation. Navigator is a abstract class and anyone can extend/inherit this class to create there own custom navigator to provide custom navigation depending on the type of destination. When using fragments as destinations the NavHostFragment uses a FragmentNavigator whose default implementation replaces the fragments whenever we navigate using FragmentTransaction.replace() which completely destroys previous fragment and adds new fragment. So we have to create our own navigator and instead of using FragmentTransaction.replace() we will use a combination of FragmentTransaction.hide() and FragmentTransaction.show() to avoid fragment from being destroyed.

Default behavior of Navigation UI :

By default whenever you navigate to any other fragment other than the home/starting fragment they won't get added to backstack so lets say if you select fragments in the following order

A -> B -> C -> D -> E

your back stack will have only

[A, E]

as you can see the fragments B, C, D weren't added to backstack so pressing back press will always get you to fragment A which is the home fragment

The behavior we want for now :

We want a simple yet effective behavior. We wan't all fragments to get added to backstack but if the fragment is already in backstack we want to pop all fragments upto the selected fragment.

Lets say I select fragment in following order

A -> B -> C -> D -> E

the backstack should also be

[A, B, C, D, E]

upon pressing back only the last fragment should be popped and backstack should be like this

[A, B, C, D]

but if we navigate to lets say fragment B, since B is already in the stack then all the fragments above B should be popped and our backstack should look like this

 [A, B]

I hope this behavior makes sense. This behavior is easy to implement using global actions as you will see below and is better than the default one.

OK Hotshot! now what ? :

Now we have two options

  1. extend FragmentNavigator
  2. copy/paste FragmentNavigator

Well I personally wanted to just extend FragmentNavigator and override navigate() method but since all its member variables are private I couldn't implement proper navigation.

So I decided to copy paste the entire FragmentNavigator class and just change the name in entire code from "FragmentNavigator" to whatever I want to call it.

JUST SHOW ME THE STEPS ALREADY !!! :

  1. Create custom navigator
  2. Use custom tag
  3. Add global actions
  4. Use global actions
  5. Add the custom navigator to the NavController

STEP 1: Create custom navigator

Here is my custom navigator called StickyCustomNavigator. All the code is same as FragmentNavigator except the navigate() method. As you can see it uses hide() , show() and add() method instead of replace(). The logic is simple. Hide the previous fragment and show the destination fragment. If this is our first time going to a specific destination fragment then add the fragment instead of showing it.

@Navigator.Name("sticky_fragment")
public class StickyFragmentNavigator extends Navigator<StickyFragmentNavigator.Destination> {


    private static final String TAG = "StickyFragmentNavigator";
    private static final String KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds";

    private final Context mContext;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final FragmentManager mFragmentManager;
    private final int mContainerId;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
            ArrayDeque<Integer> mBackStack = new ArrayDeque<>();
    @SuppressWarnings("WeakerAccess") /* synthetic access */
            boolean mIsPendingBackStackOperation = false;

    private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
            new FragmentManager.OnBackStackChangedListener() {

                @SuppressLint("RestrictedApi")
                @Override
                public void onBackStackChanged() {
                    // If we have pending operations made by us then consume this change, otherwise
                    // detect a pop in the back stack to dispatch callback.
                    if (mIsPendingBackStackOperation) {
                        mIsPendingBackStackOperation = !isBackStackEqual();
                        return;
                    }

                    // The initial Fragment won't be on the back stack, so the
                    // real count of destinations is the back stack entry count + 1
                    int newCount = mFragmentManager.getBackStackEntryCount() + 1;
                    if (newCount < mBackStack.size()) {
                        // Handle cases where the user hit the system back button
                        while (mBackStack.size() > newCount) {
                            mBackStack.removeLast();
                        }
                        dispatchOnNavigatorBackPress();
                    }
                }
            };

    public StickyFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager,
                           int containerId) {
        mContext = context;
        mFragmentManager = manager;
        mContainerId = containerId;
    }

    @Override
    protected void onBackPressAdded() {
        mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
    }

    @Override
    protected void onBackPressRemoved() {
        mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener);
    }

    @Override
    public boolean popBackStack() {
        if (mBackStack.isEmpty()) {
            return false;
        }
        if (mFragmentManager.isStateSaved()) {
            Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already"
                    + " saved its state");
            return false;
        }
        if (mFragmentManager.getBackStackEntryCount() > 0) {
            mFragmentManager.popBackStack(
                    generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
            mIsPendingBackStackOperation = true;
        } // else, we're on the first Fragment, so there's nothing to pop from FragmentManager
        mBackStack.removeLast();
        return true;
    }

    @NonNull
    @Override
    public StickyFragmentNavigator.Destination createDestination() {
        return new StickyFragmentNavigator.Destination(this);
    }

    @NonNull
    public Fragment instantiateFragment(@NonNull Context context,
                                        @SuppressWarnings("unused") @NonNull FragmentManager fragmentManager,
                                        @NonNull String className, @Nullable Bundle args) {
        return Fragment.instantiate(context, className, args);
    }

    @Nullable
    @Override
    public NavDestination navigate(@NonNull StickyFragmentNavigator.Destination destination, @Nullable Bundle args,
                                   @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (mFragmentManager.isStateSaved()) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                    + " saved its state");
            return null;
        }
        String className = destination.getClassName();
        if (className.charAt(0) == '.') {
            className = mContext.getPackageName() + className;
        }

        final FragmentTransaction ft = mFragmentManager.beginTransaction();

        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }

        String tag = Integer.toString(destination.getId());
        Fragment primaryNavigationFragment = mFragmentManager.getPrimaryNavigationFragment();
        if(primaryNavigationFragment != null)
            ft.hide(primaryNavigationFragment);
        Fragment destinationFragment = mFragmentManager.findFragmentByTag(tag);
        if(destinationFragment == null) {
            destinationFragment = instantiateFragment(mContext, mFragmentManager, className, args);
            destinationFragment.setArguments(args);
            ft.add(mContainerId, destinationFragment , tag);
        }
        else
            ft.show(destinationFragment);

        ft.setPrimaryNavigationFragment(destinationFragment);

        final @IdRes int destId = destination.getId();
        final boolean initialNavigation = mBackStack.isEmpty();
        // TODO Build first class singleTop behavior for fragments
        final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId;

        boolean isAdded;
        if (initialNavigation) {
            isAdded = true;
        } else if (isSingleTopReplacement) {
            // Single Top means we only want one instance on the back stack
            if (mBackStack.size() > 1) {
                // If the Fragment to be replaced is on the FragmentManager's
                // back stack, a simple replace() isn't enough so we
                // remove it from the back stack and put our replacement
                // on the back stack in its place
                mFragmentManager.popBackStackImmediate(
                        generateBackStackName(mBackStack.size(), mBackStack.peekLast()), 0);
                mIsPendingBackStackOperation = false;
            }
            isAdded = false;
        } else {
            ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
            mIsPendingBackStackOperation = true;
            isAdded = true;
        }
        if (navigatorExtras instanceof FragmentNavigator.Extras) {
            FragmentNavigator.Extras extras = (FragmentNavigator.Extras) navigatorExtras;
            for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
                ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
            }
        }
        ft.setReorderingAllowed(true);
        ft.commit();
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId);
            return destination;
        } else {
            return null;
        }
    }

    @Override
    @Nullable
    public Bundle onSaveState() {
        Bundle b = new Bundle();
        int[] backStack = new int[mBackStack.size()];
        int index = 0;
        for (Integer id : mBackStack) {
            backStack[index++] = id;
        }
        b.putIntArray(KEY_BACK_STACK_IDS, backStack);
        return b;
    }

    @Override
    public void onRestoreState(@Nullable Bundle savedState) {
        if (savedState != null) {
            int[] backStack = savedState.getIntArray(KEY_BACK_STACK_IDS);
            if (backStack != null) {
                mBackStack.clear();
                for (int destId : backStack) {
                    mBackStack.add(destId);
                }
            }
        }
    }

    @NonNull
    private String generateBackStackName(int backStackIndex, int destId) {
        return backStackIndex + "-" + destId;
    }

    private int getDestId(@Nullable String backStackName) {
        String[] split = backStackName != null ? backStackName.split("-") : new String[0];
        if (split.length != 2) {
            throw new IllegalStateException("Invalid back stack entry on the "
                    + "NavHostFragment's back stack - use getChildFragmentManager() "
                    + "if you need to do custom FragmentTransactions from within "
                    + "Fragments created via your navigation graph.");
        }
        try {
            // Just make sure the backStackIndex is correctly formatted
            Integer.parseInt(split[0]);
            return Integer.parseInt(split[1]);
        } catch (NumberFormatException e) {
            throw new IllegalStateException("Invalid back stack entry on the "
                    + "NavHostFragment's back stack - use getChildFragmentManager() "
                    + "if you need to do custom FragmentTransactions from within "
                    + "Fragments created via your navigation graph.");
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    boolean isBackStackEqual() {
        int fragmentBackStackCount = mFragmentManager.getBackStackEntryCount();
        // Initial fragment won't be on the FragmentManager's back stack so +1 its count.
        if (mBackStack.size() != fragmentBackStackCount + 1) {
            return false;
        }

        // From top to bottom verify destination ids match in both back stacks/
        Iterator<Integer> backStackIterator = mBackStack.descendingIterator();
        int fragmentBackStackIndex = fragmentBackStackCount - 1;
        while (backStackIterator.hasNext() && fragmentBackStackIndex >= 0) {
            int destId = backStackIterator.next();
            try {
                int fragmentDestId = getDestId(mFragmentManager
                        .getBackStackEntryAt(fragmentBackStackIndex--)
                        .getName());
                if (destId != fragmentDestId) {
                    return false;
                }
            } catch (NumberFormatException e) {
                throw new IllegalStateException("Invalid back stack entry on the "
                        + "NavHostFragment's back stack - use getChildFragmentManager() "
                        + "if you need to do custom FragmentTransactions from within "
                        + "Fragments created via your navigation graph.");
            }
        }

        return true;
    }

    @NavDestination.ClassType(Fragment.class)
    public static class Destination extends NavDestination {

        private String mClassName;

        public Destination(@NonNull NavigatorProvider navigatorProvider) {
            this(navigatorProvider.getNavigator(StickyFragmentNavigator.class));
        }

        public Destination(@NonNull Navigator<? extends StickyFragmentNavigator.Destination> fragmentNavigator) {
            super(fragmentNavigator);
        }

        @CallSuper
        @Override
        public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
            super.onInflate(context, attrs);
            TypedArray a = context.getResources().obtainAttributes(attrs,
                    R.styleable.FragmentNavigator);
            String className = a.getString(R.styleable.FragmentNavigator_android_name);
            if (className != null) {
                setClassName(className);
            }
            a.recycle();
        }

        @NonNull
        public final StickyFragmentNavigator.Destination setClassName(@NonNull String className) {
            mClassName = className;
            return this;
        }

        @NonNull
        public final String getClassName() {
            if (mClassName == null) {
                throw new IllegalStateException("Fragment class was not set");
            }
            return mClassName;
        }
    }

    public static final class Extras implements Navigator.Extras {
        private final LinkedHashMap<View, String> mSharedElements = new LinkedHashMap<>();

        Extras(Map<View, String> sharedElements) {
            mSharedElements.putAll(sharedElements);
        }

        @NonNull
        public Map<View, String> getSharedElements() {
            return Collections.unmodifiableMap(mSharedElements);
        }

        public static final class Builder {
            private final LinkedHashMap<View, String> mSharedElements = new LinkedHashMap<>();

            @NonNull
            public StickyFragmentNavigator.Extras.Builder addSharedElements(@NonNull Map<View, String> sharedElements) {
                for (Map.Entry<View, String> sharedElement : sharedElements.entrySet()) {
                    View view = sharedElement.getKey();
                    String name = sharedElement.getValue();
                    if (view != null && name != null) {
                        addSharedElement(view, name);
                    }
                }
                return this;
            }

            @NonNull
            public StickyFragmentNavigator.Extras.Builder addSharedElement(@NonNull View sharedElement, @NonNull String name) {
                mSharedElements.put(sharedElement, name);
                return this;
            }

            @NonNull
            public StickyFragmentNavigator.Extras build() {
                return new StickyFragmentNavigator.Extras(mSharedElements);
            }
        }
    }
}

STEP 2: Use custom tag

Now open up your navigation.xml file and rename the fragment tags related to your bottom navigation with whatever name you gave in @Navigator.Name() earlier.

<?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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <sticky_fragment
        android:id="@+id/navigation_home"
        android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

</navigation>

STEP 3: Add global action

Global actions are a way to navigate to destination from anywhere in your app. You can use the visual editor or directly use xml to add global actions. Set global action on each fragment with the following settings

  • destination : self
  • popUpTo : self
  • singleTop : true/checked

enter image description here enter image description here

This your how your navigation.xml should look like after adding global actions

<?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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <sticky_fragment
        android:id="@+id/navigation_home"
        android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <sticky_fragment
        android:id="@+id/navigation_images"
        android:name="com.example.bottomnavigationlogic.ui.images.ImagesFragment"
        android:label="@string/title_images"
        tools:layout="@layout/fragment_images" />

    <sticky_fragment
        android:id="@+id/navigation_videos"
        android:name="com.example.bottomnavigationlogic.ui.videos.VideosFragment"
        android:label="@string/title_videos"
        tools:layout="@layout/fragment_videos" />
    <sticky_fragment
        android:id="@+id/navigation_songs"
        android:name="com.example.bottomnavigationlogic.ui.songs.SongsFragment"
        android:label="@string/title_songs"
        tools:layout="@layout/fragment_songs" />
    <sticky_fragment
        android:id="@+id/navigation_notifications"
        android:name="com.example.bottomnavigationlogic.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
    <action
        android:id="@+id/action_global_navigation_home"
        app:destination="@id/navigation_home"
        app:launchSingleTop="true"
        app:popUpTo="@id/navigation_home" />
    <action
        android:id="@+id/action_global_navigation_notifications"
        app:destination="@id/navigation_notifications"
        app:launchSingleTop="true"
        app:popUpTo="@id/navigation_notifications" />
    <action
        android:id="@+id/action_global_navigation_songs"
        app:destination="@id/navigation_songs"
        app:launchSingleTop="true"
        app:popUpTo="@id/navigation_songs" />
    <action
        android:id="@+id/action_global_navigation_videos"
        app:destination="@id/navigation_videos"
        app:launchSingleTop="true"
        app:popUpTo="@id/navigation_videos" />
</navigation>

STEP 4: Use global actions

When you wrote

 NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());

then inside setupWithNavController() NavigationUI uses bottomNavigationView.setOnNavigationItemSelectedListener() to navigate to proper fragments depending on id of the menu item that was clicked. It's default behavior is as I mentioned before. We will add our own implementation to it and use global actions to achieve our desired back press behavior.

Here is how you do it simply in MainActivity

 bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
                int id = menuItem.getItemId();
                if (menuItem.isChecked()) return false;

                switch (id)
                {
                    case R.id.navigation_home :
                        navController.navigate(R.id.action_global_navigation_home);
                        break;
                    case R.id.navigation_images :
                        navController.navigate(R.id.action_global_navigation_images);
                        break;
                    case R.id.navigation_videos :
                        navController.navigate(R.id.action_global_navigation_videos);
                        break;
                    case R.id.navigation_songs :
                        navController.navigate(R.id.action_global_navigation_songs);
                        break;
                    case R.id.navigation_notifications :
                        navController.navigate(R.id.action_global_navigation_notifications);
                        break;
                }
                return true;

            }
        });

FINAL STEP 5: Add your custom navigator to NavController

Add your navigator as follow in your MainActivity. Make sure you are passing childFragmentManager of the NavHostFragment.

navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));

Also add the navigation graph to NavController here as well using setGraph() method as shown below.

This is how my MainActivity looks like after step 4 and step 5

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BottomNavigationView navView = findViewById(R.id.nav_view);
       
        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
                R.id.navigation_home, R.id.navigation_images, R.id.navigation_videos,R.id.navigation_songs,R.id.navigation_notifications)
                .build();
        NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
        
        final NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
        navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
        navController.setGraph(R.navigation.mobile_navigation);
        
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(navView,navController);
        
        navView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
                int id = menuItem.getItemId();
                if (menuItem.isChecked()) return false;

                switch (id)
                {
                    case R.id.navigation_home :
                        navController.navigate(R.id.action_global_navigation_home);
                        break;
                    case R.id.navigation_images :
                        navController.navigate(R.id.action_global_navigation_images);
                        break;
                    case R.id.navigation_videos :
                        navController.navigate(R.id.action_global_navigation_videos);
                        break;
                    case R.id.navigation_songs :
                        navController.navigate(R.id.action_global_navigation_songs);
                        break;
                    case R.id.navigation_notifications :
                        navController.navigate(R.id.action_global_navigation_notifications);
                        break;
                }
                return true;

            }
        });

    }

}

Hope this helps.

like image 55
Syed Ahmed Jamil Avatar answered Sep 25 '22 00:09

Syed Ahmed Jamil


I am not entirely sure if this is the answer you are looking for but if you are concerned about managing state the modern way of managing state is by using something called a view model. View models are a component of the MVVM architecture. Their purpose is to hold and expose data to your fragment/activity to display. With the navigation architecture, if you store the data related to each fragment in this view model appropriately your state will be retained in the view model.

That being said I would personally suggest looking into MVVM architecture as well as view models specifically. Otherwise, a brute force for retaining state is by using the savedInstance state throughout fragments and manually saving and recovering important data.

Links: - Android View Model Component - Android architecture guide

like image 29
Eli Dangerfield Avatar answered Sep 26 '22 00:09

Eli Dangerfield