4. 自定义 SavedStateProvider:创建与注册

SavedStateHandle 默认支持 基本类型、Bundle 可序列化对象 以及 Parcelable 数据。当需要持久化复杂业务对象(如嵌套的领域模型、大列表或自定义序列化格式)时,默认机制可能不够灵活或效率低下。本章讲解如何通过 自定义 SavedStateProvider 接管特定键的序列化逻辑,实现细粒度的数据持久化控制。

4.1 原理:Provider 注册链

每个 SavedStateHandle 内部维护一个 SavedStateProvider 列表(即 components)。当系统调用 saveState() 时,按注册顺序依次调用每个 Provider,将返回的 Bundle 合并到最终的保存状态中。自定义 Provider 只需实现 SavedStateRegistry.SavedStateProvider 接口:

public interface SavedStateProvider {
    @NonNull
    Bundle saveState();
}

核心流程:

  • 通过 SavedStateRegistryController 获取 SavedStateRegistry。
  • 调用 registry.registerSavedStateProvider(key, provider) 将自定义 Provider 绑定到指定键。
  • 保存时,系统遍历所有注册的 Provider,将返回的 Bundle 放入根状态 Bundle 中。
  • 恢复时,系统根据键从根 Bundle 中取出子 Bundle,交给对应的 ViewModel 或组件处理。

4.2 实现流程(四步法)

  1. 定义序列化/反序列化逻辑:实现 SavedStateProvider,在 saveState() 中把复杂对象转为 Bundle。
  2. 注册 Provider:在 ViewModel 构造器或 init 块中通过 SavedStateHandle 获取 SavedStateRegistry 并注册。
  3. 注入到 ViewModel:将自定义 Provider 返回的数据通过 LiveData 或 StateFlow 暴露给 UI。
  4. 测试与验证:模拟进程杀死/重建,确认数据完整恢复。

4.3 代码示例:持久化购物车列表

假设购物车项为 CartItem(包含 productId、name、quantity、price),我们需要持久化整个 List<CartItem>。默认 SavedStateHandle 不支持直接存 List,因此自定义 Provider:

// 1. 自定义 Provider
class CartProvider(private val cartList: MutableList<CartItem>) : SavedStateProvider {
    override fun saveState(): Bundle {
        val bundle = Bundle()
        val json = Gson().toJson(cartList)  // 使用 Gson 序列化
        bundle.putString("cart_json", json)
        return bundle
    }

    fun restoreFromBundle(bundle: Bundle?) {
        val json = bundle?.getString("cart_json") ?: return
        val items = Gson().fromJson(json, Array<CartItem>::class.java)
        cartList.clear()
        items?.forEach { cartList.add(it) }
    }
}

// 2. ViewModel 中使用
class CartViewModel(private val handle: SavedStateHandle) : ViewModel() {
    private val cartList = mutableListOf<CartItem>()
    private val cartProvider = CartProvider(cartList)

    init {
        // 注册 Provider
        val registry = handle.savedStateRegistry()
        registry.registerSavedStateProvider("cart", cartProvider)
        // 尝试恢复已有数据
        val savedBundle = handle.get<Bundle>("cart")
        cartProvider.restoreFromBundle(savedBundle)
    }

    // 对外暴露只读列表
    val cartItems: LiveData<List<CartItem>> = MutableLiveData(cartList.toList())

    fun addItem(item: CartItem) {
        cartList.add(item)
        // 通知更新(实际项目中使用 postValue)
        (cartItems as MutableLiveData).value = cartList.toList()
    }
}

4.4 注册 Provider 的三种方式对比

注册方式 说明 适用场景
SavedStateHandle.savedStateRegistry() 通过 ViewModel 的 handle 直接获取 Registry ViewModel 内部自包含数据
SavedStateRegistryController 在 Activity/Fragment 中手动控制注册 跨组件共享 Provider
AbstractSavedStateViewModelFactory 自定义 Factory 注入已注册的 Provider 依赖注入 + 测试

