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负责传递重量级数据

举个例子,从列表页跳转到详情页:

  1. Safe Args传递一个ID(比如商品ID)
  2. 详情页通过ViewModel,根据ID从网络或数据库加载数据
  3. 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 数据流全景图

为了让你更直观地理解整个数据传递流程,我画了一张图:

Navigation + LiveData 数据传递流程 HomeFragment 点击商品 获取 productId Safe Args productId DetailViewModel SavedStateHandle 获取 productId 调用 repository LiveData Product 对象 DetailFragment 观察数据 更新UI 关键步骤说明: 1. HomeFragment 通过 Safe Args 将 productId 传递给 DetailFragment 2. DetailViewModel 通过 SavedStateHandle 获取 productId 3. ViewModel 调用 Repository 加载商品数据 4. 数据通过 LiveData 暴露给 DetailFragment 5. DetailFragment 观察 LiveData,更新 UI 注:整个过程是单向数据流,数据从 ViewModel 流向 UI,UI 不直接修改数据

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