Navigation retrace Fragment life cycle problem

Posted by pornost4r on Mon, 31 Jan 2022 19:21:02 +0100

Google launched Navigation mainly to unify the page Jump Behavior in the application.

'androidx.navigation:navigation-fragment:2.1.0' 
'androidx.navigation:navigation-ui:2.1.0'           
'androidx.navigation:navigation-fragment-ktx:2.1.0'            
'androidx.navigation:navigation-ui-ktx:2.1.0'
Copy code

Add dependency

Navigation is easy to use. When creating a new project, you can directly select the Bottom Navigation Activity project, which has helped us realize the relevant page logic by default.

The source code of Navigation is also very simple, but it involves many classes, mainly including the following:

  1. Navigation provides methods to find NavController
  2. NavHostFragment is a container used to host the content of navigation
  3. NavController realizes page Jump through navigator
  4. Navigator is an abstract and has a main implementation class
  5. NavDestination navigation node
  6. NavGraph navigation node page collection

navigation has three elements:

  1. navigation graph(xml resource): a visual navigation resource file
  2. NavHostFragment: current Fragment container
  3. NavController: / / navigation controller

Navigation is easy to use

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val navView: BottomNavigationView = findViewById(R.id.nav_view) //1. Bottom Navigation View

        val navController = findNavController(R.id.nav_host_fragment) //2. Navigation controller
        navView.setupWithNavController(navController)
    }
}
Copy code

Let's start with NavHostFragment because it is directly defined in our XML file. We directly use the viewer lifecycle method onCreate:

    @CallSuper
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Context context = requireContext();

        mNavController = new NavHostController(context);  //1
        mNavController.setLifecycleOwner(this);
        mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
        // Set the default state - this will be updated whenever
        // onPrimaryNavigationFragmentChanged() is called
        mNavController.enableOnBackPressed(
                mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
        mIsPrimaryBeforeOnCreate = null;
        mNavController.setViewModelStore(getViewModelStore());
        onCreateNavController(mNavController);   //2

        Bundle navState = null;
        if (savedInstanceState != null) {
            navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
            if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
                mDefaultNavHost = true;
                getParentFragmentManager().beginTransaction()
                        .setPrimaryNavigationFragment(this)
                        .commit();
            }
            mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
        }

        if (navState != null) {
            // Navigation controller state overrides arguments
            mNavController.restoreState(navState);
        }
        if (mGraphId != 0) {
            // Set from onInflate()
            mNavController.setGraph(mGraphId);
        } else {
            // See if it was set by NavHostFragment.create()
            final Bundle args = getArguments();
            final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
            final Bundle startDestinationArgs = args != null
                    ? args.getBundle(KEY_START_DESTINATION_ARGS)
                    : null;
            if (graphId != 0) {
                mNavController.setGraph(graphId, startDestinationArgs);
            }
        }
    }

Copy code

Step 1 directly creates the NavHostController and exposes it to the external caller through the findNavController method. NavHostController is inherited from NavController. The code at step 2 is as follows:

   @CallSuper
   protected void onCreateNavController(@NonNull NavController navController) {
       navController.getNavigatorProvider().addNavigator(
               new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
       navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
   }
Copy code

Get the Navigator provider through navcontroller and add two navigators to it, DialogFragmentNavigator and FragmentNavigator. In addition, two other navigators are added to the construction method of navcontroller, as follows:

public NavController(@NonNull Context context) {
    ....
    mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
    mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
Copy code

They are all implementation classes of Navigator. Page Jump corresponding to DialogFragment, Fragment and Activity respectively. You may be curious about navGraph Navigator. Where is it used? In fact, the navigation corresponding to navGraph configured in XML and the startDestination in the node file jump through navGraph Navigator. This is also its only use at present.

Each Navigator implements its own jump logic by copying the navigate method. The implementation logic of FragmentNavigator is emphasized here:

 @Nullable
    @Override
    public NavDestination navigate(@NonNull 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 Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        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);
        }

        ft.replace(mContainerId, frag);//Key code: replace with replase
        ft.setPrimaryNavigationFragment(frag);

        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.popBackStack(
                        generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
                        FragmentManager.POP_BACK_STACK_INCLUSIVE);
                ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
            }
            isAdded = false;
        } else {
            ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
            isAdded = true;
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (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;
        }
    }
