| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.navigation.fragment; |
| |
| import android.app.Dialog; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.os.Bundle; |
| import android.util.AttributeSet; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import androidx.activity.OnBackPressedDispatcher; |
| import androidx.activity.OnBackPressedDispatcherOwner; |
| import androidx.annotation.CallSuper; |
| import androidx.annotation.NavigationRes; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| import androidx.fragment.app.DialogFragment; |
| import androidx.fragment.app.Fragment; |
| import androidx.fragment.app.FragmentContainerView; |
| import androidx.navigation.NavController; |
| import androidx.navigation.NavGraph; |
| import androidx.navigation.NavHost; |
| import androidx.navigation.NavHostController; |
| import androidx.navigation.Navigation; |
| import androidx.navigation.Navigator; |
| |
| /** |
| * NavHostFragment provides an area within your layout for self-contained navigation to occur. |
| * |
| * <p>NavHostFragment is intended to be used as the content area within a layout resource |
| * defining your app's chrome around it, e.g.:</p> |
| * |
| * <pre class="prettyprint"> |
| * <androidx.drawerlayout.widget.DrawerLayout |
| * xmlns:android="http://schemas.android.com/apk/res/android" |
| * xmlns:app="http://schemas.android.com/apk/res-auto" |
| * android:layout_width="match_parent" |
| * android:layout_height="match_parent"> |
| * <fragment |
| * android:layout_width="match_parent" |
| * android:layout_height="match_parent" |
| * android:id="@+id/my_nav_host_fragment" |
| * android:name="androidx.navigation.fragment.NavHostFragment" |
| * app:navGraph="@navigation/nav_sample" |
| * app:defaultNavHost="true" /> |
| * <com.google.android.material.navigation.NavigationView |
| * android:layout_width="wrap_content" |
| * android:layout_height="match_parent" |
| * android:layout_gravity="start"/> |
| * </androidx.drawerlayout.widget.DrawerLayout> |
| * </pre> |
| * |
| * <p>Each NavHostFragment has a {@link NavController} that defines valid navigation within |
| * the navigation host. This includes the {@link NavGraph navigation graph} as well as navigation |
| * state such as current location and back stack that will be saved and restored along with the |
| * NavHostFragment itself.</p> |
| * |
| * <p>NavHostFragments register their navigation controller at the root of their view subtree |
| * such that any descendant can obtain the controller instance through the {@link Navigation} |
| * helper class's methods such as {@link Navigation#findNavController(View)}. View event listener |
| * implementations such as {@link android.view.View.OnClickListener} within navigation destination |
| * fragments can use these helpers to navigate based on user interaction without creating a tight |
| * coupling to the navigation host.</p> |
| */ |
| public class NavHostFragment extends Fragment implements NavHost { |
| /** @hide */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| protected static final String KEY_GRAPH_ID = "android-support-nav:fragment:graphId"; |
| /** @hide */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| protected static final String KEY_START_DESTINATION_ARGS = |
| "android-support-nav:fragment:startDestinationArgs"; |
| private static final String KEY_NAV_CONTROLLER_STATE = |
| "android-support-nav:fragment:navControllerState"; |
| private static final String KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost"; |
| |
| /** |
| * Find a {@link NavController} given a local {@link Fragment}. |
| * |
| * <p>This method will locate the {@link NavController} associated with this Fragment, |
| * looking first for a {@link NavHostFragment} along the given Fragment's parent chain. |
| * If a {@link NavController} is not found, this method will look for one along this |
| * Fragment's {@link Fragment#getView() view hierarchy} as specified by |
| * {@link Navigation#findNavController(View)}.</p> |
| * |
| * @param fragment the locally scoped Fragment for navigation |
| * @return the locally scoped {@link NavController} for navigating from this {@link Fragment} |
| * @throws IllegalStateException if the given Fragment does not correspond with a |
| * {@link NavHost} or is not within a NavHost. |
| */ |
| @NonNull |
| public static NavController findNavController(@NonNull Fragment fragment) { |
| Fragment findFragment = fragment; |
| while (findFragment != null) { |
| if (findFragment instanceof NavHostFragment) { |
| return ((NavHostFragment) findFragment).getNavController(); |
| } |
| Fragment primaryNavFragment = findFragment.getParentFragmentManager() |
| .getPrimaryNavigationFragment(); |
| if (primaryNavFragment instanceof NavHostFragment) { |
| return ((NavHostFragment) primaryNavFragment).getNavController(); |
| } |
| findFragment = findFragment.getParentFragment(); |
| } |
| |
| // Try looking for one associated with the view instead, if applicable |
| View view = fragment.getView(); |
| if (view != null) { |
| return Navigation.findNavController(view); |
| } |
| |
| // For DialogFragments, look at the dialog's decor view |
| Dialog dialog = fragment instanceof DialogFragment |
| ? ((DialogFragment) fragment).getDialog() |
| : null; |
| if (dialog != null && dialog.getWindow() != null) { |
| return Navigation.findNavController(dialog.getWindow().getDecorView()); |
| } |
| |
| throw new IllegalStateException("Fragment " + fragment |
| + " does not have a NavController set"); |
| } |
| |
| private NavHostController mNavController; |
| private Boolean mIsPrimaryBeforeOnCreate = null; |
| private View mViewParent; |
| |
| // State that will be saved and restored |
| private int mGraphId; |
| private boolean mDefaultNavHost; |
| |
| /** |
| * Create a new NavHostFragment instance with an inflated {@link NavGraph} resource. |
| * |
| * @param graphResId resource id of the navigation graph to inflate |
| * @return a new NavHostFragment instance |
| */ |
| @NonNull |
| public static NavHostFragment create(@NavigationRes int graphResId) { |
| return create(graphResId, null); |
| } |
| |
| /** |
| * Create a new NavHostFragment instance with an inflated {@link NavGraph} resource. |
| * |
| * @param graphResId Resource id of the navigation graph to inflate. |
| * @param startDestinationArgs Arguments to send to the start destination of the graph. |
| * @return A new NavHostFragment instance. |
| */ |
| @NonNull |
| public static NavHostFragment create(@NavigationRes int graphResId, |
| @Nullable Bundle startDestinationArgs) { |
| Bundle b = null; |
| if (graphResId != 0) { |
| b = new Bundle(); |
| b.putInt(KEY_GRAPH_ID, graphResId); |
| } |
| if (startDestinationArgs != null) { |
| if (b == null) { |
| b = new Bundle(); |
| } |
| b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs); |
| } |
| |
| final NavHostFragment result = new NavHostFragment(); |
| if (b != null) { |
| result.setArguments(b); |
| } |
| return result; |
| } |
| |
| /** |
| * Returns the {@link NavController navigation controller} for this navigation host. |
| * This method will return null until this host fragment's {@link #onCreate(Bundle)} |
| * has been called and it has had an opportunity to restore from a previous instance state. |
| * |
| * @return this host's navigation controller |
| * @throws IllegalStateException if called before {@link #onCreate(Bundle)} |
| */ |
| @NonNull |
| @Override |
| public final NavController getNavController() { |
| if (mNavController == null) { |
| throw new IllegalStateException("NavController is not available before onCreate()"); |
| } |
| return mNavController; |
| } |
| |
| @CallSuper |
| @Override |
| public void onAttach(@NonNull Context context) { |
| super.onAttach(context); |
| // TODO This feature should probably be a first-class feature of the Fragment system, |
| // but it can stay here until we can add the necessary attr resources to |
| // the fragment lib. |
| if (mDefaultNavHost) { |
| getParentFragmentManager().beginTransaction() |
| .setPrimaryNavigationFragment(this) |
| .commit(); |
| } |
| } |
| |
| @CallSuper |
| @Override |
| public void onCreate(@Nullable Bundle savedInstanceState) { |
| final Context context = requireContext(); |
| |
| mNavController = new NavHostController(context); |
| mNavController.setLifecycleOwner(this); |
| if (context instanceof OnBackPressedDispatcherOwner) { |
| mNavController.setOnBackPressedDispatcher( |
| ((OnBackPressedDispatcherOwner) context).getOnBackPressedDispatcher()); |
| // Otherwise, caller must register a dispatcher on the controller explicitly |
| // by overriding onCreateNavHostController() |
| } |
| // Set the default state - this will be updated whenever |
| // onPrimaryNavigationFragmentChanged() is called |
| mNavController.enableOnBackPressed( |
| mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate); |
| mIsPrimaryBeforeOnCreate = null; |
| mNavController.setViewModelStore(getViewModelStore()); |
| onCreateNavHostController(mNavController); |
| |
| 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); |
| } |
| } |
| |
| // We purposefully run this last as this will trigger the onCreate() of |
| // child fragments, which may be relying on having the NavController already |
| // created and having its state restored by that point. |
| super.onCreate(savedInstanceState); |
| } |
| |
| /** |
| * Callback for when the {@link NavHostController} is created. If you |
| * support any custom destination types, their {@link Navigator} should be added here to |
| * ensure it is available before the navigation graph is inflated / set. |
| * <p> |
| * This provides direct access to the host specific methods available on |
| * {@link NavHostController} such as |
| * {@link NavHostController#setOnBackPressedDispatcher(OnBackPressedDispatcher)}. |
| * <p> |
| * By default, this adds a {@link DialogFragmentNavigator} and {@link FragmentNavigator}. |
| * <p> |
| * This is only called once in {@link #onCreate(Bundle)} and should not be called directly by |
| * subclasses. |
| * |
| * @param navHostController The newly created {@link NavHostController} that will be |
| * returned by {@link #getNavController()} after |
| */ |
| @SuppressWarnings("deprecation") |
| @CallSuper |
| protected void onCreateNavHostController(@NonNull NavHostController navHostController) { |
| onCreateNavController(navHostController); |
| } |
| |
| /** |
| * Callback for when the {@link #getNavController() NavController} is created. If you |
| * support any custom destination types, their {@link Navigator} should be added here to |
| * ensure it is available before the navigation graph is inflated / set. |
| * <p> |
| * By default, this adds a {@link DialogFragmentNavigator} and {@link FragmentNavigator}. |
| * <p> |
| * This is only called once in {@link #onCreate(Bundle)} and should not be called directly by |
| * subclasses. |
| * |
| * @param navController The newly created {@link NavController}. |
| * @deprecated Override {@link #onCreateNavHostController(NavHostController)} to gain |
| * access to the full {@link NavHostController} that is created by this NavHostFragment. |
| */ |
| @SuppressWarnings({"DeprecatedIsStillUsed", "deprecation"}) |
| @Deprecated |
| @CallSuper |
| protected void onCreateNavController(@NonNull NavController navController) { |
| navController.getNavigatorProvider().addNavigator( |
| new DialogFragmentNavigator(requireContext(), getChildFragmentManager())); |
| navController.getNavigatorProvider().addNavigator(createFragmentNavigator()); |
| } |
| |
| // TODO: DialogFragmentNavigator should use FragmentOnAttachListener from Fragment 1.3 |
| @SuppressWarnings("deprecation") |
| @Override |
| public void onAttachFragment(@NonNull Fragment childFragment) { |
| super.onAttachFragment(childFragment); |
| DialogFragmentNavigator dialogFragmentNavigator = |
| mNavController.getNavigatorProvider().getNavigator(DialogFragmentNavigator.class); |
| dialogFragmentNavigator.onAttachFragment(childFragment); |
| } |
| |
| @CallSuper |
| @Override |
| public void onPrimaryNavigationFragmentChanged(boolean isPrimaryNavigationFragment) { |
| if (mNavController != null) { |
| mNavController.enableOnBackPressed(isPrimaryNavigationFragment); |
| } else { |
| mIsPrimaryBeforeOnCreate = isPrimaryNavigationFragment; |
| } |
| } |
| |
| /** |
| * Create the FragmentNavigator that this NavHostFragment will use. By default, this uses |
| * {@link FragmentNavigator}, which replaces the entire contents of the NavHostFragment. |
| * <p> |
| * This is only called once in {@link #onCreate(Bundle)} and should not be called directly by |
| * subclasses. |
| * @return a new instance of a FragmentNavigator |
| * @deprecated Use {@link #onCreateNavController(NavController)} |
| */ |
| @Deprecated |
| @NonNull |
| protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() { |
| return new FragmentNavigator(requireContext(), getChildFragmentManager(), |
| getContainerId()); |
| } |
| |
| @Nullable |
| @Override |
| public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, |
| @Nullable Bundle savedInstanceState) { |
| FragmentContainerView containerView = new FragmentContainerView(inflater.getContext()); |
| // When added via XML, this has no effect (since this FragmentContainerView is given the ID |
| // automatically), but this ensures that the View exists as part of this Fragment's View |
| // hierarchy in cases where the NavHostFragment is added programmatically as is required |
| // for child fragment transactions |
| containerView.setId(getContainerId()); |
| return containerView; |
| } |
| |
| /** |
| * We specifically can't use {@link View#NO_ID} as the container ID (as we use |
| * {@link androidx.fragment.app.FragmentTransaction#add(int, Fragment)} under the hood), |
| * so we need to make sure we return a valid ID when asked for the container ID. |
| * |
| * @return a valid ID to be used to contain child fragments |
| */ |
| private int getContainerId() { |
| int id = getId(); |
| if (id != 0 && id != View.NO_ID) { |
| return id; |
| } |
| // Fallback to using our own ID if this Fragment wasn't added via |
| // add(containerViewId, Fragment) |
| return R.id.nav_host_fragment_container; |
| } |
| |
| @Override |
| public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { |
| super.onViewCreated(view, savedInstanceState); |
| if (!(view instanceof ViewGroup)) { |
| throw new IllegalStateException("created host view " + view + " is not a ViewGroup"); |
| } |
| Navigation.setViewNavController(view, mNavController); |
| // When added programmatically, we need to set the NavController on the parent - i.e., |
| // the View that has the ID matching this NavHostFragment. |
| if (view.getParent() != null) { |
| mViewParent = (View) view.getParent(); |
| if (mViewParent.getId() == getId()) { |
| Navigation.setViewNavController(mViewParent, mNavController); |
| } |
| } |
| } |
| |
| @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); |
| final int graphId = navHost.getResourceId( |
| androidx.navigation.R.styleable.NavHost_navGraph, 0); |
| if (graphId != 0) { |
| mGraphId = graphId; |
| } |
| navHost.recycle(); |
| |
| final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment); |
| final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false); |
| if (defaultHost) { |
| mDefaultNavHost = true; |
| } |
| a.recycle(); |
| } |
| |
| @CallSuper |
| @Override |
| public void onSaveInstanceState(@NonNull Bundle outState) { |
| super.onSaveInstanceState(outState); |
| Bundle navState = mNavController.saveState(); |
| if (navState != null) { |
| outState.putBundle(KEY_NAV_CONTROLLER_STATE, navState); |
| } |
| if (mDefaultNavHost) { |
| outState.putBoolean(KEY_DEFAULT_NAV_HOST, true); |
| } |
| if (mGraphId != 0) { |
| outState.putInt(KEY_GRAPH_ID, mGraphId); |
| } |
| } |
| |
| @Override |
| public void onDestroyView() { |
| super.onDestroyView(); |
| if (mViewParent != null && Navigation.findNavController(mViewParent) == mNavController) { |
| Navigation.setViewNavController(mViewParent, null); |
| } |
| mViewParent = null; |
| } |
| } |