Jetpack-Navigation笔记

1,643 阅读11分钟

前言(可跳过)

关于jetpack部分不在赘述 . "Navigation 是一个框架,用于在 Android 应用中的“目标”之间导航,不论目标是作为 Fragment、Activity 还是其他组件实现,该框架都会提供一致的 API。" 主要由以下三个关键部分组成: 官网介绍
  1. Navigation graph:xml文件 添加导航图。
  2. NavHost:显示导航图中目标的空白容器。导航组件包含一个默认 NavHost 实现 (NavHostFragment),可显示 Fragment 目标。
  3. NavController:在 NavHost 中管理应用导航的对象。当用户在整个应用中移动时,NavController 会安排 NavHost 中目标内容的交换。
  • Navigation 能出处理那些东西或者优势是什么?
    1.处理 Fragment 事务
    2.默认情况下,正确处理往返操作。
    3.为动画和转换提供标准化资源。  (动画一会体验一哈)
    4.包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。  (以下实例为底部导航)
    5.Safe Args - 可在目标之间导航和传递数据时提供类型安全的 Gradle 插件。(后面有总结)
    6.ViewModel 支持 - 您可以将 ViewModel 的范围限定为导航图,以在图表的目标之间共享与界面相关的数据。(这个不适用Navigation 常规的底部导航或者Activity+多个Fragment直接也能共享数据  fragment+dialogfragment 设置targetfragment viewmodel也能实现共享数据 这个后面就不总结了)
下面是BottomNavigationView+Navigation 快速实现底部导航栏的小栗子. 关于底部导航栏的实现方式 前人已经总结的很好了这里不在赘述 依然范特稀西-Android 底部导航栏 (底部 Tab) 最佳实践

快速开始(可跳过)

注意:如果您要在 Android Studio 中使用 Navigation 组件,则必须使用 Android Studio 3.3 或更高版本。
  • 添加依赖
dependencies {
    def nav_version = "2.3.0-alpha05"
    // Kotlin
    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
    // For BottomNavigationView from Material Components 演示使用
    implementation 'com.google.android.material:material:1.2.0-alpha02'
}
  • 创建导航图(Create a navigation graph) xml资源文件
    1. Project切换Android, 右键 NEW -> Android Resource File
    2. file name字段中输入名称 按照惯例或者命名规范 命名为 main_nav_graph
    3. 从 Resource type 下拉列表中选择 Navigation OK.
main_nav_menu.xml如下

<?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"
    tools:ignore="UnusedNavigation">

</navigation>
  • 点击加号 依次添加HomeFragment ListFragment MineFragment(自己创建的)

  • 切换Code视图代码自动添加成如下 注意 startDestination 默认第一个

  • 回过头看一下xml 文件

<!--activity_main.xml-->
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!--
      android:name 属性包含 NavHost 实现的类名称。
      app:navGraph 属性将 NavHostFragment 与导航图相关联。导航图会在此 NavHostFragment 中指定用户可以导航到的所有目的地。
      app:defaultNavHost="true" 属性确保您的 NavHostFragment 会拦截系统返回按钮。
      请注意,只能有一个默认 NavHost。如果同一布局(例如,双窗格布局)中有多个主机,请务必仅指定一个默认 NavHost。

      个人理解
      android:name="androidx.navigation.fragment.NavHostFragment" 写死  感兴趣的可以去看看源码
      app:defaultNavHost="true"  true 拦截返回键
      app:navGraph  xml文件
      -->
    <fragment
        android:id="@+id/navHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/bottomNav"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        app:navGraph="@navigation/main_nav_graph" />


    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNav"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="@android:color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:menu="@menu/bottom_nav_menu" />

</androidx.constraintlayout.widget.ConstraintLayout>

<!--main_nav_menu.xml-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/home"
        android:icon="@drawable/ic_home"
        android:contentDescription="@string/title_home"
        android:title="@string/title_home" />
    <item
        android:id="@+id/list"
        android:icon="@drawable/ic_list"
        android:contentDescription="@string/title_list"
        android:title="@string/title_list" />
    <item
        android:id="@+id/mine"
        android:icon="@drawable/ic_feedback"
        android:contentDescription="@string/title_mine"
        android:title="@string/title_mine" />