Copy code

The most critical line of code is the comment. It loads the Fragment through replace, which will cause the Fragment life cycle to be revisited during navigation switching, which is not in line with our actual development logic.

Back to the Navigator provider obtained by navController above, a HashMap is maintained internally to store relevant Navigator information. It is stored by getting the annotation Name of Navigator as key and getClass of Navigator as value.

Let's go back to the onCreate method above:

@CallSuper @Override public void onCreate(@Nullable Bundle savedInstanceState) {
 super.onCreate(savedInstanceState); final Context context = requireContext();
    ....

if (mGraphId != 0) {
    mNavController.setGraph(mGraphId);
} else {

    ....    

    if (graphId != 0) {
        mNavController.setGraph(graphId, startDestinationArgs);
    }
}

 

}setGraph is called through mNavController. This is mainly to parse the mobile configured in our XML_ Navigation node information file. It will be resolved according to different nodes.

@NonNull 
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser, 
@NonNull AttributeSet attrs, int graphResId) throws XmlPullParserException, IOException {
    Navigator navigator = mNavigatorProvider.getNavigator(parser.getName());
    final NavDestination dest = navigator.createDestination();

    dest.onInflate(mContext, attrs);

        ....

    final String name = parser.getName();
    if (TAG_ARGUMENT.equals(name)) { // argument node
        inflateArgumentForDestination(res, dest, attrs, graphResId);
    } else if (TAG_DEEP_LINK.equals(name)) { // deeplink node
        inflateDeepLink(res, dest, attrs);
    } else if (TAG_ACTION.equals(name)) { // action node
        inflateAction(res, dest, attrs, parser, graphResId);
    } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) { // include node
        final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
        final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
        ((NavGraph) dest).addDestination(inflate(id));
        a.recycle();
    } else if (dest instanceof NavGraph) { // NavGraph node
        ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
    }
}

return dest;
Copy code

}Parse the navinflator by getting it. NavGraph is returned after parsing. NavGraph is inherited from NavDestination. It mainly saves all the parsed node information.

Finally, a simple summary is to obtain navconverter through NavHostFragment and store relevant Navigator information. Jump the page through their respective navigate methods. The configured page node information is parsed through setGraph and encapsulated as NavGraph object. The Destination information is stored in SparseArray.

Back to the problem mentioned above, how to solve the problem of Fragment life cycle re walking caused by page switching, and customize the Fragment navigator.

Custom FragmentNavigator

As mentioned above, we need to customize our own Navigator to host fragments. The main implementation idea is to inherit the existing FragmentNavigator and copy its navigation method, and replace the replace method with show and hide methods to complete the switching of fragments.

So how can our custom Navigator be recognized by the system? This is also simple. Just annotate our class @ Navigator Name (value) then he is a Navigator. Finally, through the ideas analyzed above, you can add it to the Navigator provider.

@Navigator.Name("fragment")  //The name here must be consistent with the name in the FragmentNavigator, otherwise it will not work
class FixFragmentNavigator(context: Context, manager: FragmentManager, conditionId: Int) : FragmentNavigator(context, manager, conditionId) {
    private val mContext = context
    private val mManager = manager
    private val mContainerId = conditionId

    private val TAG  = "FixFragmentNavigator"

    override fun navigate(destination: Destination,
                          args: Bundle?,
                          navOptions: NavOptions?,
                          navigatorExtras: Navigator.Extras?):
            NavDestination? {

        Log.d(TAG,"124454546456")
        if (mManager.isStateSaved) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already" + " saved its state")

            return null
        }
        var className = destination.className
        if (className[0] == '.') {
            className = mContext.packageName + className
        }
        val ft = mManager.beginTransaction()

