core/configstore/README.md
A small, type-safe configuration store built on AndroidX DataStore Preferences. Use it to define a schema for your feature settings, observe them as a Flow, update atomically, and migrate between versions.
graph TB
subgraph FEATURE[App Feature]
FEATURE_DATASTORE[FeatureDataStore]
end
subgraph CORE_API[ConfigStore API]
CONFIG_STORE["`ConfigStore<T>`"]
BASE_CONFIG_STORE[BaseConfigStore]
CONFIG_DEFINITION["`ConfigDefinition<T>`"]
end
subgraph CORE_BACKEND[ConfigStore Backend]
BACKEND_PROVIDER[ConfigBackendProvider]
DATASTORE_BACKEND[DataStoreConfigBackend / DefaultDataStoreConfigBackend]
end
subgraph STORAGE[Storage]
PREFERENCES_FILE["DataStore Preferences file <backend>.preferences_pb"]
end
FEATURE_DATASTORE --> |uses| CONFIG_STORE
CONFIG_STORE --> |delegates to| BASE_CONFIG_STORE
BASE_CONFIG_STORE --> |reads schema| CONFIG_DEFINITION
BASE_CONFIG_STORE --> |requests| BACKEND_PROVIDER
BACKEND_PROVIDER --> |creates| DATASTORE_BACKEND
DATASTORE_BACKEND --> |persists to| STORAGE
BASE_CONFIG_STORE -..-> |on start| MIGRATION_CHECK{Migration?}
MIGRATION_CHECK --> |version < definition.version| RUN_MIGRATION[Run migration - write removals]
classDef feature fill:#d9ffd9,stroke:#000000,color:#000000
classDef feature_module fill:#33cc33,stroke:#000000,color:#000000
classDef core fill:#e6cce6,stroke:#000000,color:#000000
classDef core_module fill:#cc99cc,stroke:#000000,color:#000000
classDef library fill:#fff0d0,stroke:#000000,color:#000000
classDef library_module fill:#ffaa33,stroke:#000000,color:#000000
linkStyle default stroke:#999,stroke-width:2px
linkStyle 6 stroke:#999,stroke-width:2px
class FEATURE feature
class FEATURE_DATASTORE feature_module
class CORE_API,CORE_BACKEND core
class CONFIG_STORE,BASE_CONFIG_STORE,CONFIG_DEFINITION,BACKEND_PROVIDER,DATASTORE_BACKEND core_module
class STORAGE library
class PREFERENCES_FILE library_module
ConfigDefinition with:
Config (key-value pairs) and your typed modelBaseConfigStore runs migrations once per version and delegates persistence to a backend (default: DataStore Preferences).<backend>.preferences_pb.// Keys
object MyFeatureKeys {
val ENABLED = ConfigKey.BooleanKey("myfeature_enabled")
val USERNAME = ConfigKey.StringKey("myfeature_username")
}
// Model
data class MyFeatureConfig(
val enabled: Boolean,
val username: String,
)
// Mapper
object MyFeatureMapper : ConfigMapper<MyFeatureConfig> {
override fun toConfig(obj: MyFeatureConfig): Config = Config().apply {
this[MyFeatureKeys.ENABLED] = obj.enabled
this[MyFeatureKeys.USERNAME] = obj.username
}
override fun fromConfig(config: Config): MyFeatureConfig? {
val enabled = config[MyFeatureKeys.ENABLED] ?: return null
val username = config[MyFeatureKeys.USERNAME] ?: return null
return MyFeatureConfig(enabled, username)
}
}
// Migration (no-op example)
object MyFeatureMigration : ConfigMigration {
override suspend fun migrate(currentVersion: Int, newVersion: Int, current: Config): ConfigMigrationResult =
ConfigMigrationResult.NoOp
}
// Definition
object MyFeatureDefinition : ConfigDefinition<MyFeatureConfig> {
override val version = 1
override val id = ConfigId(backend = "myfeature", feature = "settings")
override val mapper = MyFeatureMapper
override val defaultValue = MyFeatureConfig(false, "")
override val keys = listOf(MyFeatureKeys.ENABLED, MyFeatureKeys.USERNAME)
override val migration: ConfigMigration = MyFeatureMigration
}
class MyFeatureConfigStore(
provider: ConfigBackendProvider,
) : BaseConfigStore<MyFeatureConfig>(
provider = provider,
definition = MyFeatureDefinition,
)
// Observe
val job = coroutineScope.launch {
myFeatureConfigStore.config.collect { cfg ->
// react to changes
}
}
// Update atomically
coroutineScope.launch {
myFeatureConfigStore.update { current ->
val cfg = current ?: MyFeatureDefinition.defaultValue
cfg.copy(enabled = true)
}
}
// Clear all stored values for this backend file
coroutineScope.launch { myFeatureConfigStore.clear() }
AndroidConfigBackendFileManager to store under Context.filesDir.JvmConfigBackendFileManager with a working directory.Example with Koin:
val appCommonCoreConfigStoreModule = module {
single<ConfigBackendFileManager> { AndroidConfigBackendFileManager(get()) }
single<ConfigBackendFactory> { DataStoreConfigBackendFactory(fileManager = get()) }
single<ConfigBackendProvider> { DefaultConfigBackendProvider(backendFactory = get()) }
}
While the default implementation uses AndroidX DataStore Preferences, the API supports other backends.
Trigger: On first subscription, BaseConfigStore compares stored vs. definition version.
Contract: ConfigMigration.migrate(currentVersion, newVersion, current: Config): ConfigMigrationResult returns:
Migrated(updated: Config, keysToRemove: Set<ConfigKey<*>>)NoOpBehavior:
Migrated: backend updates config, removes obsolete keys, writes new version.NoOp: only writes new version.Examples:
<backend>.preferences_pb; all features using the same backend id share one preferences file.