11、DataBinding与Navigation:Safe Args与DataBinding、Fragment间数据传递、NavigationUI与DataBinding、深层链接处理

各位同学,今天我们来聊聊DataBinding和Navigation这对黄金搭档。说实话,我在早期做Android开发时,Fragment之间的数据传递一直是个让人头疼的问题。Bundle、接口回调、ViewModel共享……方式不少,但总觉得不够优雅。直到Navigation组件配合DataBinding出现,才真正让我觉得「嗯,这才是我想要的开发体验」。

这一章的内容比较密集,但都是实战中高频使用的技巧。我会从Safe Args与DataBinding的整合开始,逐步深入到Fragment间数据传递、NavigationUI的绑定,最后再聊聊深层链接的处理。准备好了吗?我们开始。

11.1 Safe Args与DataBinding:类型安全的传递

Safe Args是Navigation组件提供的一个Gradle插件,它能生成类型安全的类和方法,用于在目的地之间传递数据。说白了,就是让你不用再手写Bundle的put和get,也不用担心key写错或者类型转换异常。

我个人习惯在项目一开始就启用Safe Args,因为它带来的类型安全收益实在太大了。你想想看,如果不用Safe Args,你可能会写出这样的代码:

// 不用Safe Args的方式
val bundle = Bundle()
bundle.putString("user_name", "张三")
bundle.putInt("user_age", 25)
findNavController().navigate(R.id.action_home_to_detail, bundle)

// 在DetailFragment中接收
val userName = arguments?.getString("user_name") ?: ""
val userAge = arguments?.getInt("user_age", 0)

这段代码有什么问题?第一,key是硬编码的字符串,容易写错;第二,类型不匹配时只能在运行时崩溃;第三,代码冗余,每个Fragment都要写一遍。而用了Safe Args之后,一切都变得清爽了。

首先,在build.gradle中启用Safe Args:

// 项目级build.gradle
buildscript {
    repositories {
        google()
    }
    dependencies {
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.7.7"
    }
}

// 模块级build.gradle
plugins {
    id 'androidx.navigation.safeargs.kotlin'
}

然后在navigation的XML文件中定义参数:

<fragment
    android:id="@+id/detailFragment"
    android:name="com.example.app.DetailFragment"
    android:label="详情页">
    <argument
        android:name="userName"
        app:argType="string"
        android:defaultValue="未知用户" />
    <argument
        android:name="userAge"
        app:argType="integer"
        android:defaultValue="0" />
</fragment>

现在,结合DataBinding来使用这些参数。在DetailFragment的布局文件中:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="userName"
            type="String" />
        <variable
            name="userAge"
            type="int" />
    </data>
    <LinearLayout
        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:text="@{`姓名:` + userName}" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`年龄:` + String.valueOf(userAge)}" />
    </LinearLayout>
</layout>

在Fragment中,通过Safe Args生成的Args类来获取数据,并绑定到DataBinding:

class DetailFragment : Fragment() {
    private var _binding: FragmentDetailBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentDetailBinding.inflate(inflater, container, false)

        // 使用Safe Args获取参数
        val args = DetailFragmentArgs.fromBundle(requireArguments())
        binding.userName = args.userName
        binding.userAge = args.userAge

        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
小提示:Safe Args生成的Args类名规则是「Fragment名 + Args」。比如DetailFragment对应的就是DetailFragmentArgs。这个命名是固定的,用几次就记住了。

11.2 Fragment间数据传递:不止一种方式

Fragment间的数据传递,其实有几种不同的思路。Safe Args适合那种「从A跳到B,带几个参数过去」的场景。但如果数据需要双向传递,或者需要在多个Fragment之间共享,那就得另想办法了。

我在项目中遇到过这样的需求:用户在主列表页选择一个商品,跳转到详情页,然后在详情页修改商品数量,返回列表页时列表要同步更新。这种场景下,单纯用Safe Args就不够用了。

这里我推荐几种方案:

  • SharedViewModel + DataBinding:这是最推荐的方式。多个Fragment共享同一个ViewModel实例,数据变化自动反映到UI上。
  • SavedStateHandle:Navigation 2.4.0之后引入的,可以配合ViewModel使用,支持进程杀死后恢复数据。
  • 接口回调 + Fragment Result API:适合简单的回调场景,比如从详情页返回列表页时传递一个结果。

我们重点看看SharedViewModel的方式。首先定义一个ViewModel:

class SharedCartViewModel : ViewModel() {
    private val _selectedItem = MutableLiveData<CartItem>()
    val selectedItem: LiveData<CartItem> get() = _selectedItem

    private val _itemCount = MutableLiveData<Int>()
    val itemCount: LiveData<Int> get() = _itemCount

    fun selectItem(item: CartItem) {
        _selectedItem.value = item
    }

    fun updateCount(count: Int) {
        _itemCount.value = count
    }
}

然后在两个Fragment中共享这个ViewModel:

// 列表页Fragment
class CartListFragment : Fragment() {
    private val viewModel: SharedCartViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.viewModel = viewModel
        binding.lifecycleOwner = viewLifecycleOwner

        // 观察选中商品的变化
        viewModel.selectedItem.observe(viewLifecycleOwner) { item ->
            // 跳转到详情页
            val action = CartListFragmentDirections.actionCartListToDetail(item.id)
            findNavController().navigate(action)
        }
    }
}

// 详情页Fragment
class CartDetailFragment : Fragment() {
    private val viewModel: SharedCartViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.viewModel = viewModel
        binding.lifecycleOwner = viewLifecycleOwner

