Skip to content

SafeArgs 安全传参深度解析

字数统计:约 8000 字
难度等级:⭐⭐⭐⭐
面试重要度:⭐⭐⭐⭐


目录

  1. SafeArgs 插件配置
  2. 类型安全传参
  3. 与 Navigation 集成
  4. 自定义类型支持
  5. 复杂数据类型传递
  6. null 安全处理
  7. 替代传统的 Intent 传参
  8. 面试考点
  9. 参考资料

1. SafeArgs 插件配置

1.1 添加 SafeArgs 插件

在项目的 build.gradle 中配置 SafeArgs 插件:

groovy
// 项目级 build.gradle
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:8.1.0'
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0'
        
        // SafeArgs 插件
        classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.6.0'
    }
}
groovy
// 模块级 build.gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    
    // 应用 SafeArgs 插件(必须在最后)
    id 'androidx.navigation.safeargs.kotlin'
}

android {
    namespace 'com.example.myapp'
    compileSdk 34
    
    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 21
        targetSdk 34
    }
}

dependencies {
    // Navigation 组件
    implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
    implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
    
    // ViewModel
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
}
kotlin
// Kotlin DSL (build.gradle.kts)
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("androidx.navigation.safeargs.kotlin")
}

1.2 配置说明

配置要点:
1. SafeArgs 插件必须在所有其他插件之后应用
2. 使用 'androidx.navigation.safeargs.kotlin' 用于 Kotlin 项目
3. 使用 'androidx.navigation.safeargs' 用于 Java 项目
4. 插件版本需要与 Navigation 版本匹配

支持的语言:
- Kotlin: androidx.navigation.safeargs.kotlin
- Java: androidx.navigation.safeargs
- 都支持:androidx.navigation.safeargs

1.3 生成文件位置

SafeArgs 插件会在编译时生成以下文件:

生成文件结构:
build/
└── generated/
    └── data_binding_binding_gen/
        └── debug/
            └── com.example.myapp/
                └── nav/
                    ├── NavGraphDirections.kt
                    ├── FragmentADirections.kt
                    ├── FragmentBArgs.kt
                    └── ...

或者(使用 customBuildType):
build/
└── generated/
    └── safeargs/
        └── debug/
            └── com.example.myapp/
                └── nav/
                    └── ...

1.4 自定义输出目录

groovy
android {
    // ...
    
    // 自定义 SafeArgs 输出目录
    sourceSets {
        main {
            java.srcDirs += 'build/generated/safeargs/debug'
            java.srcDirs += 'build/generated/safeargs/release'
        }
    }
}

2. 类型安全传参

2.1 定义 Navigation Graph

创建导航图文件 res/navigation/nav_graph.xml

xml
<?xml version="1.0" encoding="utf-8"?>
<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/fragment_home">

    <!-- 定义参数 -->
    <fragment
        android:id="@+id/fragment_home"
        android:name="com.example.myapp.home.HomeFragment"
        android:label="Home">
        
        <!-- 输出参数(Fragment 传递出去的数据) -->
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/fragment_detail" />
            
    </fragment>
    
    <fragment
        android:id="@+id/fragment_detail"
        android:name="com.example.myapp.detail.DetailFragment"
        android:label="Detail">
        
        <!-- 输入参数(Fragment 接收的数据) -->
        <argument
            android:name="userId"
            app:argType="string"
            android:defaultValue="\"\"" />
        
        <argument
            android:name="userName"
            app:argType="string"
            android:defaultValue="\"Unknown\"" />
        
        <argument
            android:name="userAge"
            app:argType="integer"
            android:defaultValue="0" />
        
        <argument
            android:name="userBalance"
            app:argType="float"
            android:defaultValue="0.0" />
        
        <argument
            android:name="isActive"
            app:argType="boolean"
            android:defaultValue="false" />
        
        <argument
            android:name="userTags"
            app:argType="androidx.navigation.NavType"
            app:nullable="true"
            android:defaultValue="null" />
            
    </fragment>
    
</navigation>

2.2 生成的 SafeArgs 类

编译后会自动生成以下类:

kotlin
// 自动生成的类
package com.example.myapp.nav