</menu>
  • NavController 在 NavHost 中管理应用导航的对象

    • NavHost 点进去看源码
    public interface NavHost {
    
        //...
        @NonNull
        NavController getNavController();
    }
    
    • NavController? 接着看
    public class NavController {
    @NonNull
    public NavDeepLinkBuilder createDeepLink() {
        return new NavDeepLinkBuilder(this);
    }
    
    /**
     * Saves all navigation controller state to a Bundle.
     *
     * <p>State may be restored from a bundle returned from this method by calling
     * {@link #restoreState(Bundle)}. Saving controller state is the responsibility
     * of a {@link NavHost}.</p>
     *
     * @return saved state for this controller
     */
    @CallSuper
    @Nullable
    public Bundle saveState() {
        Bundle b = null;
        ArrayList<String> navigatorNames = new ArrayList<>();
        Bundle navigatorState = new Bundle();
        for (Map.Entry<String, Navigator<? extends NavDestination>> entry :
                mNavigatorProvider.getNavigators().entrySet()) {
            String name = entry.getKey();
            Bundle savedState = entry.getValue().onSaveState();
            if (savedState != null) {
                navigatorNames.add(name);
                navigatorState.putBundle(name, savedState);
            }
        }
        if (!navigatorNames.isEmpty()) {
            b = new Bundle();
            navigatorState.putStringArrayList(KEY_NAVIGATOR_STATE_NAMES, navigatorNames);
            b.putBundle(KEY_NAVIGATOR_STATE, navigatorState);
        }
        if (!mBackStack.isEmpty()) {
            if (b == null) {
                b = new Bundle();
            }
            Parcelable[] backStack = new Parcelable[mBackStack.size()];
            int index = 0;
            for (NavBackStackEntry backStackEntry : mBackStack) {
                backStack[index++] = new NavBackStackEntryState(backStackEntry);
            }
            b.putParcelableArray(KEY_BACK_STACK, backStack);
        }
        if (mDeepLinkHandled) {
            if (b == null) {
                b = new Bundle();
            }
            b.putBoolean(KEY_DEEP_LINK_HANDLED, mDeepLinkHandled);
        }
        return b;
    }
    
    @CallSuper
    public void restoreState(@Nullable Bundle navState) {
        if (navState == null) {
            return;
        }
    
        navState.setClassLoader(mContext.getClassLoader());
    
        mNavigatorStateToRestore = navState.getBundle(KEY_NAVIGATOR_STATE);
        mBackStackToRestore = navState.getParcelableArray(KEY_BACK_STACK);
        mDeepLinkHandled = navState.getBoolean(KEY_DEEP_LINK_HANDLED);
    }
    
    void setLifecycleOwner(@NonNull LifecycleOwner owner) {
        mLifecycleOwner = owner;
        mLifecycleOwner.getLifecycle().addObserver(mLifecycleObserver);
    }
    
    void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
        if (mLifecycleOwner == null) {
            throw new IllegalStateException("You must call setLifecycleOwner() before calling "
                    + "setOnBackPressedDispatcher()");
        }
        // Remove the callback from any previous dispatcher
        mOnBackPressedCallback.remove();
        // Then add it to the new dispatcher
        dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
    }
    
    void setViewModelStore(@NonNull ViewModelStore viewModelStore) {
        if (!mBackStack.isEmpty()) {
            throw new IllegalStateException("ViewModelStore should be set before setGraph call");
        }
        mViewModel = NavControllerViewModel.getInstance(viewModelStore);
    }
    
    
    主要是处理一些返回事件 关联LifecycleObserver 状态恢复restoreState Bundle saveState数据传参处理 感兴趣的可以看一下。继续看官网文档
    • Navigate to a destination 导航实现跳转的几个方法如下
    Kotlin:
     Fragment.findNavController()
     View.findNavController()
     Activity.findNavController(viewId: Int)
     
    Java:
     NavHostFragment.findNavController(Fragment)
     Navigation.findNavController(Activity, @IdRes int viewId)
     Navigation.findNavController(View)
    
demo走起 MainActivity中
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        bottomNav.setOnNavigationItemSelectedListener{item->
            when(item.itemId){
                R.id.home->{
                    //方法点进入会发现kotlin 内联方法
                    findNavController(R.id.navHostFragment).navigate(R.id.homeFragment)
                }
                R.id.list->{
                    findNavController(R.id.navHostFragment).navigate(R.id.listFragment)
                }
                R.id.mine->{
                    findNavController(R.id.navHostFragment).navigate(R.id.mineFragment)
                }
            }
            true
        }
    }
}

