如何应对 Android 面试官 -> 玩转 Jetpack Navigation

前言


本章我们进行 Navigation 的学习;

是什么?

Navigation 是一个框架,用于在 Android 应用中的『目标』之间导航,该框架提供一致的 API,无论目标是作为 Fragment、Activity 还是其他组件实现。

使用篇

Navigation 的基础架构;

依赖

navigation的依赖支持

kotlin 复制代码
def nav_version = "xxx"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"

我们按照架构图,来构建一个简单的使用 demo;我们先来声明三个 Framgent,分别是 MainPageFragment1、MainPageFragment2、MainPageFragment3;

main_page_fragment1.xml

ini 复制代码
<LinearLayout 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/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#00BCD4">

    <TextView
        android:id="@+id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="MainPageFragment1"
        android:textSize="20sp"
        android:textStyle="bold"
        android:textColor="#3F51B5"
        android:layout_gravity="center" />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="跳转MainPageFragment2"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/message"
        android:layout_gravity="center"/>

</LinearLayout>

很简单的一个 xml,放了一个 textView 和一个 Button,用来点击跳转,接下来我们声明 MainPageFragment1

kotlin 复制代码
class MainPageFragment1 : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.main_page_fragment1, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 获取 Button 并设置点击事件
        val btn = view.findViewById<Button>(R.id.btn)
        btn.setOnClickListener { view ->
            // 点击跳转到第二个 Fragment
            Navigation.findNavController(view).navigate(R.id.action_page2)
        }
    }
}

接下来我们来声明 MainPageFramgent2 和 main_page_fragemtn2.xml,这个 Fragment 可以点击跳转到第三个 Fragment 也可以点击返回到第一个 Fragemnt;

main_page_fragment2.xml

ini 复制代码
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#3F51B5">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#3F51B5"
        android:text="MainPageFragment2"
        android:textSize="20sp"
        android:textStyle="bold"
        android:layout_gravity="center" />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="返回MainPageFragment1"
        android:textAllCaps="false"
        android:layout_gravity="center" />

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="前往MainPageFragment3"
        android:textAllCaps="false"
        android:layout_gravity="center"/>

</LinearLayout>

Fragemnt 声明如下:

kotlin 复制代码
class MainPageFragment2 : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.main_page_fragment2, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val btn = view.findViewById<Button>(R.id.btn)
        btn.setOnClickListener { view ->
            // 返回上一个 Fragment
            Navigation.findNavController(view).navigate(R.id.action_page1)
            // Navigation.findNavController(view).navigateUp(); 返回上一个Fragment
        }
        val btn2 = view.findViewById<Button>(R.id.btn2)
        btn2.setOnClickListener { view ->
            // 跳转到第三个 Fragemnt
            Navigation.findNavController(view).navigate(R.id.action_page3)
        }
    }
}

第三个 Fragment 就只能返回上一个 Fragment,参考 MainPageFragment1

ini 复制代码
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#9C27B0">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="MainPage3Fragment"
        android:textSize="20sp"
        android:textStyle="bold"
        android:textColor="#3F51B5"
        android:layout_gravity="center" />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="跳到MainPageFragment2"
        android:textAllCaps="false"
        android:layout_gravity="center" />

</LinearLayout>

Fragemnt 声明如下:

kotlin 复制代码
class MainPageFragment3 : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.main_page_fragment3, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val btn = view.findViewById<Button>(R.id.btn)
        btn.setOnClickListener { view ->
            // 返回第二个 Fragemnt
            Navigation.findNavController(view).navigate(R.id.action_page2)
            // 回退上一步
            // Navigation.findNavController(view).navigateUp();
        }
    }
}

我们接着来声明下 Activity 来管理这些 Fragment;

导航图

首先我们需要在 res 下创建 Navigation 导航包 nav_graph_main.xml,我们在 res 下右键创建 resource 的时候选择 navigation 就会自动创建一个 navigation 的文件夹以及对应的 nav_graph_main.xml 文件;

xml 复制代码
<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/nav_graph_main.xml"
    <!-- 默认启动第一个 Framgent -->
    app:startDestination="@id/page1Fragment">


    <!-- 这个是第一个 Fragment -->
    <fragment
        android:id="@+id/page1Fragment"
        android:name="com.llc.navigation.MainPageFragment1"
        android:label="fragment_page1"
        tools:layout="@layout/main_page_fragment1">
        <!--
            action:程序中使用 id 跳到 destination 对应的类
        -->
        <action
            android:id="@+id/action_page2"
            app:destination="@id/page2Fragment" />
    </fragment>


    <!-- 这个是第二个 Fragment -->
    <fragment
        android:id="@+id/page2Fragment"
        android:name="com.llc.navigation.MainPageFragment2"
        android:label="fragment_page2"
        tools:layout="@layout/main_page_fragment2">
        <action
            android:id="@+id/action_page1"
            app:destination="@id/page1Fragment" />
        <action
            android:id="@+id/action_page3"
            app:destination="@id/page3Fragment" />
    </fragment>


    <!-- 这个是第三个 Fragment -->
    <fragment
        android:id="@+id/page3Fragment"
        android:name="com.llc.navigation.MainPageFragment3"
        android:label="fragment_page3"
        tools:layout="@layout/main_page_fragment3">
        <action
            android:id="@+id/action_page2"
            app:destination="@id/page2Fragment" />
    </fragment>

