【Android Jetpack】Navigation——基础入门

1. 什么是Navigation

Jetpack里有一种管理Fragment的新架构模式,即:单个Activity嵌套多个FragmentUI架构模式。分为三个关键部分:

  • 导航图:包含所有导航相关信息的 XML 资源,便于我们理清页面间的关系。
  • NavHostFragment:显示导航图中目标的空白容器,可以认为是Fragment的容器。
  • NavController:用于控制页面的切换工作。

2. 为什么引入Navigation

在之前的开发中,通常使用FragmentManagerFragmentTransaction来管理Fragment之间的切换。对应的一般需要完成:

App bar的管理、Fragment间的切换动画,以及Fragment间的参数传递。纯代码的方式使用起来不是特别友好,并且FragmentApp bar在管理和使用的过程中显得很混乱。
Android Jetpack应用指南-叶坤-微信读书 (qq.com)

Jetpack提供了一个名为Navigation的组件可以解决上述问题。可以:

  • 方便添加页面切换动画。
  • 通过NavigationUI类,对菜单、底部导航、抽屉菜单导航进行统一的管理。
  • 页面间类型安全的参数传递。
  • 支持深层链接DeepLink
  • 包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。
  • ViewModel支持 - 您可以将ViewModel的范围限定为导航图,以在图表的目标之间共享与界面相关的数据。

注意:如果要使用导航,则必须使用Android Studio 3.3 或更高版本

3. 简单使用

3.1 环境

引入依赖:

def nav_version = "2.4.2"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")

res目录下创建navigation目录:
在这里插入图片描述

然后新建一个nav_graph.xml文件:
在这里插入图片描述

3.2 布局文件

<?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=".navi.MainActivity">

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

</androidx.constraintlayout.widget.ConstraintLayout>
  • 使用android:name告诉系统,这是一个特殊的Fragment
  • app:defaultNavHost属性设置为true,则该Fragment会自动处理系统返回键(设置为trueNavHostFragment会拦截系统返回按钮)。
  • app:navGraph用于指定导航图。

为了测试,这里新建两个fragment页面。然后在nav_graph.xml可视化界面配置fragment页面之间的链接关系。如果navigation的可视化界面一直加载,可以尝试清理一下AS的缓存,即:File -> Invalidate Caches/Restart。然后点击加号进行添加刚添加的Fragemnt
在这里插入图片描述

扫描二维码关注公众号,回复: 14215758 查看本文章

然后通过拖拽的方式添加action
在这里插入图片描述

xml文件中,可以看见:

<fragment
    android:id="@+id/firstFragment"
    android:name="com.weizu.myapplication.navi.fragments.FirstFragment"
    android:label="fragment_first"
    tools:layout="@layout/fragment_first" >
    <action
        android:id="@+id/action_firstFragment_to_secondFragment3"
        app:destination="@id/secondFragment3" />
</fragment>
<fragment
    android:id="@+id/secondFragment3"
    android:name="com.weizu.myapplication.navi.fragments.SecondFragment"
    android:label="fragment_two"
    tools:layout="@layout/fragment_two" />

对应的新增了一个action,并且指定了目的地为secondFragment3

3.3 处理逻辑

然后在FirstFragment添加点击事件,为了方便这里使用ViewBinding来完成控件的查找。并设置对应的跳转,比如:

class FirstFragment : Fragment() {
    
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
    
    
        val binding = FragmentFirstBinding.inflate(inflater, container, false)
        binding.textView.setOnClickListener {
    
    
            // 使用NavController
            Navigation.findNavController(it)
                .navigate(R.id.action_firstFragment_to_secondFragment3)
        }
        return binding.root
    }
}

来完成跳转。然后可以设置在ActionBar自动添加返回箭头。

3.4 ActionBar返回箭头

只需要在MainActivity 配置NavigationUI.setupActionBarWithNavController即可:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main4)

        val navController = Navigation.findNavController(this, R.id.nav_host_fragment_container)
        // 添加后才会在Action有对应的向上返回箭头图标
        NavigationUI.setupActionBarWithNavController(this, navController)
    }
}

结果:
在这里插入图片描述

3.5 返回键设置事件

注意到在3.4中只是显示了这个返回箭头,但是点击后没有事件响应。这里需要做一个处理,通过NavController来实现,需要在Activity中复写onSupportNavigateUp方法,比如下面的实现:

class MainActivity : AppCompatActivity() {
    
    

    private lateinit var navController: NavController

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main4)

        navController = Navigation.findNavController(this, R.id.nav_host_fragment_container)
        // 添加后才会在Action有对应的向上返回箭头图标
        NavigationUI.setupActionBarWithNavController(this, navController)
    }

    // 响应返回箭头
    override fun onSupportNavigateUp(): Boolean {
    
    
        return navController.navigateUp()
    }
}

添加后,返回按键就可以响应事件。

