This is the second article about Navigation, if you don’t know about Navigation, please read first to learn a wave of Navigation.

Execute onCreateView multiple times

In the last article, we made a page with three tabs by using Navigation and BottomNavigationView, namely Feed, Timer and Mine. The names of these three fragments are only displayed on the current page.

Now to add some content to the TimerFragment, we start a countdown in the TimerFragment onCreateView method.

private void startTimer(a) {
    new CountDownTimer(10 * 1000.1000) {

        @Override
        public void onTick(long millisUntilFinished) {
            tvLabel.setText(String.valueOf((millisUntilFinished / 1000) + 1));
        }

        @Override
        public void onFinish(a) {
            tvLabel.setText("Finished");
        }
    }.start();
}
Copy the code

If you look closely at the effect above, you can see that every time you switch to the TimerFragment, the countdown always starts again, not just once as we want. What is the problem that causes this? The TimerFragment executes onCreateView multiple times. Why is it executing multiple times? Why is it loading multiple times? We don’t have any special operation. Is it because of Navigation?

Now let’s dive into the Navigation source code to see what’s going on and how we can fix it.

First of all, we need to clarify our direction. How does Navigation switch fragments and why the onCreateView of the Fragment is executed multiple times?

Where is the entrance? If you know anything about Navigation, you’ll be familiar with the following line of code: navigate to the NavController via a View and then navigate to the NavController method.

Navigation.findNavController(view)
        .navigate(id);
Copy the code

The navigate method has multiple overloaded methods, and our initial navigate method will eventually execute to the following overloaded method.

navigate(NavDestination node, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras)
Copy the code

The specific content of the method is as follows:

In line 9, we can see that the mNavigatorProvider gets a Navigator object with a generic type of NavDestination, and in line 12, The NavDestination object is obtained by calling the navigate method of the navigator that was just obtained.

The two key lines are the one that gets the object to execute the Navigator and the one that actually executes the navigate method. Given this, we simply need to find the Navigate method for the Navigator. However, Navigator is just an abstract class, and we need to continue to find its implementation class.

Shortcut keys: Implementation(s) Mac: option(⌥) + Command (⌘)+B

Key code for the Navigator abstract class:

public abstract class Navigator<D extends NavDestination> {
    @Retention(RUNTIME)
    @Target({TYPE})
    @SuppressWarnings("UnknownNullness")
    public @interface Name {
        String value(a);
    }

    @NonNull
    public abstract D createDestination(a);

    @Nullable
    public abstract NavDestination navigate(@NonNull D destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Extras navigatorExtras);

    public abstract boolean popBackStack(a);

    @Nullable
    public Bundle onSaveState(a) {
        return null;
    }

    public void onRestoreState(@NonNull Bundle savedState) {}public interface Extras {}}Copy the code

You can use shortcuts to find implementation classes like ActivityNavigator, DialogFragmentNavigator, FragmentNavigator, Here we focus only on the navigate method in the FragmentNavigator class.

Don’t be afraid. The key piece of code is line 32 ft. Replace (mContainerId, FRAg), which uses the FragmentTransaction replace method. Replace is a fragment that removes the same ID and then adds it.

So this is why the TimerFragment onCreateView method is executed multiple times.

To circumvent the replace

Ok, so is there any way to get around, or to get around the replace? The answer is yes.

MNavigatorProvider finds a Navigator object with a generic type of NavDestination. MNavigatorProvider finds a Navigator object with a generic type of NavDestination. GetNavigatorName () and find the node. And mNavigatorProvider. GetNavigator internal what exactly happened?

Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
        node.getNavigatorName());
Copy the code

The node is essentially a NavDestination object, and a NavDestination object corresponds to the node information in the Navigation Graph. The navigation graph file I used for the Demo is as follows:

<?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/tab_navigation"
    app:startDestination="@id/feedFragment">

    <fragment
        android:id="@+id/feedFragment"
        android:name="me.monster.blogtest.tab.FeedFragment"
        android:label="fragment_feed"
        tools:layout="@layout/fragment_feed" />
    <fragment
        android:id="@+id/timerFragment"
        android:name="me.monster.blogtest.tab.TimerFragment"
        android:label="fragment_timer"
        tools:layout="@layout/fragment_timer" />
    <fragment
        android:id="@+id/mineFragment"
        android:name="me.monster.blogtest.tab.MineFragment"
        android:label="fragment_mine"
        tools:layout="@layout/fragment_mine" />
