Jetpack —— 让 Navigation 来管理 Fragment

在 Android 中,我们使用 FragmentManagerFragmentTransaction 来管理 Fragment 之间的切换,使用 addToBackStack 来管理 Fragment 栈,Android 为了推广 Fragment,在 Jetpack 库中推出了 Navigation 组件,方便我们更好的管理 Fragment。

除了管理 Fragment 之间的切换,Navigation 还提供给了更强大的功能:

  • 可视化的页面导航图,类似 xcode 中的 StoryBoard,便于我们看清页面之间的关系
  • 通过 destination 和 action 来完成页面间的导航
  • 方便的页面切换动画
  • 页面间类型安全的参数传递
  • 通过 NavigationUI 类,对菜单,底部导航,抽屉菜单导航进行方便统一的管理
  • 深层链接

Android 大力推广 Jetpack,是不是代表着 Android 在推荐使用 Fragment 替代 Activity?

在学习 Navigation 之前,先明白几个概念:

  • Navigation Graph

    一个 XML 文件,里面包含了应用中 Fragment 之间的关系

  • NavHostFragment

    这是一种特殊的布局文件,Navigation Graph 中的页面通过该 Fragment 展示

  • NavController

    用于在代码中完成 Navigation Graph 中具体的页面切换的对象

当我们要进行页面跳转时,使用 NavController 对象,告诉它你想要去 Navigation Graph 中的哪个页面,NavController 会将相关的页面展示在 NavHostFragment 中。

基础用法

第一步,创建 Navigation Graph

res 目录右键 New -> Android Resource File,输入文件名,然后 Resource Type 选择 Navigation,点击 OK 之后,系统会自动在 res 目录下生成一个 navigation 目录,里面就是我们刚才创建的资源文件 nav.xml:


<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav">

</navigation>

我们将在这个文件中描述各个 Fragment 之间的关系。

第二步,创建 Fragment 并添加到 Navigation Graph 中

创建 Fragment 的方法和之前一样:

布局


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="这是第一个 Fragment"
        android:textColor="#FF0000"
        android:textSize="30sp" />
</RelativeLayout>

Fragment

package com.antonioleiva.mvpexample.app.navigation.fragmentlayout;

public class FirstFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_first, container, false);
        return view;
    }
}

在 Navigation Graph 中添加 Fragment


<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"
    app:startDestination="@id/firstFragment">

    <fragment
        android:id="@+id/firstFragment"
        android:name="com.antonioleiva.mvpexample.app.navigation.fragmentlayout.FirstFragment"
        android:label="fragment_first"
        tools:layout="@layout/fragment_first" />
</navigation>

第三步,创建 NavHostFragment

NavHostFragment 相当于一个 Fragment 的容器,编辑 Activity 的布局文件:


<RelativeLayout 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"
    tools:context="com.antonioleiva.mvpexample.app.navigation.MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav" />
</RelativeLayout>

其中:

  • android:name 固定为 androidx.navigation.fragment.NavHostFragment,表明是一个 NavHostFragment。
  • app:defaultNavHost 值为 true 时,表示拦截返回键,即将返回交给 NavHostFragment 处理。
  • app:navGraph 将 NavHostFragment 与之前创建的 Navigation Graph 相关联。

就这样,一个简单的 Navigation 示例就完成了。

Fragment 之间跳转

一个简单的页面当然是不够的,Navigation 可以帮助我们实现 Fragment 之间的切换,再添加一个 Fragment 并按照刚才的方法添加到 Navigation Graph 中去

<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"
    app:startDestination="@id/firstFragment">


    <fragment
        android:id="@+id/firstFragment"
        android:name="com.antonioleiva.mvpexample.app.navigation.fragmentlayout.FirstFragment"
        android:label="fragment_first"
        tools:layout="@layout/fragment_first"/>
    <fragment
        android:id="@+id/secondFragment"
        android:name="com.antonioleiva.mvpexample.app.navigation.fragmentlayout.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second" />
</navigation>

怎么讲这两个 Fragment 联系在一起呢?在 nav.xmlDesign 中选中 firstFragment,鼠标选中其右侧的圆圈,拖拽至右边的 secondFragment,松开鼠标,会发现两个 Fragment 连在一起了,而此时 Text 中的内容也变成了这样:


<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"
app:startDestination="@id/firstFragment">