class DetailFragmentArgs(
    @param:JvmField val userId: String,
    @param:JvmField val userName: String,
    @param:JvmField val userAge: Int,
    @param:JvmField val userBalance: Float,
    @param:JvmField val isActive: Boolean,
    @param:JvmField val userTags: List<String>?
) {
    constructor(bundle: Bundle) : this(
        bundle.getString("userId") ?: "",
        bundle.getString("userName") ?: "Unknown",
        bundle.getInt("userAge", 0),
        bundle.getFloat("userBalance", 0f),
        bundle.getBoolean("isActive", false),
        bundle.getStringArrayList("userTags")
    )
    
    fun toBundle(): Bundle {
        val _bundle = Bundle()
        _bundle.putString("userId", userId)
        _bundle.putString("userName", userName)
        _bundle.putInt("userAge", userAge)
        _bundle.putFloat("userBalance", userBalance)
        _bundle.putBoolean("isActive", isActive)
        _bundle.putStringArrayList("userTags", userTags as ArrayList<String>?)
        return _bundle
    }
}

// 导航方向类
interface FragmentHomeDirections {
    fun actionHomeToDetail(
        userId: String = "",
        userName: String = "Unknown",
        userAge: Int = 0,
        userBalance: Float = 0f,
        isActive: Boolean = false,
        userTags: List<String>? = null
    ): NavDirections
    
    companion object {
        @JvmStatic
        fun createIfNeeded(): FragmentHomeDirections? = null
    }
}

2.3 使用 SafeArgs 传递参数

kotlin
// Fragment 中接收参数
class DetailFragment : Fragment() {
    
    // 使用 by lazy 延迟初始化
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 直接访问参数
        val userId = args.userId
        val userName = args.userName
        val userAge = args.userAge
        val userBalance = args.userBalance
        val isActive = args.isActive
        val userTags = args.userTags
        
        // 显示数据
        textViewUserId.text = userId
        textViewUserName.text = userName
        textViewUserAge.text = userAge.toString()
    }
}

// 导航到目标 Fragment
class HomeFragment : Fragment() {
    
    private val userId = "123"
    private val userName = "张三"
    private val userAge = 25
    private val userBalance = 1000.5f
    private val isActive = true
    private val userTags = listOf("tag1", "tag2")
    
    private fun navigateToDetail() {
        // 方式 1:使用 navController
        val action = HomeFragmentDirections.actionHomeToDetail(
            userId = userId,
            userName = userName,
            userAge = userAge,
            userBalance = userBalance,
            isActive = isActive,
            userTags = userTags
        )
        findNavController().navigate(action)
        
        // 方式 2:使用扩展函数
        navigate<DetailFragment>(
            HomeFragmentDirections.actionHomeToDetail(
                userId = userId,
                userName = userName
            )
        )
        
        // 方式 3:使用 ViewModel 中的导航
        viewModel.navigateToDetail(userId, userName)
    }
}

2.4 基本类型支持

xml
<!-- Navigation Graph 中支持的基本类型 -->
<argument
    android:name="stringArg"
    app:argType="string" />

<argument
    android:name="intArg"
    app:argType="integer" />

<argument
    android:name="longArg"
    app:argType="long" />

<argument
    android:name="floatArg"
    app:argType="float" />

<argument
    android:name="doubleArg"
    app:argType="double" />

<argument
    android:name="booleanArg"
    app:argType="boolean" />

<argument
    android:name="uriArg"
    app:argType="uri" />

<argument
    android:name="colorArg"
    app:argType="color" />

<argument
    android:name="dimenArg"
    app:argType="dimen" />

3. 与 Navigation 集成

3.1 Navigation Host Fragment

xml
<!-- 布局文件中添加 NavHostFragment -->
<androidx.fragment.app.FragmentContainerView
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_graph" />
kotlin
// Activity 中设置
class MainActivity : AppCompatActivity() {
    
    private lateinit var navController: NavController
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 获取 NavController
        navController = findNavController(R.id.nav_host_fragment)
        
        // 或者通过 NavHostFragment 获取
        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController
    }
}

3.2 使用 NavDirections 导航

kotlin
class HomeFragment : Fragment() {
    
