如何应对 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 天前
SnapshotFlow还是collectAsState?对于Jetpack Compose来说哪个更香?
android·kotlin·android jetpack
_一条咸鱼_2 天前
Android Runtime安全上下文管理(76)
android·面试·android jetpack
_一条咸鱼_2 天前
Android Runtime跨进程调用优化方案深度解析(75)
android·面试·android jetpack
_一条咸鱼_2 天前
OpenGL ES 深度剖析
android·面试·android jetpack
_一条咸鱼_3 天前
Android Runtime直接内存管理原理深度剖析(73)
android·面试·android jetpack
_一条咸鱼_5 天前
Vulkan入门教程:源码级解析
android·面试·android jetpack
_一条咸鱼_5 天前
Android Runtime内存共享与访问控制原理剖析(71)
android·面试·android jetpack
刘龙超6 天前
如何应对 Android 面试官 -> 玩转 Jetpack Room
android jetpack
刘龙超7 天前
如何应对 Android 面试官 -> 玩转 Jetpack ViewModel
android jetpack