</navigation>
Copy the code

Node. getNavigatorName returns the fragment node name, and getNavigator maintains a HashMap mNavigators. The HashMap stores the key as the node name and the value as the implementation class of the abstract Navigator. The FragmentNavigator corresponding to the Fragment is also stored there.

Since there is a map and we extract the corresponding Navigator implementation class from it, can we create a class and implement the Navigator, and then add the key and value to the HashMap? The answer is yes. There are two public methods in NavigatorProvider:

  • addNavigator(Navigator navigator)
  • addNavigator(String name, Navigator navigator)

The addNavigator of one parameter also calls the addNavigator method of two parameters. That name is the name of the fragment node in the Navigation Graph. It is also the value defined by the Name annotation in the Navigator abstract class. And there is a getNavigatorProvider() method in the NavController class, where we initially found navigate.

If you look at that, the relationship should be clear. So, we need to create a class ourselves that implements Navigator and adds a value for the Name annotation, Then call addNavigator after the Activity using the Navigation module gets the NavController and calls its getNavigatorProvider method.

A custom Navigator

There is already a project on Github that demonstrates a custom implementation of Navigator. This project is written in the Kotlin language.

Project address: github.com/STAR-ZERO/n…

Speaking of which, Drakeet shared the project on his planet of Knowledge. Thanks to Drakeet for sharing.

I wrote a Java version based on his code and changed two lines of code (comments). The content of the annotation is to use FragmentTranslation to control the Fragment. The original author wrote detach and attach methods, I changed to hide and show methods.

@Navigator.Name("keep_state_fragment")
public class KeepStateNavigator extends FragmentNavigator {
    private Context context;
    private FragmentManager manager;
    private int containerId;

    public KeepStateNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) {
        super(context, manager, containerId);
        this.context = context;
        this.manager = manager;
        this.containerId = containerId;
    }

    @Nullable
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        String tag = String.valueOf(destination.getId());
        FragmentTransaction transaction = manager.beginTransaction();
        boolean initialNavigate = false;
        Fragment currentFragment = manager.getPrimaryNavigationFragment();
        if(currentFragment ! =null) {
// transaction.detach(currentFragment);
            transaction.hide(currentFragment);
        } else {
            initialNavigate = true;
        }
        Fragment fragment = manager.findFragmentByTag(tag);
        if (fragment == null) {
            String className = destination.getClassName();
            fragment = manager.getFragmentFactory().instantiate(context.getClassLoader(), className);
            transaction.add(containerId, fragment, tag);
        } else {
// transaction.attach(fragment);
            transaction.show(fragment);
        }

        transaction.setPrimaryNavigationFragment(fragment);
        transaction.setReorderingAllowed(true);
        transaction.commitNow();
        return initialNavigate ? destination : null; }}Copy the code

This code does not pass the Bundle args and also breaks the navigation Graph toggle animation Settings, if necessary. See the FragmentNavigator class for an implementation.

Thank you Qwer for asking the question

Note that the navigation Graph needs to change the fragment node name to keep_state_fragment when using a custom Navigator. Set it in the hosted Activity and remove the navGraph property of the fragment from the Activity layout file.

NavController navController = Navigation.findNavController(this, R.id.fragment3);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment3);
KeepStateNavigator navigator = new KeepStateNavigator(this, navHostFragment.getChildFragmentManager(), R.id.fragment3);
navController.getNavigatorProvider().addNavigator(navigator);
navController.setGraph(R.navigation.tab_navigation);
Copy the code

Finally, take a look at TabActivity when using a custom Navigator.

Does this look like the end? Not really. We’re just getting started.

First of all, I would like to correct that the code returning from SettingFragment to RootFragment in the first blog post on Navigation had some problems.

Don’t worry if you haven’t read the article, it’s basically A tuning to B, triggering A click event in B, and then returning from B to A. The code returned is as follows.

The original code is:

btnToRoot.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) { Navigation.findNavController(btnToRoot) .popBackStack(); }});Copy the code

So in this case, in the click event, the last thing that’s going to be done is popBackStack, so you shouldn’t be calling that method you should be using navigateUp.