    private fun navigateToDetail(userId: String) {
        // 获取导航方向
        val directions = HomeFragmentDirections.actionHomeToDetail(userId)
        
        // 执行导航
        findNavController().navigate(directions)
    }
    
    // 带多个参数
    private fun navigateToDetail(
        userId: String,
        userName: String,
        userAge: Int
    ) {
        val directions = HomeFragmentDirections.actionHomeToDetail(
            userId = userId,
            userName = userName,
            userAge = userAge
        )
        findNavController().navigate(directions)
    }
    
    // 使用 bundle
    private fun navigateWithBundle() {
        val bundle = Bundle().apply {
            putString("userId", "123")
            putString("userName", "张三")
        }
        
        val directions = HomeFragmentDirections.actionHomeToDetail("123")
        findNavController().navigate(directions, bundle)
    }
}

3.3 处理导航返回

kotlin
class DetailFragment : Fragment() {
    
    // 接收参数
    private val args: DetailFragmentArgs by navArgs()
    
    // 返回结果
    private fun navigateBackWithResult() {
        val result = NavDirectionsResult(
            isSuccess = true,
            message = "Success"
        )
        
        // 使用 PopDirections 返回
        val directions = DetailFragmentDirections.popUpToHome()
        findNavController().navigate(directions)
        
        // 或者使用 setResult
        parentFragmentManager.setFragmentResult(
            REQUEST_KEY,
            Bundle().apply {
                putString("result", result.message)
            }
        )
    }
    
    // 接收返回结果
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        parentFragmentManager.setFragmentResultListener(REQUEST_KEY, viewLifecycleOwner) { key, bundle ->
            val result = bundle.getString("result")
            // 处理结果
        }
    }
}

3.4 导航动画

kotlin
// 自定义导航动画
class HomeFragment : Fragment() {
    
    private fun navigateToDetailWithAnimation(userId: String) {
        val directions = HomeFragmentDirections.actionHomeToDetail(userId)
        
        // 设置动画
        findNavController().navigate(
            directions,
            navController.navOptions {
                anim {
                    enter = R.anim.slide_in_right
                    exit = R.anim.slide_out_left
                    popEnter = R.anim.slide_in_left
                    popExit = R.anim.slide_out_right
                }
            }
        )
    }
}

// res/anim/slide_in_right.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromXDelta="100%"
        android:toXDelta="0%"
        android:duration="300" />
</set>

// res/anim/slide_out_left.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromXDelta="0%"
        android:toXDelta="-100%"
        android:duration="300" />
</set>

4. 自定义类型支持

4.1 定义自定义类型转换器

kotlin
// 定义数据类
data class User(
    val id: String,
    val name: String,
    val email: String
)

// 自定义类型转换器
object CustomTypeConverter {
    
    // 序列化为 String
    fun userToString(user: User): String {
        return "${user.id}|${user.name}|${user.email}"
    }
    
    // 从 String 反序列化
    fun stringToUser(str: String): User? {
        val parts = str.split("|")
        return if (parts.size == 3) {
            User(parts[0], parts[1], parts[2])
        } else {
            null
        }
    }
}

4.2 使用 Parcelize 注解

kotlin
// 使用 AndroidX Parcelize
@Parcelize
data class User(
    val id: String,
    val name: String,
    val email: String
) : Parcelable

// 在 Navigation Graph 中定义
<argument
    android:name="user"
    app:argType="com.example.myapp.User" />

// 生成的代码会自动处理 Parcelable
class DetailFragmentArgs(bundle: Bundle) {
    val user: User = bundle.getParcelable("user")!!
}

4.3 使用自定义转换器

kotlin
// 定义转换器
class UserConverter {
    companion object {
        fun toBundle(user: User): Bundle {
            return Bundle().apply {
                putString("userId", user.id)
                putString("userName", user.name)
                putString("userEmail", user.email)
            }
        }
        
        fun fromBundle(bundle: Bundle): User {
            return User(
                id = bundle.getString("userId") ?: "",
                name = bundle.getString("userName") ?: "",
                email = bundle.getString("userEmail") ?: ""
            )
        }
    }
}

// 在 Fragment 中使用
class DetailFragment : Fragment() {
    