4. 导航到目的地

在前面我们已经使用了NavController来完成导航到目的地的操作。事实上Kotlin中可以使用六种方式来获取NavController

导航方式一: Navigation.findNavController(View)

binding.textView.setOnClickListener {
    
    
    //导航方式一: Navigation.findNavController(View)
    Navigation.findNavController(it)
        .navigate(R.id.action_firstFragment_to_secondFragment3)
}

导航方式二: Fragment.findNavController()

binding.textView.setOnClickListener {
    
    
    // 方式二: Fragment.findNavController()
    this@FirstFragment.findNavController()
        .navigate(R.id.action_firstFragment_to_secondFragment3)
}

导航方式三: View.findNavController()

binding.textView.setOnClickListener {
    
    
    // 方式三: View.findNavController()
    it.findNavController()
        .navigate(R.id.action_firstFragment_to_secondFragment3)
}

导航方式四: Activity.findNavController(viewId: Int)

binding.textView.setOnClickListener {
    
    
    // 方式四: Activity.findNavController(viewId: Int)
    // nav_host_fragment_container 为NavHostFragment容器
    activity?.findNavController(R.id.nav_host_fragment_container)
        ?.navigate(R.id.action_firstFragment_to_secondFragment3)
}

导航方式五:NavHostFragment.findNavController(Fragment)

binding.textView.setOnClickListener {
    
    
    // 方式五:NavHostFragment.findNavController(Fragment)
    // NavHostFragment中注册了navigation controller作为根,故而可以找到
    NavHostFragment.findNavController(this)
        .navigate(R.id.action_firstFragment_to_secondFragment3)
}

导航方式六:Navigation.findNavController(Activity, @IdRes int viewId)

binding.textView.setOnClickListener {
    
    
    // 方式六:Navigation.findNavController(Activity, @IdRes int viewId)
    activity?.let {
    
     it1 ->
        Navigation.findNavController(it1, R.id.nav_host_fragment_container)
            .navigate(R.id.action_firstFragment_to_secondFragment3)
    }
}

5. 参数传递Sage Args

这里回顾一下之前非Jetpack开发中是如何来实现多个Fragment之间的参数传递的:

5.1 静态添加的fragment

如果Activityfragment比较简单,且直接使用xmlandroid:name来指定自定义Fragment页面。这种方式这里叫做静态添加方式。有如下解决方式:

  • 直接在Activity中获取的Fragment实例(findViewById)然后将Activity作为中转进行传递参数。
  • 类似的,也可以在Fragemnt中获取activity对象,然后通过findViewById来找到其余的fragment实例,然后进行操作,本质上来说和第一种一样,只是将操作数据代码放置在了自定义Fragment之中。
  • 还可以定义接口,然后在Activity中设置接口的实例。

可以看出这些方式均是通过Activity这个中间人来进行完成的参数传递。这种做法的明显缺点就是代码耦合度比较高,因此不怎么适用。

5.2 动态添加的fragment

在前面提到了,在Android中对Fragment的管理通常使用FragmentManagerFragmentTransaction来管理Fragment之间的切换。那么在这种方式下的Fragment之间参数传递是如何完成的?这里也来回顾一下,参考文档:在 Fragment 之间传递数据 | Android 开发者 | Android Developers (google.cn)

按照文档描述可以分为两类:

平级 父子级
数据从Fragment B 发送到 Fragment A 子级 Fragment 发送数据到其父级 Fragment
在这里插入图片描述 在这里插入图片描述

5.2.1 平级

Fragment 1.3.0-alpha04 开始,每个FragmentManager都会实现 FragmentResultOwner。这意味着 FragmentManager 可以充当Fragment结果的集中存储区。此更改通过设置 Fragment 结果并监听这些结果,而不要求 Fragment 直接引用彼此,让单独的 Fragment 相互通信。

假定此时需要AFragmentBFragment通讯,那么需要在AFragment中找到FragmentManager对象,然后通过setFragmentResult传递一个Bundle对象。这里需要使用相同的requestKey在同一FragmentManager上设置结果。

首先是发送数据:

class AFragment : Fragment() {
    
    

    private var listener: SwitchListener? = null

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

    override fun onResume() {
    
    
        super.onResume()
        view?.findViewById<TextView>(R.id.a_fragment_textView)?.setOnClickListener {
    
    
            Snackbar.make(it, "正在切换到BFragment", Snackbar.LENGTH_LONG)
                .show()
            // 回调函数
            listener?.onClick()
            // 传递数据到BFragment
            parentFragmentManager.setFragmentResult("requestKey", Bundle().apply {
    
    
                putString("bundleKey", "value")
            })
        }
    }

    fun setSwitchListener(l: SwitchListener){
    
    
        listener = l
    }
}

然后在BFragment中接收:

