With the increasing market recognition of Jetpack series frameworks, it is once again possible to use Navigation framework to develop an app with single Activity+ multiple fragments. However, there are always some problems when using Navigation. For example, when FragmentA opens FragmentB and then comes back, FragmentA resets the life cycle. Many times this is not what we want. Why does this happen and how do we fix it? Today from the source level to explore.
First, I created a very simple NavGraph, as shown:
The function of both fragments is very simple, there is only one button in SourceFragment to click to jump to TargetFragment, and no logic in TargetFargment.
Then modify the resource file of the container Activity:
<androidx.constraintlayout.widget.ConstraintLayout
.>
<fragment
android:id="@+id/container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_main" />
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code
This gives a simple Navigation jump Demo.
Then let’s take a look at the life cycle of the SourceFragment from first loading to opening the TargetFragment and then returning to the SourceFragment.
Phase 1: Load SourceFragment:
onAttach -> onCreateView -> onViewCreated -> onStart -> onResume
This phase is fine, and the lifecycle changes are consistent with normal Fragments.
Stage 2: Open TargetFragment:
onPause -> onStop
The SourceFragment enters onStop, not onDestory, when it is not visible. So far, everything looks fine.
Stage 3: Return to SourceFragment
onCreateView -> onViewCreated -> onStart -> onResume
We were surprised to find that the life cycle of this phase was different from what we expected. We went back to SourceFragment and revisited the life cycle outside of onAttach. Revisiting the life cycle not only meant consuming additional resources to re-render the SourceFragment, but also reduced the user experience. Then come to the main topic of this article:
First, why do they go back to the life cycle?
Two, how to solve?
In order to analyze the problem, it is necessary to understand the principle first, and first take a brief look at the general implementation principle of Navigation framework.
In the layout file of the Activity container, we use a fragment tag and specify an Android :name attribute for the display of the tag, which contains the full path of the fragment. Official offer is androidx. Navigation. Fragments. NavHostFragment, as we all know, the Activity when loading layout will be based on the configuration of the full path to fragment object accessed by the reflection, then attach to the Activity, The Fragment is finally loaded. NavHostFragment is a good place to start to understand the Navigation framework.
public class NavHostFragment extends Fragment implements NavHost {... }public interface NavHost {
@NonNull
NavController getNavController(a);
}
Copy the code
NavHostFragment is a Fragment subclass that implements a simple interface that provides a method to retrieve NavController. The return value of this method is the mNavController property of NavHostFragment.
private NavHostController mNavController;
@NonNull
@Override
public final NavController getNavController(a) {
if (mNavController == null) {
throw new IllegalStateException("NavController is not available before onCreate()");
}
return mNavController;
}
Copy the code
The initialization of the mNavController property is done during the onCreate lifecycle.
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
/ / a little... Call some mNavController methods
onCreateNavController(mNavController); // This method is more important and will be discussed below
// Set the navigation chart ID
if(mGraphId ! =0) {
mNavController.setGraph(mGraphId);
} else {
// Set an empty navigation}}Copy the code
MGraphId is configured in the Fragment tag and the navGraph property is obtained in the onInflate method:
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,@Nullable Bundle savedInstanceState){
super.onInflate(context, attrs, savedInstanceState);
final TypedArray navHost = context.obtainStyledAttributes(attrs,androidx.navigation.R.styleable.NavHost);
// Use custom attributes to get the navigation chart
final int graphId = navHost.getResourceId(androidx.navigation.R.styleable.NavHost_navGraph, 0);
if(graphId ! =0) { mGraphId = graphId; }... }Copy the code
NavHostFragment is the first Fragment loaded by the Activity container. After the mnavController.setgraph method is called, there will be a series of method calls. Eventually replaced with the Fragment in the startDestination property configured in the Navigation resource file.
This is the theme function of NavHostFragment class. The NavController may seem a bit much, but its functions are more explicit: it provides Settings for the NavGraph, navigate jump methods, return event controls, and listen for changes to destinations. But the logic that actually performs the view jump is not performed by NavController, but is distributed to different Navigators via mNavigatorProvider, which then performs the real jump logic:
// Navigate final overload in NavController
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
/ /...
// Depending on the jump type, distribute to different navigators to execute the jump logic
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
// Call the navigate method in navigator
NavDestination newDest = navigator.navigate(node, finalArgs,navOptions, navigatorExtras);
/ /... Update mBackStack stack
}
Copy the code
There are five subclasses of the abstract Navigator class:
@Navigator.Name("activity")
public class ActivityNavigator extends Navigator<ActivityNavigator.Destination> {
/ /... Controls the jump of the Activity
}
@Navigator.Name("dialog")
public final class DialogFragmentNavigator extends Navigator<DialogFragmentNavigator.Destination> {
/ /... Controls the jump of DialogFragment
}
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
/ /... Controls the Fragment jump
}
@Navigator.Name("navigation")
public class NavGraphNavigator extends Navigator<NavGraph> {
/ /... Control changes to NavGraph
}
@Navigator.Name("NoOp")
public class NoOpNavigator extends Navigator<NavDestination> {
/ /... Ignore STH.
}
Copy the code
The NavigatorProvider class manages these five navigators in a very simple way. MNavigators HashMap
cache navigators added via addNavigator, where key is the XXX given in @navigator.name (” XXX “) annotation. It is fetched from the cache to the caller when getNavigator is used.
When we use NavHostFragment, the framework will add the first four navigators for me, These are the onCreateNavController(mNavController) methods called in the onCreate method of NavHostFragment mentioned above:
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
/ / add DialogFragmentNavigator
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
/ / add FragmentNavigator
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
Copy the code
And NavController constructor:
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
/ / here
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
Copy the code
The general logic of Navigation framework is as follows:
NavHostFragment as the first Fragment loaded by the Activity container, maintains an instance of NavController, and adds four NavigatorProvider types to perform different view jump logic. At the end of the onCreate method, the navController.setgraph method is used to set the nvGraph ID configured in the Fragment tag. Redirect NavHostFragment to startDestination configured in navigation. XML. The NavController jump logic is also distributed to different Navigators via the jump type, however, via the internally maintained NavigatorProvider.
Now it’s clear that the jump method we call in SourceFragment:
nextButton.setOnClickListener {
findNavController().navigate(R.id.action_sourceFragment_to_targetFragment)
}
Copy the code
It is eventually dispatched to the Navigate method of the FragmentNavigator through a series of processes:
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
/ / a little...
final FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
/ / a little...
ft.setReorderingAllowed(true);
ft.commit();
/ / a little...
}
Copy the code
The Navigation framework is still based on FragmentTransaction encapsulation! Because when opening a new Fragment, the old Fragment is replaced directly, and the Fragment reliving life cycle is a cliche.
Now that we know why, let’s start working on the solution. Just replace the replace method with hidden and add methods, as with the original Fragment redraw solution. There’s one thing that needs to be noted, though, because the Activity container first loads navHostFragments. This Fragment needs to be replaced, and other fragments are not.
Create copy a FragmentNavigator class named NoReplaceFragmentNavigator pay equal attention to, only to navigate method modifying part:
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
//ft.replace(mContainerId, frag); Before the change
if (mBackStack.size > 0) {
ft.hide(mFragmentManager.getFragments.get(mBackStack.size - 1)).add(mContainerId,frag);
}else{ ft.replace(mContainerId, frag); }}Copy the code
Copy the NavHostFragment class and rename it NoReplaceNavHostFragment. Modify createFragmentNavigator. The original FragmentNavigator replaced with NoReplaceFragmentNavigator can. CreateFragmentNavigator is deprecated, but there is no alternative to createFragmentNavigator
Finally, the Activity in the container layout file fragments of the tag in the android: name attribute is modified to NoReplaceFragmentNavigator the full path.
Unresolved issues:
Using the hidden method of FragmentTransaction does not change the life cycle of the current Fragment. The SourceFragment lifecycle does not change at all during phases 2 and 3 mentioned above. FragmentTransaction has always had this problem, but there is a compromise solution that overwrites the Fragment onHiddenChanged method:
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if(hidden){
// The Fragment is not visible
}else{
// The current Fragment is visible}}Copy the code
You can put logic that needs to be processed during the Lifecycle into this method, but using Lifecycle listening for Fragment Lifecycle changes doesn’t help…
If you have a better solution, welcome to share ~
If this article can help you, please like, comment and follow us