    private val args: DetailFragmentArgs by navArgs()
    
    // 使用转换器获取 User
    private val user: User by lazy {
        UserConverter.fromBundle(args.userBundle)
    }
}

4.4 使用 Kotlinx Serialization

kotlin
// 添加依赖
dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
}

// 定义序列化数据类
@Serializable
data class User(
    val id: String,
    val name: String,
    val email: String
)

// 序列化
fun userToString(user: User): String {
    return Json.encodeToString(User.serializer(), user)
}

// 反序列化
fun stringToUser(str: String): User? {
    return try {
        Json.decodeFromString(User.serializer(), str)
    } catch (e: Exception) {
        null
    }
}

// 在 Navigation 中使用
<argument
    android:name="user"
    app:argType="string" />

// 传递序列化后的字符串
val directions = HomeFragmentDirections.actionHomeToDetail(
    userString = userToString(user)
)

// 接收时反序列化
val user: User = stringToUser(args.userString)!!

4.5 复杂对象传递

kotlin
// 使用 UUID 作为标识符
class HomeFragment : Fragment() {
    
    private val userViewModel: UserViewModel by activityViewModels()
    
    private fun navigateToDetail(user: User) {
        // 将 User 存入 ViewModel
        userViewModel.setSelectedUser(user)
        
        // 只传递 ID
        val directions = HomeFragmentDirections.actionHomeToDetail(
            userId = user.id
        )
        findNavController().navigate(directions)
    }
}

// DetailFragment 从 ViewModel 获取
class DetailFragment : Fragment() {
    
    private val args: DetailFragmentArgs by navArgs()
    private val userViewModel: UserViewModel by activityViewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 从 ViewModel 获取 User
        val user = userViewModel.getSelectedUser(args.userId)
        
        // 或者使用 LiveData/Flow
        userViewModel.userFlow.collect { user ->
            // 更新 UI
        }
    }
}

5. 复杂数据类型传递

5.1 传递 List

xml
<!-- Navigation Graph -->
<argument
    android:name="userList"
    app:argType="androidx.navigation.NavType"
    app:nullable="true"
    android:defaultValue="null" />
kotlin
// 传递 List
class HomeFragment : Fragment() {
    
    private val users = listOf(
        User("1", "张三", "zhangsan@example.com"),
        User("2", "李四", "lisi@example.com")
    )
    
    private fun navigateToUserList() {
        // 方式 1:传递 List 的字符串表示
        val userStrings = users.joinToString(",") { "${it.id}:${it.name}" }
        val directions = HomeFragmentDirections.actionHomeToUserList(
            userStrings = userStrings
        )
        findNavController().navigate(directions)
        
        // 方式 2:使用 ArrayList(推荐)
        val userArrayList = ArrayList(users)
        val directions2 = HomeFragmentDirections.actionHomeToUserList(
            userList = userArrayList
        )
        findNavController().navigate(directions2)
        
        // 方式 3:使用 ViewModel 传递
        userListViewModel.setUsers(users)
        findNavController().navigate(
            HomeFragmentDirections.actionHomeToUserList()
        )
    }
}

// 接收 List
class UserListFragment : Fragment() {
    
    private val args: UserListFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 方式 1:解析字符串
        val users = args.userStrings.split(",")
            .map { parts ->
                val (id, name) = parts.split(":")
                User(id, name, "")
            }
        
        // 方式 2:直接获取 List
        val users = args.userList
        
        // 方式 3:从 ViewModel 获取
        val users = userListViewModel.users.value ?: emptyList()
    }
}

5.2 传递 Map

kotlin
// 传递 Map
class HomeFragment : Fragment() {
    
    private val dataMap = mapOf(
        "key1" to "value1",
        "key2" to "value2",
        "key3" to "value3"
    )
    
    private fun navigateWithMap() {
        // 方式 1:转换为 Bundle
        val bundle = Bundle().apply {
            dataMap.forEach { (key, value) ->
                putString(key, value)
            }
        }
        
        val directions = HomeFragmentDirections.actionHomeToMapFragment()
        findNavController().navigate(directions, bundle)
        
        // 方式 2:使用 JSON 序列化
        val jsonString = Json.encodeToString(dataMap)
        val directions2 = HomeFragmentDirections.actionHomeToMapFragment(
            dataJson = jsonString
        )
        findNavController().navigate(directions2)
    }
}