</navigation>

导航包的创建就按照这样来创建;

接下来我们在 Activity 中使用这个『导航图』,我们来创建一个 MainActivity 以及对应的 xml;

ini 复制代码
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    
    <!--
        app:defaultNavHost="true"
        拦截系统back键
    -->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/my_nav_host_fragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="9"
        <!-- 核心逻辑在这里,一定要声明为 androidx.navigation.fragment.NavHostFragment -->
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        <!-- 核心逻辑在这里,一定要指定我们的导航图 -->
        app:navGraph="@navigation/nav_graph_main"/>
    
    <!-- 底部导航 View,由它来决定菜单怎么摆放 -->
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:itemTextColor="#ff0000"
        app:menu="@menu/menu" />

</LinearLayout>

xml 中一定要使用androidx.navigation.fragment.NavHostFragment来管理 Fragment;

xml 中一定要使用app:navGraph="@navigation/nav_graph_main"来指定导航图

xml 中可以使用app:defaultNavHost="true"接管系统的 back 键

Activity 中进行绑定;

我们先来提供下 BottomNavigationView 的 menu 菜单

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/page1Fragment"
        android:icon="@drawable/ic_launcher_foreground"
        android:title="第一页"/>

    <item
        android:id="@+id/page2Fragment"
        android:icon="@drawable/ic_launcher_foreground"
        android:title="第二页"/>

    <item
        android:id="@+id/page3Fragment"
        android:icon="@drawable/ic_launcher_foreground"
        android:title="第三页"/>

</menu>

接着在 Activity 中绑定

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    var bottomNavigationView: BottomNavigationView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        bottomNavigationView = findViewById(R.id.nav_view)

        val navHostFragment =
            supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment?
        val controller = navHostFragment!!.navController
        NavigationUI.setupWithNavController(bottomNavigationView!!, controller)
    }

}

运行,可以看下效果;到此就完成了 Navigation 的基础使用;

我们可以通过

scss 复制代码
Navigation.findNavController(view).navigateUp(); 返回上一个Fragment

这种方式就是将当前 Fragment 的上一个 Fragment 展示出来;

原理篇


原理:本质上就是 NavHostFragment 的生命周期,内部维护了一个栈用来存放 Fragment;

所以我们进入这个NavHostFragment看下:

NavHostFragment 中第一个激活的方法是create方法,为什么是这个方法呢?因为早期的官网提供的使用方式是这样的:

scss 复制代码
// 通过 create 方法获取 NavHostFragment
val finalHost = NavHostFragment.create(R.navigation.nav_graph_main)

supportFragmentManager.beginTransaction()
     .replace(R.id.ll_fragment_navigation, finalHost)
     .setPrimaryNavigationFragment(finalHost)
     .commit()

通过 create 方法获取 NavHostFragment,所以不管是早期方式,还是现在新的方式,都会调用到create 这个方法;

less 复制代码
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;
}

这个 graphResId 就是导航图的 id,create 方法就是把 graphResId、startDestinationArgs 保存到 bundle 中,并创建 NavHostFragment 对象;

接下来要执行的方法就是 onInfalte 方法,我们进入这个方法看下:

less 复制代码
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);
    // 核心逻辑1: app:navGraph="@navigation/nav_graph_main"       
    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);
    // 核心逻辑2:解析获取:app:defaultNavHost="true"
    final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
    if (defaultHost) {
        mDefaultNavHost = true;
    }
    a.recycle();
}

这个方法有两个核心逻辑,一个是:获取是否拦截系统 back 键(app:defaultNavHost="true"),一个是获取导航图 nav_graph_main.xml(app:navGraph="@navigation/nav_graph_main"),解析这个导航图中的所有 Fragment;

接下来执行的是 onCreateNavController方法,我们来看下这个方法;

less 复制代码
protected void onCreateNavController(@NonNull NavController navController) {
    navController.getNavigatorProvider().addNavigator(
            new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
    // 核心逻辑        
    navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}

navController.getNavigatorProvider() 这个 provider 中会保存我们需要的所有 Fragment;

swift 复制代码
public class NavigatorProvider {
    // 导航图中的所有的 Fragment 会存入到这个 HashMap 中;
    private static final HashMap<Class<?>, String> sAnnotationNames = new HashMap<>();
}

创建 Fragment 的 导航者 NavController ,我们进入这个 createFragmentNavigator 方法看下:

scss 复制代码
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
    return new FragmentNavigator(requireContext(), getChildFragmentManager(),
            getContainerId());
}