4.5 处理复杂对象序列化的策略

  • 嵌套对象:递归调用 Bundle.putBundle(),每个子对象对应一个子 Bundle。
  • 大列表:使用 JSON 或 ProtoBuf 序列化后存入 putString(注意字符串长度限制,一般 8KB 以内可接受)。
  • 二进制数据:用 putByteArray,配合 Kryo 或 FlatBuffers 降低体积。
  • 时间/日期:统一转为 Long(毫秒时间戳)存入。
  • 枚举:用 putString(name) 或 putInt(ordinal),两端约定。
注意:Bundle 本身对大小有限制(约 1 MB 内,Android 10+ 放宽)。若数据超过阈值,应考虑 Room 存储 + 仅在切后台时保存关键索引,而非全量序列化。

4.6 调试与验证技巧

  • 打印保存状态:在 saveState() 中 Log.d 输出序列化后的内容。
  • 模拟进程杀死:使用 adb shell am kill <package> 或 Android Studio 的 “Force Stop” + 重新打开。检查 ViewModel 恢复后的数据是否完整。
  • 检查注册冲突:同一键名注册多个 Provider 会导致后注册者覆盖前者。使用 registry.isRegistered("key") 在注册前确认。

4.7 性能与稳定性优化

  • 避免在主线程序列化:尤其大列表选用 JSON 或 ProtoBuf 时,建议在 Dispatchers.IO 进行序列化,然后将 Bundle 缓存。
  • 懒加载恢复:Provider 的 restoreFromBundle 改为在第一次访问数据时才反序列化,减少启动耗时。
  • Provider 生命周期管理:在 ViewModel 的 onCleared() 中移除注册的 Provider,防止内存泄漏。
  • 版本兼容:序列化时写入版本号字段,反序列化时检查版本,支持向后兼容。
最佳实践:对于频繁变动的数据(如列表编辑状态),使用 SavedStateHandle.set() 替代手动 Provider 反而更简单;仅当默认序列化不满足需求(如需要 Gson/Room 等自定义转换)时才定义 Provider。
自定义 SavedStateProvider 注册与保存流程 ViewModel.init() 注册 Provider 系统调用 saveState() Provider.saveState() 返回 Bundle SavedStateRegistry 合并到根 Bundle 写入 Bundle.savedState Activity.onSaveInstanceState 键值映射 关键要点 • 每个键只能注册一个 Provider • 恢复时按键从根 Bundle 中取 • 自定义序列化避免主线程 • 建议使用版本号兼容

4.8 常见错误排查

错误现象 根因 解决方案
进程重启后数据丢失 Provider 未注册或注册位置不对 确保在 init 块或 onCreate 之前注册
类型转换异常(Bundle 中存的是 String,但取出变成 null) 序列化时丢失类型信息,或键名拼错 使用常量定义键名,并在 save/restore 两端统一
反复注册导致性能下降 每次配置改变都重新创建 Provider 使用 lifecycleScope 或单例 Provider 缓存
Bundle 过大导致 TransactionTooLargeException 序列化了大体积数据 改用 Room 持久化,仅在 Provider 中保存 ID 或索引

4.9 部署建议

在正式项目中使用自定义 SavedStateProvider 时,建议将 Provider 与 ViewModel 分离,封装为独立的序列化策略类。推荐结构:

// 序列化策略接口
interface StateSerializer<T> {
    fun serialize(data: T): Bundle
    fun deserialize(bundle: Bundle): T
}

// 具体实现:购物车序列化器
class CartSerializer : StateSerializer<List<CartItem>> {
    override fun serialize(data: List<CartItem>): Bundle { /* 使用 Gson */ }
    override fun deserialize(bundle: Bundle): List<CartItem> { /* 反序列化 */ }
}

// ViewModel 中组合
class CartViewModel(handle: SavedStateHandle) : ViewModel() {
    private val serializer = CartSerializer()
    // ... 注册 Provider 时委托给 serializer
}

通过依赖注入(如 Hilt)可以提供测试替身,方便单元测试。