// 接收 Map
class MapFragment : Fragment() {
    
    private val args: MapFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 方式 1:从 Bundle 获取
        val map = args.bundle.toMap()
        
        // 方式 2:从 JSON 解析
        val map = Json.decodeFromString<Map<String, String>>(args.dataJson)
    }
}

5.3 传递大对象

kotlin
// ❌ 不推荐:直接传递大对象
class BigObject(
    val data: ByteArray = ByteArray(10 * 1024 * 1024) // 10MB
)

// ✅ 推荐:传递引用或 ID
class HomeFragment : Fragment() {
    
    private val bigObject = BigObject()
    
    private fun navigateWithBigObject() {
        // 方式 1:存入 ViewModel
        dataViewModel.setBigObject(bigObject)
        findNavController().navigate(
            HomeFragmentDirections.actionHomeToBigObjectFragment()
        )
        
        // 方式 2:使用文件路径
        val filePath = saveObjectToFile(bigObject)
        findNavController().navigate(
            HomeFragmentDirections.actionHomeToBigObjectFragment(
                filePath = filePath
            )
        )
        
        // 方式 3:使用数据 ID
        val dataId = database.insertBigObject(bigObject)
        findNavController().navigate(
            HomeFragmentDirections.actionHomeToBigObjectFragment(
                dataId = dataId
            )
        )
    }
    
    private fun saveObjectToFile(obj: BigObject): String {
        val file = File(cacheDir, "big_object.tmp")
        FileOutputStream(file).use { it.write(obj.data) }
        return file.absolutePath
    }
}

5.4 传递图片

kotlin
// 传递图片
class HomeFragment : Fragment() {
    
    private val bitmap: Bitmap = ...
    
    private fun navigateWithImage() {
        // 方式 1:使用 URI
        val uri = saveImageToContentProvider(bitmap)
        findNavController().navigate(
            HomeFragmentDirections.actionHomeToImageFragment(
                imageUri = uri.toString()
            )
        )
        
        // 方式 2:使用文件路径
        val filePath = saveImageToFile(bitmap)
        findNavController().navigate(
            HomeFragmentDirections.actionHomeToImageFragment(
                imagePath = filePath
            )
        )
        
        // 方式 3:使用图片 ID
        val imageId = database.insertImage(bitmap)
        findNavController().navigate(
            HomeFragmentDirections.actionHomeToImageFragment(
                imageId = imageId
            )
        )
    }
}

6. null 安全处理

6.1 定义可空参数

xml
<!-- Navigation Graph -->
<!-- 可空参数 -->
<argument
    android:name="optionalString"
    app:argType="string"
    app:nullable="true"
    android:defaultValue="null" />

<!-- 非空参数(有默认值) -->
<argument
    android:name="requiredString"
    app:argType="string"
    android:defaultValue="\"default\"" />

<!-- 非空参数(无默认值,必须提供) -->
<argument
    android:name="mandatoryString"
    app:argType="string" />

6.2 处理可空参数

kotlin
// 接收可空参数
class DetailFragment : Fragment() {
    
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 处理可空参数
        val optional = args.optionalString  // String?
        val required = args.requiredString  // String
        val mandatory = args.mandatoryString  // String
        
        // 安全使用
        optional?.let {
            // 处理非 null 情况
        } ?: run {
            // 处理 null 情况
        }
        
        // 使用 Elvis 操作符
        val displayName = optional ?: "Unknown"
    }
}

6.3 验证必需参数

kotlin
// 在 Fragment 中验证必需参数
class DetailFragment : Fragment() {
    
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 验证必需参数
        require(!args.mandatoryString.isNullOrEmpty()) {
            "mandatoryString cannot be null or empty"
        }
        
        // 或者使用 assert
        assert(args.mandatoryString.isNotEmpty()) {
            "mandatoryString must not be empty"
        }
    }
}

6.4 提供默认值

xml
<!-- Navigation Graph 中提供默认值 -->
<argument
    android:name="stringArg"
    app:argType="string"
    android:defaultValue="\"default\"" />

