You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Dagger Hilt实现Proto DataStore的数据迁移?

DataStore迁移问题排查与Proto DataStore结合Hilt实现指南

一、当前Json DataStore迁移失效的原因

  1. 反序列化失败直接触发重置:当新增/重命名字段时,旧数据的Json结构与新AppSettings不匹配,readFrom会抛出SerializationException进而触发CorruptionException,此时corruptionHandler直接返回默认值,完全没进入迁移流程——迁移是成功读取当前数据后才会执行的逻辑,不负责处理数据损坏。
  2. 迁移无版本判断逻辑:shouldMigrate(currentData) = true会导致每次初始化DataStore都执行迁移,但你的migrate方法只是原样返回数据,既没处理字段变更,也无法区分哪些数据需要升级。
  3. 迁移未覆盖字段变更场景:比如新增字段时未补充默认值、重命名字段时未完成值的转移,导致旧数据无法适配新结构。

二、Json DataStore正确迁移实现方案

1. 给数据类添加版本标识

修改AppSettings,加入版本字段用于判断迁移时机:

@Serializable
data class AppSettings(
    val version: Int = 1, // 初始版本为1,字段变更时递增
    val prepareSeconds: Long = 10L,
    val workSeconds: Long = 90L,
    val restSeconds: Long = 30L,
    val totalSets: Int = 3,
    val newFeatureEnabled: Boolean = false // 新增字段,版本升级到2
) {
    companion object {
        fun getDefaultInstance() = AppSettings()
    }
}

2. 调整Serializer兼容旧数据

在readFrom中处理旧版本无version字段的情况,避免直接抛出异常:

object AppSettingsSerializer : Serializer<AppSettings> {

    override val defaultValue: AppSettings
        get() = AppSettings()

    override suspend fun readFrom(input: InputStream): AppSettings {
        return try {
            val jsonString = input.readBytes().decodeToString()
            val jsonElement = Json.parseToJsonElement(jsonString)
            // 兼容旧版本无version字段的情况
            val version = jsonElement.jsonObject["version"]?.jsonPrimitive?.intOrNull ?: 1
            
            when (version) {
                1 -> {
                    // 从旧版本数据构造新版本对象
                    val prepareSeconds = jsonElement.jsonObject["prepareSeconds"]?.jsonPrimitive?.longOrNull ?: 10L
                    val workSeconds = jsonElement.jsonObject["workSeconds"]?.jsonPrimitive?.longOrNull ?: 90L
                    val restSeconds = jsonElement.jsonObject["restSeconds"]?.jsonPrimitive?.longOrNull ?: 30L
                    val totalSets = jsonElement.jsonObject["totalSets"]?.jsonPrimitive?.intOrNull ?: 3
                    
                    AppSettings(
                        version = 2,
                        prepareSeconds = prepareSeconds,
                        workSeconds = workSeconds,
                        restSeconds = restSeconds,
                        totalSets = totalSets,
                        newFeatureEnabled = false // 新增字段默认值
                    )
                }
                else -> Json.decodeFromString(AppSettings.serializer(), jsonString)
            }
        } catch (e: SerializationException) {
            throw CorruptionException("Unable to read AppSettingsPrefs", e)
        }
    }

    @Suppress("BlockingMethodInNonBlockingContext")
    override suspend fun writeTo(t: AppSettings, output: OutputStream) {
        output.write(
            Json.encodeToString(
                serializer = AppSettings.serializer(),
                value = t
            ).encodeToByteArray()
        )
    }
}

3. 基于版本的DataMigration实现

如果需要通过DataMigration而非Serializer处理升级,调整Hilt模块中的迁移逻辑:

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    private const val DATA_STORE_FILE_NAME = "app_settings_preferences.pb"

    @Singleton
    @Provides
    fun provideAppSettingsDataStore(
        @ApplicationContext appContext: Context,
        @ApplicationScope coroutineScope: CoroutineScope,
    ): DataStore<AppSettings> {
        return DataStoreFactory.create(
            serializer = AppSettingsSerializer,
            corruptionHandler = ReplaceFileCorruptionHandler {
                AppSettings.getDefaultInstance()
            },
            scope = coroutineScope,
            produceFile = {
                appContext.dataStoreFile(DATA_STORE_FILE_NAME)
            },
            migrations = listOf(object : DataMigration<AppSettings> {
                // 仅当前数据版本低于目标版本时执行迁移
                override suspend fun shouldMigrate(currentData: AppSettings) = currentData.version < 2

                override suspend fun migrate(currentData: AppSettings): AppSettings {
                    // 处理版本升级:新增字段赋值默认值,或重命名字段等
                    return currentData.copy(
                        version = 2,
                        newFeatureEnabled = false
                    )
                }

                override suspend fun cleanUp() = Unit
            })
        )
    }
}

