19. LiveData与Navigation组件:Navigation传参、LiveData与Safe Args结合、实战:详情页数据传递
各位同学,今天我们来聊一个非常实际的话题——Navigation组件中的数据传递。
说实话,我在早期做Android开发时,页面间传参一直是个让人头疼的问题。用Bundle吧,容易写错key;用Intent extra吧,类型安全又没保障。直到Navigation组件配合Safe Args出现,才算是真正解决了这个痛点。
这一章,我会带你从最基础的Navigation传参讲起,再到LiveData与Safe Args的结合使用,最后用一个完整的详情页数据传递实战来收尾。嗯,内容不少,但都是干货。
19.1 Navigation传参的几种方式
Navigation组件支持多种传参方式,我个人最常用的是以下三种:
- Bundle方式:最传统,通过Bundle传递数据
- Safe Args方式:类型安全,编译时检查
- ViewModel共享方式:适合复杂数据传递
咱们先看Bundle方式,虽然它不够优雅,但理解它有助于你理解Safe Args的原理。
// 发送方
val bundle = Bundle().apply {
putString("userId", "12345")
putInt("age", 28)
}
findNavController().navigate(R.id.action_home_to_detail, bundle)
// 接收方
val userId = arguments?.getString("userId") ?: ""
val age = arguments?.getInt("age") ?: 0
这种方式的问题很明显——key是字符串,写错了编译器不会报错。我在项目中就吃过这个亏,有一次把"userId"写成了"userID",结果线上崩溃了。从那以后,我就坚决推荐使用Safe Args。
19.2 Safe Args:类型安全的传参方案
Safe Args是Navigation官方推荐的传参方式。它会在编译时生成对应的类,保证类型安全。
首先,你需要在项目的build.gradle中添加Safe Args插件:
// 根目录build.gradle
buildscript {
repositories {
google()
}
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.7.7"
}
}
// app模块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="userId"
app:argType="string"
android:defaultValue="" />
<argument
android:name="age"
app:argType="integer"
android:defaultValue="0" />
</fragment>
编译后,Safe Args会生成一个DetailFragmentArgs类。使用起来非常方便:
// 发送方
val action = HomeFragmentDirections.actionHomeToDetail("12345", 28)
findNavController().navigate(action)
// 接收方
val args: DetailFragmentArgs by navArgs()
val userId = args.userId
val age = args.age
核心优势:编译时检查参数类型和名称,运行时不会因为拼写错误而崩溃。
19.3 LiveData与Safe Args的结合
光有Safe Args还不够,因为很多时候我们需要在页面间传递动态数据——比如从网络请求回来的数据。这时候,LiveData就派上用场了。
我建议的做法是:Safe Args负责传递轻量级标识符,LiveData负责传递重量级数据。
举个例子,从列表页跳转到详情页:
- Safe Args传递一个ID(比如商品ID)
- 详情页通过ViewModel,根据ID从网络或数据库加载数据
- ViewModel将数据暴露为LiveData,供UI层观察
// DetailViewModel.kt
class DetailViewModel(
private val savedStateHandle: SavedStateHandle,
private val repository: ProductRepository
) : ViewModel() {
private val productId: String = savedStateHandle.get<String>("productId") ?: ""
private val _product = MutableLiveData<Product?>()
val product: LiveData<Product?> get() = _product
init {
loadProduct()
}
private fun loadProduct() {
viewModelScope.launch {
val result = repository.getProductById(productId)
_product.value = result
}
}
}
这里有个关键点——SavedStateHandle。它是Navigation与ViewModel之间的桥梁,能自动保存和恢复数据。即使进程被杀死,数据也不会丢失。
个人经验:SavedStateHandle不仅适用于Navigation传参,还适用于屏幕旋转等配置变更场景。我一般在ViewModel中都会用它来保存关键状态。
19.4 实战:详情页数据传递
好了,理论讲完了,咱们来一个完整的实战案例。假设我们有一个商品列表页和一个商品详情页。
第一步:定义Navigation参数
<navigation 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:id="@+id/nav_graph"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.app.HomeFragment"
android:label="首页">
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment" />
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.example.app.DetailFragment"
android:label="详情页">
<argument
android:name="productId"
app:argType="string"
android:defaultValue="" />
</fragment>
</navigation>
第二步:列表页跳转
// HomeFragment.kt
class HomeFragment : Fragment() {
private val viewModel: HomeViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 假设有一个RecyclerView,点击商品跳转
adapter.setOnItemClickListener { product ->
val action = HomeFragmentDirections.actionHomeToDetail(product.id)
findNavController().navigate(action)
}
}
}
第三步:详情页接收参数并加载数据
// DetailFragment.kt
class DetailFragment : Fragment() {
private val args: DetailFragmentArgs by navArgs()
private val viewModel: DetailViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 观察商品数据
viewModel.product.observe(viewLifecycleOwner) { product ->
// 更新UI
binding.productName.text = product?.name
binding.productPrice.text = "¥${product?.price}"
binding.productDesc.text = product?.description
}
}
}
// DetailViewModel.kt
class DetailViewModel(
private val savedStateHandle: SavedStateHandle,
private val repository: ProductRepository
) : ViewModel() {
val productId: String = savedStateHandle.get<String>("productId") ?: ""
private val _product = MutableLiveData<Product?>()
val product: LiveData<Product?> get() = _product
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> get() = _isLoading
init {
loadProduct()
}
private fun loadProduct() {
_isLoading.value = true
viewModelScope.launch {
try {
val result = repository.getProductById(productId)
_product.value = result
} catch (e: Exception) {
// 处理错误
_product.value = null
} finally {
_isLoading.value = false
}
}
}
}
避坑指南:我曾经在项目中犯过一个错误——直接在Fragment的onCreateView中调用viewModel.loadProduct(),结果每次屏幕旋转都会重新加载数据。正确的做法是在ViewModel的init块中加载,或者使用viewModelScope.launch配合lifecycle.repeatOnLifecycle来控制加载时机。
19.5 数据流全景图
为了让你更直观地理解整个数据传递流程,我画了一张图:
19.6 进阶:传递复杂对象
有时候我们需要传递复杂对象,比如一个完整的User对象。Safe Args默认支持基本类型,对于复杂对象,有几种方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 序列化(Serializable) | 实现简单 | 性能较差,反射开销 | 简单对象,数据量小 |
| Parcelable | 性能好,Android原生 | 需要手写模板代码 | 大多数场景 |
| JSON字符串 | 通用性强 | 需要序列化/反序列化 | 跨模块传递 |
| 只传ID,ViewModel加载 | 最推荐,数据最新 | 需要额外网络请求 | 需要最新数据的场景 |
我个人强烈推荐只传ID,ViewModel加载的方案。为什么呢?因为数据是会变的。你传一个User对象过去,如果用户在详情页修改了信息,你传的那个旧对象就没用了。不如只传ID,让ViewModel每次都从数据源获取最新数据。
小技巧:如果你确实需要传递复杂对象,可以用Parcelable + Safe Args。Safe Args支持自定义Parcelable类型,只需要在XML中指定app:argType="com.example.User"即可。
19.7 总结
这一章我们聊了Navigation组件中的数据传递,核心要点就三个:
- Safe Args:类型安全,编译时检查,替代传统的Bundle方式
- LiveData + ViewModel:负责加载和暴露数据,与Navigation无缝配合
- 只传ID:避免传递复杂对象,保证数据始终是最新的
嗯,内容就这么多。下一章我们会继续深入,聊聊Navigation的深层链接和动态导航。不过那是后话了,先把今天的内容消化掉。
如果你在实际项目中遇到什么问题,欢迎随时交流。毕竟,这些坑我都踩过,能帮你少走弯路也是好的。
公众号:蓝海资料掘金营,微信deep3321