class BFragment : Fragment() {
    
    

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

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)

        parentFragmentManager.setFragmentResultListener(
            "requestKey",
            this,
            object : FragmentResultListener{
    
    
                override fun onFragmentResult(requestKey: String, result: Bundle) {
    
    
                    view?.let {
    
    
                        val string = result.getString("bundleKey")
                        Snackbar.make(it, "BFragment收到数据内容:${
      
       string }", Snackbar.LENGTH_LONG)
                            .show()
                    }
                }
            }
        )
    }
}

至于Activity中就是完成切换Fragment,核心为:

supportFragmentManager.beginTransaction()
    .add(R.id.fragment_container, aFragment)
    .add(R.id.fragment_container, bFragment)
    .hide(bFragment)
    .show(aFragment)
    .commit()

最终就可以做到将数据从AFragment传递到BFragment

5.2.2 父子级

如需将结果从子级 Fragment 传递到父级 Fragment,父级 Fragment 在调用setFragmentResultListener()时应使用getChildFragmentManager()而不是getParentFragmentManager()
孩子传递数据:

Bundle result = new Bundle();
result.putString("bundleKey", "result");
// The child fragment needs to still set the result on its parent fragment manager
getParentFragmentManager().setFragmentResult("requestKey", result);

Fragment 接收:

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    // We set the listener on the child fragmentManager
    getChildFragmentManager().setFragmentResultListener("key", this, new FragmentResultListener() {
    
    
        @Override
        public void onFragmentResult(@NonNull String key, @NonNull Bundle bundle) {
    
    
            String result = bundle.getString("bundleKey");
            // Do something with the result..
        }
    });
}

5.3 其余方式

比如EventBusViewModel、文件等方式。当然,在NavController中也提供了一个可传递普通对象的方法:

navigate(@IdRes resId: Int, args: Bundle?)

也可以用于参数的传递。

5.4 Navigation的Safe Args

参考文档:在目的地之间传递数据 | Android 开发者 | Android Developers (google.cn)

值得注意的是,文档提到了:

通常情况下,强烈建议您仅在目的地之间传递最少量的数据。例如,您应该传递键来检索对象而不是传递对象本身,因为在Android上用于保存所有状态的总空间是有限的。如果您需要传递大量数据,不妨考虑使用 ViewModel(如在 Fragment 之间共享数据中所述)。

也即是:其实Safe Args比较鸡肋。

5.4.1 环境配置

在项目的build.gradle文件中配置:

buildscript {
    repositories {
        google()
    }
    dependencies {
        val nav_version = "2.4.2"
        classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version")
    }
}

以及在app模块中添加:

plugins {
    ...
    id "androidx.navigation.safeargs"
}

启用 Safe Args 后,该插件会生成代码,其中包含您定义的每个操作的类和方法。对于每个操作,Safe Args 还会为每个源目的地(生成相应操作的目的地)生成一个类。生成的类的名称由源目的地类的名称和“Directions”一词组成。例如,如果目的地的名称为SpecifyAmountFragment,生成的类的名称为SpecifyAmountFragmentDirections。生成的类为源目的地中定义的每个操作提供了一个静态方法。该方法会将任何定义的操作参数作为参数,并返回可传递到navigate()NavDirections对象。

5.4.2 使用

比如在nav_graph.xml中配置一个username参数:

<fragment
    android:id="@+id/firstFragment"
    android:name="com.weizu.myapplication.navi.fragments.FirstFragment"
    android:label="fragment_first"
    tools:layout="@layout/fragment_first" >
    <action
        android:id="@+id/action_firstFragment_to_secondFragment3"
        app:destination="@id/secondFragment3"
        app:enterAnim="@android:anim/slide_in_left"
        app:exitAnim="@android:anim/slide_out_right"
        >
 <!--定义参数-->
    <argument
        android:name="user_name"
        app:argType="string"
        android:defaultValue="张三"
        />
</action>
   
</fragment>

注意到argument定义在action标签下,故而在导航的时候,是通过action来封装和赋值。比如在源Fragment中传递:

binding.textView.setOnClickListener {
    // 方式六:Navigation.findNavController(Activity, @IdRes int viewId)
    activity?.let { it1 ->
//                Navigation.findNavController(it1, R.id.nav_host_fragment_container)
//                    .navigate(R.id.action_firstFragment_to_secondFragment3)

        val action: NavDirections =
            FirstFragmentDirections.actionFirstFragmentToSecondFragment3("李思思")
        view?.findNavController()?.navigate(action)
    }
}

在目标Fragment中,直接获取即可:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    val string = arguments?.getString("user_name")
    Snackbar.make(view, "${ string }", Snackbar.LENGTH_LONG)
        .show()
}

但其实这个过程和Bundle类似。

5. 后

由于篇幅原因,拟将接下来的内容放置到下个小节中继续。

猜你喜欢

转载自blog.csdn.net/qq_26460841/article/details/124531062