        // 当用户修改数量时,自动更新ViewModel
        binding.btnIncrease.setOnClickListener {
            val current = viewModel.itemCount.value ?: 0
            viewModel.updateCount(current + 1)
        }
    }
}
关键点:使用by activityViewModels()而不是by viewModels(),这样才能保证两个Fragment拿到的是同一个ViewModel实例。它们的生命周期跟随Activity,而不是Fragment。

11.3 NavigationUI与DataBinding:让导航更优雅

NavigationUI是Navigation组件中一个容易被忽视但非常实用的部分。它能把Navigation和AppBar、DrawerLayout、BottomNavigationView等UI组件无缝集成。配合DataBinding,代码可以写得非常简洁。

我记得有一次重构一个老项目,里面有个底部导航栏,每个Tab对应一个Fragment。原来的代码里,每个Fragment切换都要手动处理AppBar的标题、返回按钮的显示隐藏,代码量很大。用NavigationUI之后,这些全自动了。

先看布局文件,使用DataBinding绑定BottomNavigationView:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="navController"
            type="androidx.navigation.NavController" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_graph"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/bottom_nav"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottom_nav"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:menu="@menu/bottom_nav_menu"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

在Activity中,只需要几行代码就能完成绑定:

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        val navController = navHostFragment.navController

        // 将NavigationUI与BottomNavigationView绑定
        binding.bottomNav.setupWithNavController(navController)

        // 将Toolbar与Navigation绑定
        binding.toolbar.setupWithNavController(
            navController,
            AppBarConfiguration(navController.graph)
        )
    }
}

这里有个细节要注意:setupWithNavController方法会自动处理Fragment切换时的菜单选中状态、AppBar的标题更新、以及返回按钮的显示。你不需要写任何监听器。

注意:如果你的BottomNavigationView的菜单项和导航图中的目的地ID不一致,需要手动建立映射关系。可以在menu的item中设置app:navGraph="@navigation/nav_graph"来指定对应的导航图。

11.4 深层链接处理:从外部打开指定页面

深层链接(Deep Link)是移动开发中一个很重要的功能。用户点击一个链接,可以直接打开App的某个特定页面,而不是只能打开首页。Navigation组件对深层链接的支持非常完善,配合DataBinding可以做到无缝集成。

我曾经遇到一个场景:用户收到一条推送消息,点击后需要直接跳转到订单详情页。如果App还没启动,要先启动App再跳转;如果App已经在后台,就直接跳转。用Navigation的深层链接,这个需求实现起来非常优雅。

首先,在导航图中定义深层链接:

<fragment
    android:id="@+id/orderDetailFragment"
    android:name="com.example.app.OrderDetailFragment"
    android:label="订单详情">
    <argument
        android:name="orderId"
        app:argType="string" />
    <deepLink
        app:uri="myapp://order/{orderId}" />
    <deepLink
        app:uri="https://www.example.com/order/{orderId}" />
</fragment>

然后在AndroidManifest.xml中为Activity添加intent-filter:

<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="myapp"
            android:host="order" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="https"
            android:host="www.example.com"
            android:pathPrefix="/order" />
    </intent-filter>
    <!-- 默认的启动intent-filter -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

在OrderDetailFragment中,通过Safe Args获取参数:

class OrderDetailFragment : Fragment() {
    private var _binding: FragmentOrderDetailBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentOrderDetailBinding.inflate(inflater, container, false)

        // 通过Safe Args获取深层链接传递的参数
        val args = OrderDetailFragmentArgs.fromBundle(requireArguments())
        binding.orderId = args.orderId

        // 加载订单数据
        loadOrder(args.orderId)

        return binding.root
    }

    private fun loadOrder(orderId: String) {
        // 模拟加载数据
        binding.orderStatus = "加载中..."
        viewModelScope.launch {
            val order = repository.getOrder(orderId)
            binding.order = order
            binding.orderStatus = "已加载"
        }
    }
}

现在,用户点击链接 myapp://order/ORD123456 或者 https://www.example.com/order/ORD123456,系统会自动打开App并跳转到订单详情页,同时把 orderId 参数传递过去。

测试技巧:在Android模拟器或真机上,可以用adb命令测试深层链接:adb shell am start -W -a android.intent.action.VIEW -d "myapp://order/ORD123456" com.example.app。这样不用真的发链接就能测试。

11.5 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

DataBinding与Navigation知识体系 Navigation + DataBinding Safe Args + DataBinding 类型安全的数据传递,避免Bundle硬编码 Fragment间数据传递 SharedViewModel / SavedStateHandle / Result API NavigationUI + DataBinding BottomNav / Toolbar / DrawerLayout 自动绑定 深层链接处理 Deep Link / URI匹配 / 外部跳转 核心优势 • 类型安全:编译时检查参数类型,减少运行时崩溃 • 代码简洁:减少样板代码,关注业务逻辑 • 自动管理:生命周期感知,避免内存泄漏

这张图把本章的四个核心知识点串联起来了。你可以看到,Safe Args负责「怎么传数据」,Fragment间数据传递解决「数据共享」的问题,NavigationUI处理「UI与导航的绑定」,而深层链接则是「外部入口」的解决方案。四者结合,基本覆盖了日常开发中绝大多数导航场景。

好了,这一章的内容就到这里。代码示例我都亲自跑过,你可以放心使用。如果在实际项目中遇到什么问题,欢迎随时交流。


公众号:蓝海资料掘金营,微信deep3321