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 里写一堆点击监听了。
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 负责导航逻辑。
@{() -> 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