In the first post, all the node names in the Navigation Graph were fragments. What would happen if I used the keep_state_fragment method above?

As you can see, after replacing the Navigation Graph node name with keep_state_fragment, clicking on the SettingFragment does not return. Why is that? I didn’t do anything. It’s not working.

No, I need to see what is done in Navigation source code. So I started my debug tour. Later, I found in the Navigation. FindNavController (btnToRoot). NavigateUp (); Internally I checked if the current return stack number was 1, and was shocked to find that it was actually 1. NavigateUp, of course, returns false and does not return from SettingFragment to RootFragment.

The following two pieces of code are: NavController# navigateUp and NavController# getDestinationCountOnBackStack

private int getDestinationCountOnBackStack(a) {
    int count = 0;
    for (NavBackStackEntry entry : mBackStack) {
        if(! (entry.getDestination()instanceofNavGraph)) { count++; }}return count;
}
Copy the code

I looked up the data type mBackStack, found that it was a stack, and then found the method to push mBackStack.

The first method is called from the NavController#NavController method, and the other three add methods are called from NavController#navigate. There is a nuller outside of calling the Add method. The null object is from the Navigator#navigate method.

Navigate method for NavController is truncated.

private void navigate(@NonNull NavDestination node, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    / /...
    Navigator<NavDestination> navigator = mNavigatorProvider
           .getNavigator(node.getNavigatorName());
    Bundle finalArgs = node.addInDefaultArgs(args);
    NavDestination newDest = navigator.navigate(node, finalArgs,
            navOptions, navigatorExtras);
    if(newDest ! =null) {
        // The mGraph should always be on the back stack after you navigate()
        if (mBackStack.isEmpty()) {
            mBackStack.add(new NavBackStackEntry(mGraph, finalArgs));
        }
        // Now ensure all intermediate NavGraphs are put on the back stack
        // to ensure that global actions work.
        ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
        NavDestination destination = newDest;
        while(destination ! =null && findDestination(destination.getId()) == null) {
            NavGraph parent = destination.getParent();
            if(parent ! =null) {
                hierarchy.addFirst(new NavBackStackEntry(parent, finalArgs));
            }
            destination = parent;
        }
        mBackStack.addAll(hierarchy);
        // And finally, add the new destination with its default args
        NavBackStackEntry newBackStackEntry = new NavBackStackEntry(newDest,
                newDest.addInDefaultArgs(finalArgs));
        mBackStack.add(newBackStackEntry);
    }
  / /...
}
Copy the code

According to our previous experience, we can find that the Navigator is our custom KeepStateNavigator object, and the navgate method return value is our own control, that is, we dug a hole for ourselves. 2333 –

So let’s go ahead and look at the code we just wrote.

public NavDestination navigate(Destination destination, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras) {
    String tag = String.valueOf(destination.getId());
    FragmentTransaction transaction = manager.beginTransaction();
    boolean initialNavigate = false;
    Fragment currentFragment = manager.getPrimaryNavigationFragment();
    if(currentFragment ! =null) {
      transaction.hide(currentFragment);
    } else {
      initialNavigate = true;
    }
    Fragment fragment = manager.findFragmentByTag(tag);
    if (fragment == null) {
      String className = destination.getClassName();
      fragment = manager.getFragmentFactory().instantiate(context.getClassLoader(), className);
      transaction.add(containerId, fragment, tag);
    } else {
      transaction.show(fragment);
    }

    transaction.setPrimaryNavigationFragment(fragment);
    transaction.setReorderingAllowed(true);
    transaction.commitNow();
    return initialNavigate ? destination : null;
  }
Copy the code

In the last line, we check the initialNavigate and return either null or destination object. If initialNavigate is true, it will only be null if the currentFragment is null. When is the currentFragment null? True only when an Activity is opened and the first Fragment is filled. In our case, initialNavigate is true when the app is started and the RootFragment is opened. InitialNavigate is false when jumping from RootFragment to SettingFragment.

Add (containerId, fragment, tag) when this fragmen is empty; Then give initialNavigate to true. As a result, NavController# getDestinationCountOnBackStack can access to the actual fragment size, there would be no direct return fase.