不等你提出质疑 就这?就这?就这?

果断先跑一下

跑出了hello world的开心

  • 顺着文档往下看 发现NavigationUI组件 使用也很简单将上述代码改为
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
//        bottomNav.setOnNavigationItemSelectedListener{item->
//            when(item.itemId){
//                R.id.homeFragment->{
//                    findNavController(R.id.navHostFragment).navigate(R.id.homeFragment)
//                }
//                R.id.listFragment->{
//                    findNavController(R.id.navHostFragment).navigate(R.id.listFragment)
//                }
//                R.id.mineFragment->{
//                    findNavController(R.id.navHostFragment).navigate(R.id.mineFragment)
//                }
//            }
//            true
//        }
        bottomNav.setupWithNavController(findNavController(R.id.navHostFragment))
    }
}
一开始发现点击死活都没反应 后来发现要将menu中的item id要和navigation中fragment中的id保持一致。这点需要注意哈
  • 回过头处理小细节性的东西
main_nav_graph.xml中 
fragment android:label 标签是actionBar显示的情况下标题,如果看到切换标题未随之切换 还需要在activity中设置 setupActionBarWithNavController(navController) 即可。不过国内这种设计几乎可以不考虑 大部分隐藏了supportActionBar?.hide() 即可。
  • 跳转处理

    通过小栗子我们知道了activity跳转fragment的场景 其他场景怎么跳转

    1.fragment跳转fragment

    • 1.进入nav_graph.xml 导航视图文件,把要跳转的fragment加入进来 然后在homefragment 右侧拖动箭头指向要跳转的fragment.切到code视图xml如下

      会发现导航fragment里多了action标签指向目的地fragment.同时我们在切换Design会发现action已经有内容了

      点开内容如下

      既然如此 我们试着直接点击action添加试试看 首先我们新建Mine2Fragment 然后点击加号 NEW Destination.点击导航fragment Actions 在Destinations找到我们刚才添加进来的目的地fragment选中 之后Id 会自动帮我们填上.同时我们会发现我们可以设置转场动画 启动方式等。感兴趣的可以自己试试 跳转代码 findNavController().navigate(R.id.list2Fragment) 具体见demo

  • 关于动画

    • 1.action 添加动画
    <fragment
        android:id="@+id/listFragment"
        android:name="com.pan.navigationdemo.ListFragment"
        android:label="ListFragment"
        tools:layout="@layout/list_frag">
    
        <!--
            值得注意的是api跳转的时候
            findNavController().navigate(R.id.action_listFragment_to_list2Fragment)
            id 指的是action id 而非 目的地fragment的id 否则动画效果无效
            app:enterAnim="@anim/fragment_open_enter"
            app:exitAnim="@anim/fragment_close_exit"
        -->
        <action
            android:id="@+id/action_listFragment_to_list2Fragment"
            app:destination="@id/list2Fragment"
            />
    
    </fragment>
    

    2.在目的地之间添加过渡效果 代码示例:

    ListFragment 中
    doAction.setOnClickListener {
           // Fragment 目的地共享元素过渡
           // https://developer.android.com/guide/navigation/navigation-animate-transitions#fragment_destination_shared_element_transitions
           val extras = FragmentNavigatorExtras(doAction to "textView")
           findNavController().navigate(R.id.list2Fragment,
           null,       // Bundle of args
           null,  // NavOptions
               extras)
          // findNavController().navigate(R.id.action_listFragment_to_list2Fragment) 转场动画
       }
    list_frag.xml 
    <TextView
       android:id="@+id/doAction"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center"
       android:text="点我跳List2Fragment"
       android:transitionName="textView"
       android:textSize="25sp" />
    
    list2_frag.xml
    <TextView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center_horizontal"
       android:layout_marginTop="100dp"
       android:text="List2Fragment"
       android:textSize="25sp"
       android:transitionName="textView" />
    
    注意android:transitionName="textView" 两边都要设置相同的键
     ```
     
    
  • 然后到传参这块

    传参这块自从有了livedata+viewmodel之后方便了太多
      1.单个Activity+多个fragment viewmodel实现订阅
      2.fragment+dialogfragment setTargetFragment viewmodel订阅
    
    Navigation怎么传呢?
      1.xml 目的地fragment 设置argument 然后点击rebuild
      <fragment
      android:id="@+id/listFragment"
      android:name="com.pan.navigationdemo.ListFragment"
      android:label="ListFragment"
      tools:layout="@layout/list_frag">
    
          <action
          android:id="@+id/action_listFragment_to_list2Fragment"
          app:destination="@id/list2Fragment"
          app:enterAnim="@anim/fragment_open_enter"
          app:popExitAnim="@anim/fragment_close_exit"
          >
          </action>
      </fragment>
      
      <!--目的地fragment 设置argument 然后点击rebuild-->
      <fragment
      android:id="@+id/list2Fragment"
      android:name="com.pan.navigationdemo.List2Fragment"
      android:label="List2Fragment" >
      <!--   具体参考 https://developer.android.google.cn/guide/navigation/navigation-pass-data?hl=zh-cn#Safe-args-->
      <argument
          android:name="testArgs"
          app:argType="string"
          android:defaultValue="hello pp." />
    
      2.使用 Safe Args 传递安全的数据
      配置环境不说了 文档很详细配置完 在xml添加完argument  clear rebuild project.传参代码如下:
      
      class ListFragment : BaseFragment() {
          override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
              super.onViewCreated(view, savedInstanceState)
              doAction.setOnClickListener {
              //==================== 传参 方式一
              //  ListFragmentDirections 编译生成的 不行的话 退出重新打开Android Studio或 clear rebuild一下试试.
              //如果还不行
              // apply plugin: 'com.android.application'
              //apply plugin: 'kotlin-android'
              //apply plugin: 'kotlin-android-extensions'
              //apply plugin: 'kotlin-kapt'
              //apply plugin: 'androidx.navigation.safeargs.kotlin' 顺序放在最下面
              //  val action = ListFragmentDirections.actionListFragmentToList2Fragment("方式一")
              //  findNavController().navigate(action)
    
              //==================== 传参 方式二 Bundle
                  val bundleOf = bundleOf("testArgs" to "方式二", "xx" to "xxx")
                  findNavController().navigate(R.id.action_listFragment_to_list2Fragment,bundleOf)
              }
          }
      }
      
      目的地fragment如下: X+Args 自动生成的文件 前提是你在目的地fragment xml文件中先设置过argument才行
      //by navArgs() 错误提示 Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper '-jvm-target' option 需要在gradle中设置如下
      compileOptions {
          targetCompatibility JavaVersion.VERSION_1_8
          sourceCompatibility JavaVersion.VERSION_1_8
      }
      //Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper '-jvm-target' option
      kotlinOptions {
          jvmTarget = JavaVersion.VERSION_1_8.toString()
      }
      
      // val args:List2FragmentArgs by navArgs() 
      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
          super.onViewCreated(view, savedInstanceState)
          //方式一 Bundle 直接拿
          textView.text = arguments?.getString("testArgs")
          //方式二  List2FragmentArgs 
          // textView.text = args.testArgs
      }
    
  • deep link 为目的地创建深层链接

    在 Android 中,深层链接是指将用户直接转到应用内特定目的地的链接。借助 Navigation 组件,您可以创建两种不同类型的深层链接:显式深层链接和隐式深层链接。 先抛开理论性的东西。我们知道了这块大概有显示和隐式链接两个概念。

    1.Create an explicit deep link(创建显示链接)

    显式深层链接是深层链接的一个实例,该实例使用 PendingIntent 将用户转到应用内的特定位置。 例如,您可以在通知、应用快捷方式或应用微件中显示显式深层链接。

    您可以使用 NavDeepLinkBuilder 类构建 PendingIntent,如下例所示。请注意,如果提供的上下文不是 Activity,构造函数会使用 PackageManager.getLaunchIntentForPackage() 作为默认 Activity 来启动(如果有)。(官网)

    应用场景: 通知、应用快捷方式或应用微件中显示显式深层链接。
    关键词: NavDeepLinkBuilder 类构建 PendingIntent.
    PendingIntent类相信大家都不陌生(都是知识点啊 陌生的话花个几分钟查一下),主要三个方法PendingIntent.getActivities(), PendingIntent.getService(),PendingIntent.getBroadcast().大家用的最多的就是通知场景了 下面一段通知的小代码:
    list2_frag.xml中 添加一个点击事件  点击生成通知 点击通知跳转AlertDetailsActivity
    
    <!--  为目标创建深层链接  -->
    <TextView
        android:id="@+id/deepLink"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="deep link"
        android:textSize="25sp"
        android:transitionName="textView" />
        
     List2Fragment中生成点击事件
     //为目标创建深层链接
        val channel_name = getString(R.string.app_name)
        deepLink.setOnClickListener {
            val notificationId = System.currentTimeMillis().toInt()
            val intent = Intent()
            val mainIntent = Intent()
            intent.setClass(requireContext(), AlertDetailsActivity::class.java)
            mainIntent.setClass(requireContext(), MainActivity::class.java)
            mainIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK  or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
            val pendingIntent = PendingIntent.getActivities(
                requireContext(),
                0,
                arrayOf(mainIntent, intent),
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            var builder: NotificationCompat.Builder
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val importance = NotificationManager.IMPORTANCE_HIGH
                val channel =
                    NotificationChannel(notificationId.toString(), channel_name, importance).apply {
                        description = "我曾经听人讲过 当你不可以再拥有的时候 你唯一可以做的 就是令自己莫忘记"
                    }
    
                val notificationManager: NotificationManager =
                    requireActivity().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
                notificationManager.createNotificationChannel(channel)
    
                builder = NotificationCompat.Builder(requireContext(), notificationId.toString())
                    .setSmallIcon(android.R.drawable.ic_popup_reminder)
                    .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_background))
                    .setContentTitle("My notification")
                    .setContentText("Hello World!")
                    .setStyle(NotificationCompat.BigTextStyle().bigText("我曾经听人讲过 当你不可以再拥有的时候 你唯一可以做的 就是令自己莫忘记"))
                    .setContentIntent(pendingIntent)
                    .setAutoCancel(true)  //点击自动取消 不设置需要滑动删除
            } else {
                builder = NotificationCompat.Builder(requireContext(), channel_name)
                    .setSmallIcon(android.R.drawable.ic_popup_reminder)
                    .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_background))
                    .setContentTitle("My notification")
                    .setContentText("Hello World!")
                    .setPriority(NotificationCompat.PRIORITY_MAX) //设置优先级
                    .setStyle(NotificationCompat.BigTextStyle().bigText("我曾经听人讲过 当你不可以再拥有的时候 你唯一可以做的 就是令自己莫忘记"))
                    .setContentIntent(pendingIntent)
                    .setAutoCancel(true)  //点击自动取消 不设置需要滑动删除
            }
            with(NotificationManagerCompat.from(requireContext())) {
                notify(notificationId, builder.build())
            }
        }
       
    
    上面同样的需求用NavDeepLinkBuilder生成的pendingIntent实现
    • 创建名为 x_nav_garpg.xml(命名规范)的导航视图文件 将目的地activity加进来即可。传参代码如下
        
        <?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/alertdetails_nav_graph.xml"
            app:startDestination="@id/alertDetailsActivity"
            tools:ignore="UnusedNavigation">
            <activity
                android:id="@+id/alertDetailsActivity"
                android:name="com.pan.navigationdemo.AlertDetailsActivity"
                android:label="AlertDetailsActivity">
                <argument
                    android:name="textView"
                    android:defaultValue="hello pp."
                    app:argType="string" />
            </activity>
        </navigation>
        
        通知代码pendingIntent替换为
        val bundle = Bundle().apply {
                putString("textView","我曾经听人讲过 当你不可以再拥有的时候 你唯一可以做的 就是令自己莫忘记")
            }
        val pendingIntent = NavDeepLinkBuilder(requireContext())
            .setGraph(R.navigation.main_nav_graph)
            .setDestination(R.id.mineFragment)
            .setArguments(bundle)
            .createPendingIntent()
        
        目的地activity接受参数为 
        private val args by navArgs<AlertDetailsActivityArgs>()
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.alertdetails_act)
    //        textView.text = intent.getStringExtra("textView")
            textView.text = args.textView
        }
    
  • 创建隐式深层链接

  • Demo地址 NavigationDemo

总结

1.下次尽量录屏

2.还是不太深入 比如如何动态化实现导航 如果全部都要在xml中添加的话感觉不太舒服

感谢

1.android-navigation

2.官方文档

3.底部Tab的历史演变以及前人总结 值得花两分钟浏览 依然范特稀西-Android 底部导航栏 (底部 Tab) 最佳实践

4.NavigationDemo