<argument
    android:name="intArg"
    app:argType="integer"
    android:defaultValue="0" />

<argument
    android:name="booleanArg"
    app:argType="boolean"
    android:defaultValue="false" />

<argument
    android:name="floatArg"
    app:argType="float"
    android:defaultValue="0.0f" />
kotlin
// 在代码中使用默认值
class HomeFragment : Fragment() {
    
    private fun navigateWithDefaults() {
        // 不传递参数,使用默认值
        val directions = HomeFragmentDirections.actionHomeToDetail(
            // 不传参数,使用默认值
        )
        findNavController().navigate(directions)
        
        // 只传递部分参数
        val directions2 = HomeFragmentDirections.actionHomeToDetail(
            mandatoryString = "value",
            // 其他参数使用默认值
        )
        findNavController().navigate(directions2)
    }
}

6.5 处理 null 安全

kotlin
// 使用 Safe Args 的空安全特性
class DetailFragment : Fragment() {
    
    private val args: DetailFragmentArgs by navArgs()
    
    // 方式 1:使用 let
    args.optionalString?.let { optional ->
        // 使用 optional
    }
    
    // 方式 2:使用 run
    val result = args.optionalString?.run {
        // 使用 this
        length
    } ?: 0
    
    // 方式 3:使用 takeIf
    val validString = args.optionalString?.takeIf { it.isNotEmpty() }
    
    // 方式 4:使用 takeUnless
    val nonEmptyString = args.optionalString?.takeUnless { it.isEmpty() }
    
    // 方式 5:使用 orEmpty
    val safeString = args.optionalString.orEmpty()
    
    // 方式 6:使用 orNull
    val nonNull = args.optionalString?.ifEmpty { null }
}

7. 替代传统的 Intent 传参

7.1 传统 Intent 传参方式

kotlin
// ❌ 传统 Intent 传参(不类型安全)
class HomeActivity : AppCompatActivity() {
    
    private fun navigateToDetail() {
        val intent = Intent(this, DetailActivity::class.java).apply {
            putExtra("userId", "123")
            putExtra("userName", "张三")
            putExtra("userAge", 25)
        }
        startActivity(intent)
    }
}

class DetailActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 容易出错:参数名拼写错误、类型不匹配
        val userId = intent.getStringExtra("userId")  // 可能为 null
        val userName = intent.getStringExtra("userName")  // 可能为 null
        val userAge = intent.getIntExtra("userAge", 0)  // 默认值可能掩盖错误
    }
}

7.2 SafeArgs 类型安全方式

kotlin
// ✅ SafeArgs 传参(类型安全)
// Navigation Graph
<fragment
    android:id="@+id/fragment_detail"
    android:name="com.example.myapp.detail.DetailFragment">
    <argument
        android:name="userId"
        app:argType="string" />
    <argument
        android:name="userName"
        app:argType="string" />
    <argument
        android:name="userAge"
        app:argType="integer" />
</fragment>

// 生成的代码
class DetailFragmentArgs(
    val userId: String,  // 非空
    val userName: String,  // 非空
    val userAge: Int  // 非空
)

// 使用
class HomeFragment : Fragment() {
    
    private fun navigateToDetail() {
        val directions = HomeFragmentDirections.actionHomeToDetail(
            userId = "123",
            userName = "张三",
            userAge = 25
        )
        findNavController().navigate(directions)
    }
}

class DetailFragment : Fragment() {
    
    private val args: DetailFragmentArgs by navArgs()
    
    // 直接访问,类型安全
    val userId = args.userId
    val userName = args.userName
    val userAge = args.userAge
}

7.3 对比总结

特性IntentSafeArgs
类型安全❌ 否✅ 是
编译时检查❌ 否✅ 是
参数名拼写检查❌ 运行时✅ 编译时
默认值支持✅ 是✅ 是
可空参数处理❌ 需要手动✅ 自动
重构支持❌ 困难✅ 容易
IDE 自动完成❌ 有限✅ 完整

7.4 迁移指南

kotlin
// 迁移步骤:
// 1. 在 Navigation Graph 中定义参数
// 2. 使用 SafeArgs 替换 Intent
// 3. 更新 Fragment 接收参数
// 4. 测试所有导航路径