        var enterAnim = navOptions?.enterAnim ?: -1
        var exitAnim = navOptions?.exitAnim ?: -1
        var popEnterAnim = navOptions?.popEnterAnim ?: -1
        var popExitAnim = navOptions?.popExitAnim ?: -1
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = if (enterAnim != -1) enterAnim else 0
            exitAnim = if (exitAnim != -1) exitAnim else 0
            popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
            popExitAnim = if (popExitAnim != -1) popExitAnim else 0
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
        }

//        ft.replace(mContainerId, frag)

        /**
         * 1,Query the currently displayed fragment first. If it is not empty, hide it
         * 2,Query whether the currently added fragment is not null according to the tag. If it is not null, show it directly
         * 3,If it is null, create a fragment instance through the instantiateFragment method
         * 4,Add the created instance to the transaction
         */
        val fragment = mManager.primaryNavigationFragment //Currently displayed fragment
        if (fragment != null) {
            ft.hide(fragment)
        }

        var frag: Fragment?
        val tag = destination.id.toString()
        frag = mManager.findFragmentByTag(tag)
        if (frag != null) {
            ft.show(frag)
        } else {
            frag = instantiateFragment(mContext, mManager, className, args)
            frag.arguments = args
            ft.add(mContainerId, frag, tag)
        }

        ft.setPrimaryNavigationFragment(frag)

        @IdRes val destId = destination.id


        /**
         *  Get mBackStack by reflection
         */
        val mBackStack: ArrayDeque<Int>

        val field = androidx.navigation.fragment.FragmentNavigator::class.java.getDeclaredField("mBackStack")
        field.isAccessible = true
        mBackStack = field.get(this) as ArrayDeque<Int>


        val initialNavigation = mBackStack.isEmpty()
        // TODO Build first class singleTop behavior for fragments
        val isSingleTopReplacement = (navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId)

        val isAdded: Boolean
        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
                mManager.popBackStack(
                        generateBackStackName(mBackStack.size, mBackStack.peekLast()),
                        FragmentManager.POP_BACK_STACK_INCLUSIVE
                )
                ft.addToBackStack(generateBackStackName(mBackStack.size, destId))
            }
            isAdded = false
        } else {
            ft.addToBackStack(generateBackStackName(mBackStack.size + 1, destId))
            isAdded = true
        }
        if (navigatorExtras is Extras) {
            val extras = navigatorExtras as Extras?
            for ((key, value) in extras!!.sharedElements) {
                ft.addSharedElement(key, value)
            }
        }
        ft.setReorderingAllowed(true)
        ft.commit()
        // The commit succeeded, update our view of the world
        if (isAdded) {
            mBackStack.add(destId)
            return destination
        } else {
            return null
        }
    }

    /**
     * If the parent class is private, you can directly define a method
     */
    private fun generateBackStackName(backIndex: Int, destid: Int): String {
        return "$backIndex - $destid"
    }

}
Copy code

Points to note here: @ navigator Name ("fragment") the name here must be consistent with the name in the FragmentNavigator, otherwise it will not work. It is mentioned above that the Nacitator is stored in the Map set. Only the same name can overwrite the initially added FragmentNavigator.

After customization, you need to delete the node reference of NavHostFragment in the layout file

 <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/release_navigation" />  //Delete this node
 Copy code

Manually associate FixFragmentNavigator with NavControl in your code.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val navView: BottomNavigationView = findViewById(R.id.nav_view) //1. Bottom Navigation View

        val navController = findNavController(R.id.nav_host_fragment) //2. Navigation controller

        //Custom Navigator
        //1. Create NavHostFragment
        val fragment  = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        //2. Create a custom navigator
        val navigator = FixFragmentNavigator(this,supportFragmentManager,fragment!!.id)
        //3. Add navigator
        navController.navigatorProvider.addNavigator(navigator)
        //4. Set xml navigation
        navController.setGraph(R.navigation.mobile_navigation)
        navView.setupWithNavController(navController)
    }
}
Copy code

This completes the custom Navigator implementation. When switching tabs, the Fragment life cycle will not be re executed.

Reference is made here Navigation modified - avoid repeated callbacks in the lifecycle

Thank you for your help.

Topics: Android