<fragment
    android:id="@+id/firstFragment"
    android:name="com.antonioleiva.mvpexample.app.navigation.fragmentlayout.FirstFragment"
    android:label="fragment_first"
    tools:layout="@layout/fragment_first">
    <action
        android:id="@+id/action_firstFragment_to_secondFragment2"
        app:destination="@id/secondFragment" />
</fragment>
<fragment
    android:id="@+id/secondFragment"
    android:name="com.antonioleiva.mvpexample.app.navigation.fragmentlayout.SecondFragment"
    android:label="fragment_second"
    tools:layout="@layout/fragment_second" />
</navigation>

你会发现在 firstFragment 中多了一个 action 节点,其中:

  • id:系统自动生成,是这个 action 的 id,从名字上就可以看到这个 Action 是干什么的。
  • destination:目标 Fragment 的 id。

接下来,修改一下 FirstFragment 的布局:


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="这是第一个 Fragment"
        android:textColor="#FF0000"
        android:textSize="30sp" />

    <Button
        android:id="@+id/to_second_fragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/text"
        android:text="跳转到 SecondFragment" />
</RelativeLayout>

之前说过了,在 Navigation 中,通过 NavController 完成 Fragment 切换,那么编辑下 FirstFragment:

public class FirstFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_first, container, false);
        Button button = view.findViewById(R.id.to_second_fragment);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                NavController controller = Navigation.findNavController(v);
                controller.navigate(R.id.action_firstFragment_to_secondFragment2);
            }
        });
        return view;
    }
}

上面是通过 Navigation.findNavController() 方法获取 NavController,系统提供了两种获取 NavController 的方法:

  1. findNavController(Activity activity, int viewId) :通过 Activity 及其包含的 View 获取 NavController。
  2. findNavController(View view) :直接通过 View 获取 NavController。

然后调用 NavController 的 navigate 方法来完成切换,该方法的参数就是 Navigation Graph 中 Action 的 id。

app:defaultNavHost 属性的作用在这里就显示出来了,当该属性值为 true 时,表示返回键受到 Navigation 接管,在 SecondFragment 点击返回键,会返回到 FirstFragment;该属性值为 false 时,点击返回键会直接退出 Activity。

添加切换动画

首先,在 res/anim 文件夹下加入常见的动画文件,动画文件的写法跟 Activity 动画的写法一样,这里不赘述,系统也提供了几个默认的动画效果可以使用。

点击两个关联 Fragment 之间的连线,就可以在右边的 Animations 就可以指定切换动画,如图所示:

其中:

  • enterAnim 和 exitAnim 是去往栈里添加一个 destination 时两个 destination 的动画
  • popEnterAnim 和 popExitAnim 是从栈里移除一个 destination 时的动画。

共享组件

除了切换动画之外,Navigation 还支持 Fragment 之间的元素共享,但元素共享只能通过代码方式添加,因为它包含共享的 View 实例。

每种类型的目标都通过 Navigator.Extras 接口的子类实现此编程 API。 Extras 被传递给 navigate() 调用。

FragmentNavigator.Extras extras = new FragmentNavigator.Extras.Builder()
    .addSharedElement(imageView, "header_image")
    .addSharedElement(titleView, "header_title")
    .build();
Navigation.findNavController(view).navigate(R.id.details,
    null, // Bundle of args
    null, // NavOptions
    extras);

传递数据

Fragment 之间传递参数有两种方法:

  1. 使用 navigate(@IdRes int resId, @Nullable Bundle args) 方法传递一个 Bundle 参数,Bundle 对象携带着要传递的数据,这种方式也就限制了可传递的数据类型为:

    1. 基本数据类型
    2. 可以被序列化的对象。
    3. 资源(必须是 @resourceType/resourceName 格式的)

    目标 Fragment 可以使用 getArguments() 方法获取到传递过来的 Bundle 对象。

  2. 类型安全的传递方式。

    这种方式需要添加依赖:

       dependencies {
           classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha01"
       }

xml apply plugin: 'androidx.navigation.safeargs'

添加这个依赖,需要项目是 AndroidX 的,如果不是,会抛出异常:Cause: androidx.navigation.safeargs can only be used with an androidx project

