16、导航与数据绑定:在布局中使用Navigation变量、通过数据绑定处理点击事件、简化Fragment之间的数据传递。

好,咱们今天聊点实战中特别爽的东西——Navigation 组件 + DataBinding

说实话,我早期做 Android 开发时,Fragment 之间的数据传递简直是个噩梦。Bundle、setArguments、接口回调……写多了真的想吐。后来 Navigation 组件出来,配合 DataBinding,整个流程变得清爽多了。

这一章,我就带你看看怎么在布局里直接用 Navigation 变量,怎么用数据绑定处理点击事件,以及如何简化 Fragment 之间的数据传递。

16.1 在布局中使用 Navigation 变量

先问一个问题:你平时在 Fragment 里怎么获取 NavController?是不是这样:

val navController = findNavController()

嗯,这没问题。但如果你用了 DataBinding,其实可以在 XML 里直接拿到 NavController 的引用。我个人习惯在布局中声明一个 navController 变量,然后直接在点击事件里调用它的 navigate 方法。

来看一个例子:

<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>

    <Button
        android:id="@+id/btnNext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="下一步"
        android:onClick="@{() -> navController.navigate(R.id.action_to_detail)}" />
</layout>

然后在 Fragment 里绑定:

binding.navController = findNavController()

搞定。这样你的 XML 就拥有了导航能力,不需要在 Fragment 里写一堆点击监听了。

小提示: 如果你在多个 Fragment 里都用到了同样的导航逻辑,可以考虑把 NavController 放到 BaseFragment 的 binding 里统一处理,减少重复代码。

16.2 通过数据绑定处理点击事件

刚才的例子其实已经展示了点击事件的处理。但我想多说一句:不要把业务逻辑写在 XML 里

你可能会看到有人这样写:

android:onClick="@{() -> viewModel.loadData()}"

嗯,这样写虽然能跑,但我不推荐。为什么呢?因为 XML 里不应该知道 ViewModel 的存在。它只应该知道「点击了按钮」,至于点击后做什么,应该交给 Fragment 或 Activity 去处理。

我建议的做法是:

<data>
    <variable
        name="listener"
        type="com.example.app.OnItemClickListener" />
</data>

<Button
    android:onClick="@{() -> listener.onNextClick()}" />

然后在 Fragment 里实现这个接口:

binding.listener = object : OnItemClickListener {
    override fun onNextClick() {
        findNavController().navigate(R.id.action_to_detail)
    }
}

这样职责就清晰了:XML 只负责绑定,Fragment 负责导航逻辑。

注意: 我曾经见过有人直接在 XML 里写 @{() -> viewModel.navigate()},结果 ViewModel 里持有 NavController,导致内存泄漏。切记,NavController 是 UI 层的概念,不要放到 ViewModel 里。

16.3 简化 Fragment 之间的数据传递

这是本章的重头戏。以前 Fragment 之间传数据,要么用 Bundle,要么用接口回调,要么用 EventBus。现在有了 Navigation + DataBinding,我们可以用 Safe Args 或者 Shared ViewModel 来搞定。

16.3.1 使用 Safe Args 传递简单数据

Safe Args 是 Navigation 的官方插件,可以生成类型安全的参数类。用法很简单:

第一步,在 nav_graph.xml 中定义参数:

<fragment
    android:id="@+id/detailFragment"
    android:name="com.example.app.DetailFragment">
    <argument
        android:name="itemId"
        app:argType="integer"
        android:defaultValue="0" />
</fragment>

第二步,在源 Fragment 中导航时传递参数:

val action = ListFragmentDirections.actionToListToDetail(itemId = 42)
findNavController().navigate(action)

第三步,在目标 Fragment 中接收:

val args: DetailFragmentArgs by navArgs()
val itemId = args.itemId

这样传数据,类型安全,不会出现 Bundle 的 key 拼写错误问题。我在项目中用过很多次,确实省心。

16.3.2 使用 Shared ViewModel 传递复杂数据

如果数据比较复杂,比如一个用户对象、一个列表,或者需要多个 Fragment 共享状态,我建议用 Shared ViewModel。

做法很简单:

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

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

然后在两个 Fragment 中获取同一个 ViewModel 实例:

// 在 Fragment A 中
val sharedViewModel: SharedViewModel by activityViewModels()
sharedViewModel.selectItem(item)

// 在 Fragment B 中
val sharedViewModel: SharedViewModel by activityViewModels()
sharedViewModel.selectedItem.observe(viewLifecycleOwner) { item ->
    // 更新 UI
}

注意这里用的是 activityViewModels(),而不是 viewModels()。因为前者会返回 Activity 作用域的 ViewModel,这样两个 Fragment 才能共享同一个实例。

核心要点:
  • 简单数据用 Safe Args,类型安全,代码清晰
  • 复杂数据用 Shared ViewModel,避免序列化开销
  • 不要用 EventBus 或 LiveData 在 Fragment 之间传数据,容易造成内存泄漏

16.4 实战:一个完整的例子

咱们来写一个完整的流程。假设有一个列表页和一个详情页,点击列表项跳转到详情页,并传递数据。

第一步:定义导航图

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

    <fragment
        android:id="@+id/listFragment"
        android:name="com.example.app.ListFragment">
        <action
            android:id="@+id/action_list_to_detail"
            app:destination="@id/detailFragment" />
    </fragment>

    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.app.DetailFragment">
        <argument
            android:name="itemId"
            app:argType="integer" />
    </fragment>
</navigation>

第二步:在列表 Fragment 中处理点击

class ListFragment : Fragment() {
    private lateinit var binding: FragmentListBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentListBinding.inflate(inflater, container, false)
        binding.navController = findNavController()
        return binding.root
    }
}

第三步:在布局中使用 DataBinding 处理点击

<layout>
    <data>
        <variable name="navController" type="androidx.navigation.NavController" />
        <variable name="item" type="com.example.app.Item" />
    </data>

    <LinearLayout>
        <TextView
            android:text="@{item.name}"
            android:onClick="@{() -> navController.navigate(
                ListFragmentDirections.actionListToDetail(item.id)
            )}" />
    </LinearLayout>
</layout>

第四步:在详情 Fragment 中接收参数

class DetailFragment : Fragment() {
    private val args: DetailFragmentArgs by navArgs()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val itemId = args.itemId
        // 根据 itemId 加载数据
    }
}

你看,整个流程下来,代码量少了很多,而且逻辑都在 XML 和 Fragment 中清晰可见。

16.5 本章小结

这一章我们聊了三个核心点:

  • 在布局中使用 Navigation 变量:直接在 XML 里拿到 NavController,减少 Fragment 中的样板代码
  • 通过数据绑定处理点击事件:用接口或 Lambda 表达式,保持职责清晰
  • 简化 Fragment 之间的数据传递:简单数据用 Safe Args,复杂数据用 Shared ViewModel

我个人觉得,Navigation + DataBinding 的组合,是 Android 开发中少有的「用了就回不去」的技术。它让代码更简洁,逻辑更清晰,也更容易维护。

嗯,下一章咱们聊聊更进阶的话题——如何用 Navigation 处理深层链接和动态导航。不过那是后话了,先把今天的内容消化掉。


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