直接 new 出来 FragmentNavigator;另外这个 getContainerId方法中,此 ID,如果不写 xml 文件,单纯用代码实现的时候,需要得到一个父容器 ID;

接下来执行的方法是 onCreate 方法,我们进入这个方法看下:

ini 复制代码
public void onCreate(@Nullable Bundle savedInstanceState) {
    final Context context = requireContext();
    // 核心逻辑1 初始化 NavHostController
    mNavController = new NavHostController(context);
    mNavController.setLifecycleOwner(this);
    mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());

    mNavController.enableOnBackPressed(
            mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
    mIsPrimaryBeforeOnCreate = null;
    mNavController.setViewModelStore(getViewModelStore());
    onCreateNavController(mNavController);

    Bundle navState = null;
    // 核心逻辑2 用来恢复数据
    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) {
        mNavController.restoreState(navState);
    }
    if (mGraphId != 0) {
        // 核心逻辑3 解析nav_graph_main.xml文件 获取里面的配置信息
        mNavController.setGraph(mGraphId);
    } else {
        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) {
            // 核心逻辑3 解析nav_graph_main.xml文件 获取里面的配置信息
            mNavController.setGraph(graphId, startDestinationArgs);
        }
    }
    super.onCreate(savedInstanceState);
}

主要是 setGraph 方法,解析 nav_graph_main.xml 文件,并获取第一个要启动的目标 Fragment,闭并导航过去,我们进入这个方法看下:

less 复制代码
public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
    if (mGraph != null) {
        popBackStackInternal(mGraph.getId(), true);
    }
    mGraph = graph;
    // 核心逻辑
    onGraphCreated(startDestinationArgs);
}

onGraphCreated 创建第一个要启动的 Fragment 并导航过去;

less 复制代码
private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
    
    ....省略部分代码
    
    
    if (mGraph != null && mBackStack.isEmpty()) {
        boolean deepLinked = !mDeepLinkHandled && mActivity != null
                && handleDeepLink(mActivity.getIntent());
        if (!deepLinked) {
            // 核心逻辑,创建并导航过去
            navigate(mGraph, startDestinationArgs, null, null);
        }
    }
}

我们进入这个 navigate 方法看下:

less 复制代码
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    .... 省略部分代码
    
    // 核心逻辑,创建并导航过去 
    NavDestination newDest = navigator.navigate(node, finalArgs,
        navOptions, navigatorExtras);
}

这个 navigate 方法有几个实现,我们直接进入这个 FragmentNavigator 的 navigate 方法看下:

less 复制代码
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
        @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    // 反射创建第一个要启动的 Fragment
    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);
    }
    // replace 启动目标 Fragment
    // 将第一个要展示的Fragment replace NavHostFragment 到 xml 中定义的 FragmentContainerView 上
    ft.replace(mContainerId, frag);
    
    ....省略部分代码
    
    ft.commit();
}
  1. 反射创建目标 Fragment

  2. 通过 replace 启动目标 Fragment,将第一个要展示的 Fragment replace NavHostFragment 到 xml 中定义的 FragmentContainerView 上

接下来,我们来看下 onCreateView

less 复制代码
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                         @Nullable Bundle savedInstanceState) {
    FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
    containerView.setId(getContainerId());
    return containerView;
}

接下来,我们来看下onViewCreated

less 复制代码
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    .... 省略部分代码
    // 设置控制器
    Navigation.setViewNavController(view, mNavController);

    .... 省略部分代码
}

总结:创建 NavHostFragemnt,绑定到目前 Activity 之后,通过解析『导航图』获取要创建的 Fragment 存入到 NavigatorProvide(内部是一个 HashMap),并获取第一个要启动的 Fragment,通过反射创建这个 Fragment(后面的 Fragment 也都是通过反射创建的),并交给 FragmentManager 通过 replace 方法替换成目标 Fragemnt;

好了 Navigation 就写到这里吧~

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~

相关推荐
alexhilton1 天前
在Jetpack Compose中创建CRT屏幕效果
android·kotlin·android jetpack
峰哥的Android进阶之路1 天前
viewModel机制及原理总结
android jetpack
我命由我123453 天前
Android WebView - loadUrl 方法的长度限制
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Coffeeee3 天前
面试被问到Compose的副作用不会,只怪我没好好学
android·kotlin·android jetpack
Frank_HarmonyOS6 天前
Android APP 的压力测试与优化
android jetpack
QING6187 天前
Jetpack Compose 条件布局与 Layout 内在测量详解
android·kotlin·android jetpack
Lei活在当下7 天前
【现代 Android APP 架构】09. 聊一聊依赖注入在 Android 开发中的应用
java·架构·android jetpack
bqliang7 天前
Jetpack Navigation 3:领航未来
android·android studio·android jetpack
用户693717500138410 天前
🚀 Jetpack MVI 实战全解析:一次彻底搞懂 MVI 架构,让状态管理像点奶茶一样丝滑!
android·android jetpack
俩个逗号。。13 天前
ViewPager+Fragment 切换主题崩溃
android·android studio·android jetpack