Navigation导航组件的使用:内容承载容器NavHostFragment加载原理
Posted LQS_Android
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Navigation导航组件的使用:内容承载容器NavHostFragment加载原理相关的知识,希望对你有一定的参考价值。
Navigation导航组件由以下三个关键部分组成:
- 导航图:在一个集中的位置包含所有与导航相关的信息的XML资源。这包括应用程序中所有单独的内容区域,称为目的地,以及用户可以在应用程序中使用的可能路径。
NavHost
一个空容器,显示导航图中的目的地。导航组件包含一个默认的NavHost实现NavHostFragment,它显示片段目的地。NavController:一个在NavHost中管理应用导航的对象。当用户在应用中移动时,NavController在NavHost中协调目标内容的交换。
当你在应用中导航时,你告诉NavController你想沿着导航图中的特定路径导航,或者直接导航到特定的目的地。NavController然后在NavHost中显示相应的目的地。
Navigation组件提供了许多其他的好处,包括:
①片段处理事务。
②默认情况下正确处理Up和Back操作。
③为动画和过渡提供标准化资源。
④实现和处理深度链接。
⑤包括导航UI模式,比如导航抽屉和底部导航,只需要最少的额外工作。
⑥安全参数-一个Gradle插件,在目标之间导航和传递数据时提供类型安全。
⑦ViewModel支持——您可以将ViewModel限定在导航图中,以便在图的目的地之间共享与ui相关的数据。
⑧此外,你可以使用android Studio的导航编辑器来查看和编辑你的导航图。
其他基础知识参见总结:Android导航组件Navigation从入门到精通_yingaizhu的博客-CSDN博客_android 导航组件
在模块化开发我们Android项目时,我们是分模块进行开发的,我们不可能都知道每一个类的全类名的,也就无法在提供页面节点信息时,准确的提供name属性的值,例如Fragment页面节点name属性:
<?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/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_home"
android:name="com.xw.ppjoke.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/navigation_dashboard"
android:name="com.xw.ppjoke.ui.dashboard.DashboardFragment"
android:label="@string/title_dashboard"
tools:layout="@layout/fragment_dashboard" />
<fragment
android:id="@+id/navigation_notifications"
android:name="com.xw.ppjoke.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
</navigation>
这里的:android:name="com.xw.ppjoke.ui.home.HomeFragment"...等;
介于原生Navigation导航组件这点的局限性,我们需要将Navigation组件结合资源文件进行打造成一个更加灵活的Navigation导航组件。
为此,我们需要了解原生导航组件Navigation的原理。下面我们通过内容承载容器NavHostFragment类进行学习和熟悉原理。
在MainActivity中找到对应的资源文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<fragment
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
在<fragment/>标签中name属性值“androidx.navigation.fragment.NavHostFragment”就是内容承载容器,通过它我们来学习Navigation的工作原理。它的源代码如下:
/*
* 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.annotation.CallSuper;
import androidx.annotation.NavigationRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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" />
* <android.support.design.widget.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 {
private static final String KEY_GRAPH_ID = "android-support-nav:fragment:graphId";
private 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);
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);
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 #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 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}.
*/
@SuppressWarnings({"WeakerAccess", "deprecation"})
@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)}
*/
@SuppressWarnings("DeprecatedIsStillUsed")
@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;
}
}
我们从它的生命周期OnCreate()方法说起:
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
final Context context = requireContext();
mNavController = new NavHostController(context);
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);
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 #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 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}.
*/
@SuppressWarnings({"WeakerAccess", "deprecation"})
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
这其中添加了两个导航器,一个是用于FragmentDialog导航的DialogFragmentNavigator和另一个用于Fragment导航的createFragmentNavigator()导航器。那么在这个onCreateNavController()方法的参数类型NavController中,它的构造方法又定义了其他的导航器:
/**
* Constructs a new controller for a given {@link Context}. Controllers should not be
* used outside of their context and retain a hard reference to the context supplied.
* If you need a global controller, pass {@link Context#getApplicationContext()}.
*
* <p>Apps should generally not construct controllers, instead obtain a relevant controller
* directly from a navigation host via {@link NavHost#getNavController()} or by using one of
* the utility methods on the {@link Navigation} class.</p>
*
* <p>Note that controllers that are not constructed with an {@link Activity} context
* (or a wrapped activity context) will only be able to navigate to
* {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK new tasks} or
* {@link android.content.Intent#FLAG_ACTIVITY_NEW_DOCUMENT new document tasks} when
* navigating to new activities.</p>
*
* @param context context for this controller
*/
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
一个是NavGraph导航器NavGraphNavigator(mNavigatorProvider)),它是用来启动第一个Fragment的。另外一个是用于Activity进行导航的ActivityNavigator(mContext)导航器;
至此这里Navigation导航器中共提供了四类导航器:
①NavGraph导航器NavGraphNavigator(mNavigatorProvider));
②Activity进行导航的ActivityNavigator(mContext)导航器;
③Fragment导航的createFragmentNavigator()导航器;
④FragmentDialog导航的DialogFragmentNavigator导航器;
待续...
以上是关于Navigation导航组件的使用:内容承载容器NavHostFragment加载原理的主要内容,如果未能解决你的问题,请参考以下文章
Android Jetpack的导航组件(Navigation组件)的使用
向@react-navigation/drawer 组件发送道具