Skip to content
This repository was archived by the owner on Jul 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

Android SharedPreference delegation for Kotlin.

> [!IMPORTANT]
> **Kotpref is no longer maintained.** Development has ended and this repository is
> archived: there will be no further releases, and issues and pull requests are no
> longer accepted.
>
> Use [Jetpack DataStore](https://developer.android.com/topic/libraries/architecture/datastore)
> instead β€” see the [migration guide](docs/MIGRATION.md).

Existing builds are unaffected. All published artifacts remain on Maven Central and will
not be removed, so there is no rush to migrate. The Install and How to use sections below
stay in place as a reference for current users.

For most Kotpref models, [Preferences DataStore](https://developer.android.com/topic/libraries/architecture/datastore/preferences-datastore)
is the direct replacement: the same key-value storage, with an asynchronous, observable
API that SharedPreferences never had. [Proto DataStore](https://developer.android.com/topic/libraries/architecture/datastore/proto-datastore)
is worth a look as well β€” Kotpref existed largely to give a group of preferences the shape
of a typed object, and Proto DataStore does that natively, with a real schema instead of
loose keys. Community libraries offering a delegate-style API on top of DataStore also
exist, if that is what you liked about Kotpref.

Thanks to everyone who used Kotpref and contributed to it over the years.

[![kotlin](https://img.shields.io/badge/kotlin-1.4.10-blue.svg)]() [![codecov](https://codecov.io/gh/chibatching/Kotpref/branch/master/graph/badge.svg)](https://codecov.io/gh/chibatching/Kotpref) [![license](https://img.shields.io/github/license/chibatching/Kotpref.svg?maxAge=2592000)]()

## Install
Expand Down
206 changes: 206 additions & 0 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# Migrating from Kotpref to Jetpack DataStore

Kotpref is no longer maintained. This guide covers the parts of the move to
[Preferences DataStore](https://developer.android.com/topic/libraries/architecture/datastore/preferences-datastore)
that are specific to Kotpref β€” how its delegates, key names and preference file
names map across β€” and links to the official documentation for everything else.

Preferences DataStore is the closest match to how Kotpref stored things, which is
why this guide targets it. [Proto DataStore](https://developer.android.com/topic/libraries/architecture/datastore/proto-datastore)
is the other option, and a reasonable one: a `KotprefModel` was already a typed
object, and Proto DataStore gives that a real schema instead of loose keys. It
needs a serializer and a `migrate` lambda mapping the old values onto your type,
but the key-name and file-name notes below apply either way.

> [!NOTE]
> This was written when the project was archived and has not been verified against
> a real migration. Treat it as a starting point, and check the behaviour against
> your own data before shipping.

The published Kotpref artifacts stay on Maven Central, so nothing breaks if you
migrate gradually β€” or not at all.

## Delegate mapping

Each delegate maps onto a `Preferences.Key<T>` plus a default value that you now
supply at the read site: `preferences[key]` returns `null` when the key is absent,
and DataStore has no notion of a stored default.

| Kotpref delegate | Kotpref default | DataStore key | Read |
| --- | --- | --- | --- |
| `stringPref()` | `""` | `stringPreferencesKey` | `prefs[KEY] ?: ""` |
| `nullableStringPref()` | `null` | `stringPreferencesKey` | `prefs[KEY]` |
| `intPref()` | `0` | `intPreferencesKey` | `prefs[KEY] ?: 0` |
| `longPref()` | `0L` | `longPreferencesKey` | `prefs[KEY] ?: 0L` |
| `floatPref()` | `0f` | `floatPreferencesKey` | `prefs[KEY] ?: 0f` |
| `booleanPref()` | `false` | `booleanPreferencesKey` | `prefs[KEY] ?: false` |
| `stringSetPref()` | empty set | `stringSetPreferencesKey` | `prefs[KEY] ?: emptySet()` |

### Key names

The string passed to `xxxPreferencesKey(...)` **must match the key Kotpref used**,
or migrated data will not be found. Kotpref's rule is `key ?: property.name`: the
property name, unless the delegate was given an explicit `key`.

```kotlin
var highScore by longPref() // key: "highScore"
var useFunc1 by booleanPref(key = "use_func1") // key: "use_func1"
```

## Rewriting a KotprefModel

```kotlin
// Before
object UserInfo : KotprefModel() {
var name by stringPref()
var age by intPref(default = 14)
}

UserInfo.name = "chibatching"
val age = UserInfo.age
```

```kotlin
// After
private val Context.userInfoDataStore: DataStore<Preferences> by preferencesDataStore(name = "user_info")

class UserInfoRepository(private val dataStore: DataStore<Preferences>) {

private val NAME = stringPreferencesKey("name")
private val AGE = intPreferencesKey("age")

val name: Flow<String> = dataStore.data.map { it[NAME] ?: "" }
val age: Flow<Int> = dataStore.data.map { it[AGE] ?: 14 }

suspend fun setName(value: String) {
dataStore.edit { it[NAME] = value }
}
}
```

Taking the `DataStore` as a constructor parameter rather than keeping an `object`
makes the class easy to fake in tests. If a screen needs several values at once,
map them into a single data class in one `map { }` rather than combining flows.

Other equivalents:

- `bulk { }` / `blockingBulk { }` β†’ a single `edit { }`, which is atomic and
suspends until the write is durable. `commitByDefault` and
`commitAllPropertiesByDefault` have no counterpart and can be dropped.
- `clear()` β†’ `dataStore.edit { it.clear() }`
- `remove(UserInfo::age)` β†’ `dataStore.edit { it.remove(AGE) }`
- `Kotpref.init(context)` and the `initializer` module β†’ nothing; the
`preferencesDataStore` delegate creates the instance lazily.

## Synchronous `var` to `suspend` / `Flow`

This is where the real work is. Kotpref reads hit an in-memory SharedPreferences
map, so `UserInfo.age` returns immediately from anywhere. DataStore reads are a
`Flow` and writes are `suspend`, by design, so that disk I/O never runs on the
main thread. Three consequences to plan for:

1. **You cannot read a value at an arbitrary point in synchronous code.** Something
like `if (UserInfo.isLoggedIn)` inside a click listener has to become either a
collected state value or a `suspend` call inside a coroutine.
2. **The first emission is not instantaneous.** There is a window at startup where
the value is not available yet. Decide per screen whether that means a loading
state or a default value.
3. **Reads are a stream, not a snapshot.** Usually an upgrade: the UI updates by
itself when the value changes, which previously needed `livedata-support`.

For a genuine one-shot read β€” a `WorkManager` worker, an interceptor building a
header β€” use `dataStore.data.map { it[KEY] }.first()`. Reach for
`runBlocking { }` only where there is no alternative; it blocks the calling thread
and reintroduces exactly the jank DataStore avoids.

For plumbing the flow into a UI, the standard patterns apply and are documented
upstream: [`stateIn`](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow)
in a `ViewModel` with an explicit initial value,
[`collectAsStateWithLifecycle()`](https://developer.android.com/topic/libraries/architecture/compose#state-flow)
in Compose, `repeatOnLifecycle` in Views, or `.asLiveData()` if you want to keep
LiveData at the boundary. One thing to watch: writes are now fire-and-forget from
the caller's side, so do not write and then read the value back on the next line β€”
collect the flow instead.

## Migrating existing XML data

[`SharedPreferencesMigration`](https://developer.android.com/topic/libraries/architecture/datastore/preferences-datastore#datastore-sharedpreferences)
copies an existing SharedPreferences file into DataStore on first read, then
deletes it. Keys and types carry over as-is β€” which is why the key names above
must match.

### Which name do I pass?

`sharedPreferencesName` is the file name **without the `.xml` extension**, and
Kotpref derives it as follows:

- **`kotprefName` not overridden** β†’ the model's simple class name.
`object UserInfo : KotprefModel()` writes `UserInfo.xml`, so pass
**`"UserInfo"`**, capitalised exactly as the class is.
- **`kotprefName` overridden** β†’ that exact string.
`override val kotprefName: String = "user_info"` β†’ pass `"user_info"`.

Getting this wrong fails silently: no file is found, DataStore starts empty, and
every read falls back to your defaults β€” which looks identical to a working
migration. Confirm the real name on a device that has existing data:

```
adb shell run-as your.package.name ls shared_prefs/
```

```kotlin
private val Context.userInfoDataStore: DataStore<Preferences> by preferencesDataStore(
name = "user_info",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context = context,
sharedPreferencesName = "UserInfo", // Kotpref's kotprefName β€” the class name by default
)
)
}
)
```

The DataStore's own `name` is unrelated and does not have to match. Each
`KotprefModel` had its own XML file, so one DataStore per model is the natural
mapping; several migrations can feed one DataStore, but watch for key collisions,
since two models can both have a `name` property without conflicting today.

Passing `keysToMigrate` limits which keys move, in which case only those keys are
removed and the XML file survives if others remain. Otherwise the file is deleted
once everything has been migrated β€” so to retest, clear app storage, install the
old version, create data, and upgrade. Worth doing once before release.

## Optional modules

| Module | Replacement |
| --- | --- |
| `initializer` | Not needed β€” DataStore has no global initialisation step. |
| `livedata-support` | Built in. `asLiveData(UserInfo::name)` becomes `dataStore.data.map { it[NAME] }`, plus `.asLiveData()` if you still need LiveData. |
| `enum-support` | Store it yourself. `enumValuePref` persisted `Enum.name`, so read with `enumValueOf<T>(string)` guarded by `runCatching`. `enumOrdinalPref` persisted the ordinal β€” migrating is a good moment to switch to name-based storage, since reordering the enum silently changes the meaning of stored data. |
| `gson-support` | Either keep serialising to a string key yourself (existing JSON migrates across as a plain string, so you can keep reading it with Gson), or move to [Proto DataStore](https://developer.android.com/topic/libraries/architecture/datastore/proto-datastore) with a typed `Serializer` β€” the better home for structured objects. |
| `preference-screen-dsl` | No equivalent. `androidx.preference` is built directly on SharedPreferences and reads synchronously, so it does not fit DataStore's model. Either build the settings UI yourself, keep `androidx.preference` and SharedPreferences for that one screen (fine, as long as each key has exactly one owner), or bridge with a custom `PreferenceDataStore` β€” possible, but its API is synchronous, which defeats much of the point. This module was always experimental. |

## Other behaviour differences

- **Read-modify-write is safe inside `edit { }`.** Concurrent edits are serialised,
unlike `prefs.getInt(...) + 1` followed by a `put`.
- **`stringSetPref` returned a mutable set** whose mutations were written back
(`UserInfo.prizes.add("Gold")`). DataStore returns an immutable `Set<String>`;
replace the whole value: `prefs[PRIZES] = (prefs[PRIZES] ?: emptySet()) + "Gold"`.
- **No `MODE_MULTI_PROCESS`.** If you relied on `kotprefMode`, multi-process access
needs `MultiProcessDataStoreFactory`; opening one file from two processes with the
standard delegate corrupts it.
- **One instance per file.** Creating two DataStores over the same file name in one
process throws. Declare the delegate once, at top level.
- **Corruption surfaces as an `IOException`** from `dataStore.data`; the usual
handling is `.catch { if (it is IOException) emit(emptyPreferences()) else throw it }`.
- **Testing** no longer needs Robolectric for the storage layer β€” build a DataStore
over a temporary folder in a plain JVM test, or fake the repository outright.

---

Thanks for having used Kotpref. This repository is archived and cannot take
corrections, but this guide is Apache-2.0 like the rest of the project β€” copy and
fix it freely.
Loading