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:
- Navigation provides methods to find NavController
- NavHostFragment is a container used to host the content of navigation
- NavController realizes page Jump through navigator
- Navigator is an abstract and has a main implementation class
- NavDestination navigation node
- NavGraph navigation node page collection
navigation has three elements:
- navigation graph(xml resource): a visual navigation resource file
- NavHostFragment: current Fragment container
- 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.