添加完依赖之后编辑 NavigationGraph 文件,例如我们想要像 SecondFragment 传递一个名为 Address 的参数:

       <fragment
           android:id="@+id/secondFragment"
           android:name="com.antonioleiva.mvpexample.app.navigation.fragmentlayout.SecondFragment"
           android:label="fragment_second"
           tools:layout="@layout/fragment_second">
           <argument
               android:name="Address"
               android:defaultValue="未设置"
               app:argType="string" />
       </fragment>

可以看到添加了一个 argument 项,其中:

  • name:属性名
  • defaultValue:默认值
  • argType:数据类型

    然后 rebuild 一下项目,就会发现在 GeneratedJava 目录下多了两个文件:

  • FirstFragmentDirections

  • SecondFragmentArgs

    这两个文件是自动生成的,不用理会,如果没有自动生成,就需要检查一下究竟是哪一步做错了。

    然后发送数据也很简单:

   FirstFragmentDirections.ActionFirstFragmentToSecondFragment2 action = FirstFragmentDirections.actionFirstFragmentToSecondFragment2();
   action.setAddress("北京市昌平区");
   Navigation.findNavController(v).navigate(action);

接受数据:

   Bundle arguments = getArguments();
           if (arguments != null) {
               String address = SecondFragmentArgs.fromBundle(arguments).getAddress();
           }

深层链表(DeepLink)

Navigation 组件提供了对深层链接(DeepLink)的支持。通过该特性,我们可以利用 PendingIntent 或者一个真实的 URL 链接,直接跳转到应用程序的某个 destination(Fragment/Activity)。

最常见的两种使用场景:

  1. PendingIntent 的方式。当你的应用程序收到某个通知推送,你希望用户在点击该通知时,能够直接跳转到展示该通知内容的页面,那么就可以通过 PendingIntent 来完成此操作。

  2. URL 的方式。当用户在手机 Web 页面上浏览我们网站上的某个页面时,我们可以在网页上放置一个类似“在应用内打开”的按钮。当用户的手机安装有你的应用程序,通过 deepLink 就能打开相应的页面,如果没有安装,那么我们的网站可以导航到应用程序的下载页面,从而引导用户安装应用程序。

    下面举例来说明这两种情况的使用方式:

1.PendingIntent

通过 sendNotification() 方法,向通知栏发送一条通知,发送通知的时候需要设置 PendingIntent

   /**
    * 向通知栏发送一个通知
    * */
   private void sendNotification()
   {
       if(getActivity() == null)
       {
           return;
       }

       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
       {
           int importance = NotificationManager.IMPORTANCE_DEFAULT;
           NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "ChannelName", importance);
           channel.setDescription("description");
           NotificationManager notificationManager = getActivity().getSystemService(NotificationManager.class);
           notificationManager.createNotificationChannel(channel);
       }

       NotificationCompat.Builder builder = new NotificationCompat.Builder(getActivity(), CHANNEL_ID)
               .setSmallIcon(R.drawable.ic_launcher_foreground)
               .setContentTitle("DeepLinkDemo")
               .setContentText("Hello World!")
               .setPriority(NotificationCompat.PRIORITY_DEFAULT)
               .setContentIntent(getPendingIntent())//设置PendingIntent
               .setAutoCancel(true);

       NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getActivity());
       notificationManager.notify(notificationId, builder.build());
   }

构建一个 PendingIntent 对象,在其中设置,当通知被点击后需要跳转到的 destination 及传递的参数。

   /**
    * 通过PendingIntent设置,当通知被点击后需要跳转到哪个destination,以及传递的参数
    * */
   private PendingIntent getPendingIntent()
   {
       if(getActivity() != null)
       {
           Bundle bundle = new Bundle();
           bundle.putString("params", "from Notification");
           return Navigation
                   .findNavController(getActivity(), R.id.sendNotification)
                   .createDeepLink()
                   .setGraph(R.navigation.graph_deep_link_activity)
                   .setDestination(R.id.deepLinkSettingsFragment)
                   .setArguments(bundle)
                   .createPendingIntent();
       }
       return null;
   }

2.URL

这种方式也很简单。

第一步:在导航图中为 destination 添加 <deepLink/> 标签。

注意: app:uri 属性中填入的是你的网站的相应 web 页面地址,后方的参数会通过 Bundle 对象传递到 destination 中