Let’s run it and see what happens? Don’t worry. Check again. Just now I said in the Navigation. FindNavController (btnToRoot). NavigateUp (); Internally it determines whether the current number of return stacks is 1. Now we have solved the case of 1. What does it do when the number of return stacks is not 1? NavController#popBackStackInternal after an internal call to determine that the number of stacks returned is not 1.

NavController’s popBackStackInternal method has been deleted

boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
    if (mBackStack.isEmpty()) {
        // Nothing to pop if the back stack is empty
        return false;
    }
    ArrayList<Navigator> popOperations = new ArrayList<>();
    Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
    boolean foundDestination = false;
    while (iterator.hasNext()) {
        NavDestination destination = iterator.next().getDestination();
        Navigator navigator = mNavigatorProvider.getNavigator(
                destination.getNavigatorName());
        if(inclusive || destination.getId() ! = destinationId) { popOperations.add(navigator); }/ /...
    boolean popped = false;
    for (Navigator navigator : popOperations) {
        if (navigator.popBackStack()) {
            NavBackStackEntry entry = mBackStack.removeLast();
            popped = true;
     / /...
    return popped;
}
Copy the code

Within this method, I see the familiar face of the Navigator again, where the navigator executes a method called popBackStack that looks like it’s doing a return event. However, our KeepStateNavigator doesn’t have this method, that’s because we chose to inherit from FragmentNavigator, which has a popBackStack logic that we can’t use. So we need to override this method in FragmentNavigator.

Add (containerId, fragment, tag) in the KeepStateNavigator#navigate method since we need to return to the previous page, we also need to have a management stack; Add the current Fragment to the return stack and remove it in the popBackStack according to some criteria.

That will do.

For some reason, the recorded GIF kept flashing…

Arbitrary switching

Ok, that’s it, finally happy to use Navigation. Until one day, the boss came to me and told me a need.

From page A to page B, and then from page B to page C, an event is generated on page C. When the user returns to page C, the user needs to skip PAGE B, that is, go directly from PAGE C to page A.

Asked me if this could be implemented on Navigation, I thought about it and said yes. The following is to share the idea of achieving this effect. I personally think there are two solutions, and I will talk about them one by one.

Assume that the page opening sequence is A, B, and C.

  • The first type: priority shutdown

    When you return from C to A, it doesn’t have to be at the time of return, it might be after an event occurs, and then B is closed, and then only A and B are left in the rollback stack. In this case, you just need to go back to the logic of the page.

  • Second: priority return

    When returning from C to A, you can also skip B directly. The specific method is as follows: When clicking back from C, the operation of returning to the stack will be triggered. After the return operation is completed, it is found that the current page needs to be skipped, and then it will return to A.

To implement Navigation, you add some methods to the custom Navigator, and then get the Navigator object where you need to perform such operations and perform related operations.

Get the relevant code for the Navigator as follows:

NavController navController = Navigation.findNavController(btnToRoot); NavigatorProvider navigatorProvider = navController.getNavigatorProvider(); Navigator<? > navigator = navigatorProvider.getNavigator("keep_state_fragment");
if (navigator instanceof KeepStateNavigator) {
    ((KeepStateNavigator) navigator).closeMiddle(R.id.settingsFragment);
}
Copy the code

thinking

When I first learned Navigation, I glanced at it and thought it was a Fragment management framework. What’s better than other Fragment management frameworks? It looks so-so. Wow, it’s so complicated. Forget it. Just know how to use it. I could not help but look at the usage of Navigation and read the source code. Is it a Fragment management framework? NavigatorProvider, Navigator, Destination etc.

As I read more and more content, I practiced more and more, and the native Navigation became more and more unable to meet the requirements, I found that Google had thought of it for a long time, but did not provide us with specific solutions, and just opened up some things for developers to customize in different scenarios.

If you think about the code in your own project, it seems that if you want to extend it, you have to change the original code a lot. Unlike Navigation, when you need to change the original code, you should try not to touch the original code. Instead, interface, Provider, generics and other skills are more coding skills or design mode skills to fulfill the business requirements.

Instead of focusing too much on various third-party libraries, focus more on basic skills, which may not be obvious for a while, but may be the last thing on the table.


This article was first published on my blog. The full source code has been uploaded to GitHub with the code branch closeBefore. If you like this article, please click 🌟.

Photo by Joao Silas on Unsplash