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
}
}
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的标题更新、以及返回按钮的显示。你不需要写任何监听器。
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 参数传递过去。
adb shell am start -W -a android.intent.action.VIEW -d "myapp://order/ORD123456" com.example.app。这样不用真的发链接就能测试。
11.5 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把本章的四个核心知识点串联起来了。你可以看到,Safe Args负责「怎么传数据」,Fragment间数据传递解决「数据共享」的问题,NavigationUI处理「UI与导航的绑定」,而深层链接则是「外部入口」的解决方案。四者结合,基本覆盖了日常开发中绝大多数导航场景。
好了,这一章的内容就到这里。代码示例我都亲自跑过,你可以放心使用。如果在实际项目中遇到什么问题,欢迎随时交流。
公众号:蓝海资料掘金营,微信deep3321