<fragment
          android:id="@+id/deepLinkSettingsFragment"
          android:name="com.michael.deeplinkdemo.DeepLinkSettingsFragment"
          android:label="fragment_deep_link_settings"
          tools:layout="@layout/fragment_deep_link_settings">

    <!-- 为destination添加<deepLink/>标签 -->
    <deepLink app:uri="www.YourWebsite.com/{params}" />

</fragment>

第二步:为相应的Activity设置 <nav-graph/> 标签,这样,当用户在 Web 中访问到你的网站时,你的应用程序便能监听到

<application
             android:allowBackup="true"
             android:icon="@mipmap/ic_launcher"
             android:label="@string/app_name"
             android:roundIcon="@mipmap/ic_launcher_round"
             android:supportsRtl="true"
             android:theme="@style/AppTheme">
    <activity android:name=".DeepLinkActivity">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>

        <!-- 为Activity设置<nav-graph/>标签 -->
        <nav-graph android:value="@navigation/graph_deep_link_activity" />

    </activity>

</application>

第三步:测试

我们可以在 Google app 中输入相应的 Web 地址,也可以通过 adb 工具,使用命令行来完成操作

adb shell am start -a android.intent.action.VIEW -d "http://www.YourWebsite.com/fromWeb"

执行该命令,你的手机便能直接打开 deepLinkSettingsFragment。在该 Fragment 中,我们可以通过 Bundle 对象获取相应的参数(fromWeb),从而完成后续的操作。

   @Override
   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
   {
       View view = inflater.inflate(R.layout.fragment_deep_link_settings, container, false);
       Bundle bundle = getArguments();
       if(bundle != null)
       {
           String params = bundle.getString("params");
           TextView tvDesc = view.findViewById(R.id.tvDesc);
           if(!TextUtils.isEmpty(params))
           {
               tvDesc.setText(params);
           }
       }
       return view;
   }

本部分内容转载自 Navigation(四)DeepLink的使用

NavigationUI

Fragment 的切换,除了 Fragment 页面本身的切换,通常还伴有 App bar 的变化。为了方便统一管理, Navigation 组件引入了 NavigationUI 类。通过这篇文章,我们来看看如何使用 NavigationUI 来对 App bar 和页面切换进行管理。

App bar的管理

NavigationUI 提供了 setupActionBarWithNavController 方法,将 App bar 与 NavController 绑定,这样,当 NavController 为你完成 Fragment 切换时,系统会自动为你在 App bar 中完成一些常见操作。比如,当你切换到一个新的 Fragment 时,系统会自动在 App bar 的左侧添加返回按钮,响应返回事件等。

NavigationUI 对三种类型的 App bar 提供了支持:

  • Toolbar
  • ActionBar
  • CollapsingToolbarLayout

我们来看 NavController 具体如何与 App bar 绑定。以 ActionBar 为例:

appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).setDrawerLayout(drawerLayout).build();
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

通过 AppBarConfiguration 类,我们可以对 App bar 进行配置

通过 NavigationUI.setupActionBarWithNavController() 方法,将 App bar 与 NavController 绑定

注意:对于CollapsingToolbarLayout/Toolbar也有类似的绑定方法

Fragment的管理

常见的 Fragment 切换通常是通过菜单来完成的,菜单通常有三种形式:

  • App bar 左侧的抽屉菜单(DrawLayout + NavigationView)
  • App bar 右侧的菜单(menu)
  • 底部菜单(BottomNavigationView)

当我们在导航图中为 Fragment 页面添加 id 后,在对应的 menu 中,也为其指定相同的 id,那么,当菜单被点击时,系统会为我们自动切换 Fragment。

与此同时,NavController 提供了一个名为 OnDestinationChangedListener 的接口,对 Destination 切换事件进行监听。

navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener()
{
    @Override
    public void onDestinationChanged(@NonNull NavController controller, @NonNull NavDestination destination, @Nullable Bundle arguments)
    {
        Toast.makeText(DrawerLayoutActivity.this, "onDestinationChanged() called", Toast.LENGTH_SHORT).show();
    }
});

以上便是 NavigationUI 的核心内容。相关代码量较大,这里不再叙述。我通过项目的方式为大家演示三种形式的菜单页面切换。

项目地址:NavigationUIDemo

本节内容转载自:Navigation(三)NavigationUI的使用

哦,忘了写添加依赖了:

    implementation "androidx.navigation:navigation-fragment:2.1.0-beta02"
    implementation "androidx.navigation:navigation-ui:2.1.0-beta02"
Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1