// 从 Activity 迁移到 Fragment
// Before:
startActivity(Intent(this, DetailActivity::class.java).apply {
    putExtra("userId", userId)
})

// After:
findNavController().navigate(
    HomeFragmentDirections.actionHomeToDetail(userId)
)

7.5 处理 Activity 与 Fragment 混合

kotlin
// 如果项目中有 Activity 和 Fragment 混合,可以使用 FragmentResult
// Activity 中
class HomeActivity : AppCompatActivity() {
    
    private fun navigateToFragment() {
        val fragment = DetailFragment()
        val bundle = Bundle().apply {
            putString("userId", "123")
        }
        fragment.arguments = bundle
        supportFragmentManager.beginTransaction()
            .replace(R.id.container, fragment)
            .commit()
    }
}

// Fragment 中接收
class DetailFragment : Fragment() {
    
    private val userId: String? by lazy {
        arguments?.getString("userId")
    }
}

8. 面试考点

基础考点

1. SafeArgs 是什么?有什么作用?

答案要点:
- SafeArgs 是 Navigation 组件提供的类型安全的参数传递工具
- 编译时生成参数类,确保参数类型和名称正确
- 避免传统的 Intent 传参的类型错误和拼写错误
- 提供更好的 IDE 支持和重构能力

2. 如何配置 SafeArgs 插件?

groovy
// 项目级
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.6.0'

// 模块级
id 'androidx.navigation.safeargs.kotlin'

// 注意事项:
// - 插件必须在最后应用
// - Kotlin 项目使用 .kotlin 版本

3. SafeArgs 支持哪些数据类型?

基本类型:
- string
- integer / int
- long
- float
- double
- boolean
- uri
- color
- dimen

自定义类型:
- Parcelable
- Serializable
- 自定义转换器

集合类型:
- List
- Map(需要转换)

进阶考点

4. 如何在 SafeArgs 中传递自定义类型?

方式 1:使用 @Parcelize 注解
@Parcelize
data class User(...) : Parcelable

方式 2:使用转换器序列化
fun userToString(user: User): String

方式 3:使用 ViewModel 传递
viewModel.setObject(user)

5. SafeArgs 如何处理可空参数?

xml
<argument
    android:name="optional"
    app:argType="string"
    app:nullable="true"
    android:defaultValue="null" />
kotlin
// 接收可空参数
val optional: String? = args.optional

// 提供默认值
val withDefault = args.optional ?: "default"

6. 如何在 ViewModel 中使用 SafeArgs 导航?

kotlin
class HomeViewModel : ViewModel() {
    
    private val navController: NavController? = null
    
    fun navigateToDetail(userId: String) {
        val directions = HomeFragmentDirections.actionHomeToDetail(userId)
        navController?.navigate(directions)
    }
}

高级考点

7. SafeArgs 生成的代码原理是什么?

答案要点:
1. 插件读取 Navigation Graph XML
2. 根据 argument 定义生成 Args 类
3. 根据 action 定义生成 Directions 类
4. 使用 KSP 或注解处理器生成 Kotlin 代码
5. 生成的代码在编译时加入项目

8. SafeArgs 如何处理配置变化?

答案要点:
1. SafeArgs 使用 Bundle 存储参数
2. 配置变化时,参数会自动保存在 Fragment 的 arguments
3. 使用 by navArgs() 会自动从 arguments 获取
4. 与 ViewModel 配合可以保持数据

9. SafeArgs 的性能开销?

答案要点:
1. 编译时生成代码,运行时开销小
2. 使用 Bundle 传递,与传统方式类似
3. 自定义类型需要序列化,可能有性能开销
4. 大对象建议通过 ViewModel 或数据库传递

10. SafeArgs 的最佳实践?

1. 优先使用基本类型
2. 大对象使用 ViewModel 或数据库
3. 自定义类型使用 Parcelable
4. 提供合理的默认值
5. 使用可空参数处理可选值
6. 避免传递敏感数据
7. 使用导航图管理导航逻辑

9. 参考资料

官方文档

推荐资源

相关工具


本文完,感谢阅读!