三、Proto DataStore结合Dagger Hilt的迁移实现

1. 定义Proto文件

在app/src/main/proto下创建app_settings.proto:

syntax = "proto3";

option java_package = "com.yourpackage.app";
option java_multiple_files = true;

message AppSettings {
  int32 version = 1; // 版本标识,用于迁移
  int64 prepare_seconds = 2;
  int64 work_seconds = 3;
  int64 rest_seconds = 4;
  int32 total_sets = 5;
  bool new_feature_enabled = 6; // 新增字段,版本升级到2
}

2. 配置Proto编译(build.gradle.kts)

在app模块的build.gradle.kts中添加依赖与配置:

plugins {
    id("com.android.application")
    id("kotlin-android")
    id("kotlin-kapt")
    id("dagger.hilt.android.plugin")
    id("com.google.protobuf")
}

android {
    sourceSets {
        getByName("main") {
            proto.srcDir("src/main/proto")
        }
    }
}

dependencies {
    // Proto DataStore
    implementation("androidx.datastore:datastore-core:1.0.0")
    implementation("androidx.datastore:datastore-proto:1.0.0")
    // Protobuf
    implementation("com.google.protobuf:protobuf-javalite:3.21.12")
    // Hilt
    implementation("com.google.dagger:hilt-android:2.44")
    kapt("com.google.dagger:hilt-android-compiler:2.44")
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.21.12"
    }
    generateProtoTasks {
        all().forEach { task ->
            task.builtins {
                create("java") {
                    option("lite")
                }
            }
        }
    }
}

3. 实现迁移与Hilt注入

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    private const val DATA_STORE_FILE_NAME = "app_settings.pb"

    @Singleton
    @Provides
    fun provideAppSettingsDataStore(
        @ApplicationContext appContext: Context,
        @ApplicationScope coroutineScope: CoroutineScope,
    ): DataStore<AppSettings> {
        val serializer = AppSettingsSerializer()
        return DataStoreFactory.create(
            serializer = serializer,
            corruptionHandler = ReplaceFileCorruptionHandler {
                AppSettings.getDefaultInstance()
            },
            scope = coroutineScope,
            produceFile = {
                appContext.dataStoreFile(DATA_STORE_FILE_NAME)
            },
            migrations = listOf(object : DataMigration<AppSettings> {
                override suspend fun shouldMigrate(currentData: AppSettings): Boolean {
                    // 版本1升级到版本2
                    return currentData.version < 2
                }

                override suspend fun migrate(currentData: AppSettings): AppSettings {
                    return currentData.toBuilder()
                        .setVersion(2)
                        .setNewFeatureEnabled(false) // 新增字段设置默认值
                        .build()
                }

                override suspend fun cleanUp() = Unit
            })
        )
    }

    // Proto DataStore的Serializer实现
    class AppSettingsSerializer : Serializer<AppSettings> {
        override val defaultValue: AppSettings = AppSettings.getDefaultInstance()

        override suspend fun readFrom(input: InputStream): AppSettings {
            return try {
                AppSettings.parseFrom(input)
            } catch (e: InvalidProtocolBufferException) {
                throw CorruptionException("Unable to read AppSettings", e)
            }
        }

        override suspend fun writeTo(t: AppSettings, output: OutputStream) {
            t.writeTo(output)
        }
    }
}

4. 关键注意事项

  • Proto字段的标签号(如version = 1中的1)绝对不能修改,否则会导致旧数据无法解析;新增字段使用全新的标签号即可。
  • 迁移时通过toBuilder()修改旧数据,确保保留原有字段的值,仅调整需要变更的部分。
  • 每次版本升级需递增version字段,并在shouldMigrate中判断是否需要执行迁移。

内容的提问来源于stack exchange,提问作者Vikash Singh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 04:42:06