Aller au contenu
Tous les articles

Jetpack DataStore en 2026 : migrer depuis SharedPreferences sans perdre un seul réglage

Un guide pratique pour passer de SharedPreferences à Jetpack DataStore sur Android — les pièges asynchrones, le chemin de migration qui préserve les valeurs existantes, et comment le tester.

MFKAPPS 5 min de lecture

SharedPreferences fonctionne encore. Mais il effectue aussi une lecture disque synchrone au premier accès, n’offre aucune sécurité de type à la compilation, et ne propose aucun moyen d’observer un changement de valeur sans brancher un listener à la main. Jetpack DataStore corrige ces trois points, et en 2026 c’est le choix par défaut que recommande Google pour tout ce qui dépasse un simple flag jetable. Ce qui bloque la plupart des migrations, ce n’est pas d’apprendre la nouvelle API — c’est de faire passer les utilisateurs existants sans réinitialiser leurs réglages à la prochaine mise à jour.

Ce qui ne va vraiment pas avec SharedPreferences

getSharedPreferences().getString(...) a l’air synchrone et bon marché, mais lors du premier accès dans un process, il peut bloquer le thread appelant sur une vraie lecture de fichier — souvent le thread principal, au démarrage de l’app. Il n’existe aucun moyen intégré de collecter les changements sous forme de flux ; il faut enregistrer un OnSharedPreferenceChangeListener et gérer soi-même son cycle de vie. Et chaque lecture est typée par chaîne de caractères : une faute de frappe dans un nom de clé compile sans problème et échoue silencieusement à l’exécution en renvoyant la valeur par défaut.

Rien de tout cela n’est un cas limite exotique. C’est le comportement normal de SharedPreferences, et Preferences DataStore a été conçu spécifiquement pour corriger cela sans trop bouleverser le modèle mental.

Preferences DataStore : la même forme, en sûr

Une instance de Preferences DataStore se construit une seule fois, généralement comme propriété d’extension sur Context :

val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
    name = "settings"
)

Les lectures reviennent sous forme de Flow, donc vous obtenez les notifications de changement gratuitement au lieu de les construire vous-même :

val TIMER_MINUTES = intPreferencesKey("timer_minutes")

val timerMinutes: Flow<Int> = context.settingsDataStore.data
    .map { prefs -> prefs[TIMER_MINUTES] ?: 25 }

Les écritures passent par une fonction suspend, donc elles ne sont jamais appelées par accident depuis le thread principal :

suspend fun setTimerMinutes(context: Context, minutes: Int) {
    context.settingsDataStore.edit { prefs ->
        prefs[TIMER_MINUTES] = minutes
    }
}

C’est assez proche de SharedPreferences pour que la réécriture elle-même soit mécanique. Le risque se situe entièrement dans ce qui arrive aux données déjà présentes sur l’appareil d’un utilisateur.

La partie que tout le monde saute : migrer les valeurs existantes

DataStore fournit un SharedPreferencesMigration conçu exactement pour ça, et il est facile de le manquer car rien ne vous force à l’utiliser — votre app compilera et fonctionnera très bien sans lui, puis réinitialisera silencieusement tous les réglages à leur valeur par défaut au moment où vous changez le backend de stockage.

val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(context, "settings_prefs"))
    }
)

La migration s’exécute une seule fois, la première fois que le DataStore est ouvert après le déploiement de ce code : elle lit l’ancien fichier SharedPreferences, copie chaque entrée dans le nouveau store Preferences, et laisse l’ancien fichier en place (elle ne le supprime pas — c’est à vous de le faire séparément, une fois certain que la migration s’est exécutée partout). Si le type d’une clé ne se transpose pas proprement — un Set<String> là où vous voulez maintenant une List, par exemple — vous le filtrez dans le shouldRunMigration de la migration ou le transformez explicitement plutôt que de laisser une incompatibilité de type lever une exception à la lecture.

Se tromper sur le nom de l’ancien fichier de préférences — une erreur courante s’il a été créé avec un nom personnalisé plutôt que le nom par défaut du package — fait que la migration ne trouve rien à copier, et chaque utilisateur repart avec les valeurs par défaut à la mise à jour. Avant de déployer, vérifiez le nom exact avec les appels context.getSharedPreferences("name", MODE_PRIVATE) déjà présents dans le code plutôt que de le deviner.

Quand Preferences DataStore ne suffit pas

Preferences DataStore conserve un espace de clés plat et typé par chaîne — il a résolu les problèmes de threading et d’observabilité mais pas celui de la sécurité de type. Proto DataStore remplace le sac clé-valeur par un schéma défini une fois dans un fichier .proto, de sorte qu’un objet de réglages correspond au schéma ou ne compile pas — pas de résultat null venant d’une clé mal orthographiée à l’exécution. Cela vaut la configuration supplémentaire dès qu’un écran de réglages dépasse une poignée de flags, ou dès que des objets imbriqués (une préférence de notification avec ses propres champs son, vibration et heures silencieuses) commencent à apparaître dans l’espace de clés comme trois ou quatre clés dans des espaces de noms séparés au lieu d’une seule valeur structurée. Dans Mintly, les réglages de son, de vibration et de redémarrage automatique du minuteur sont passés à un petit message Proto DataStore exactement pour cette raison — dès que des réglages liés doivent être lus et écrits ensemble, un store clé-valeur plat commence à vous compliquer la tâche.

Tester la migration, pas seulement la nouvelle API

Le nouveau code de lecture/écriture est assez simple pour qu’on soit tenté de ne pas le tester. La migration, elle, ne l’est pas — c’est la seule pièce de ce changement qui s’exécute exactement une fois, silencieusement, sur de vraies données utilisateur, sans possibilité de réessayer si elle est fausse. Un test minimal crée un vrai fichier SharedPreferences, ouvre un DataStore avec la migration attachée, et vérifie que les valeurs ont survécu :

@Test
fun migration_preservesExistingTimerSetting() = runTest {
    val prefs = context.getSharedPreferences("settings_prefs", Context.MODE_PRIVATE)
    prefs.edit().putInt("timer_minutes", 45).commit()

    val dataStore = PreferenceDataStoreFactory.create(
        migrations = listOf(SharedPreferencesMigration(context, "settings_prefs")),
        produceFile = { File(context.filesDir, "test_settings.preferences_pb") }
    )

    val minutes = dataStore.data.first()[intPreferencesKey("timer_minutes")]
    assertEquals(45, minutes)
}

Exécutez ceci une fois pour chaque clé actuellement en production, pas seulement celles d’une nouvelle branche — la migration doit reporter tout ce qu’un vrai appareil a accumulé, y compris des réglages venant de fonctionnalités déployées des années avant cette réécriture.

La check-list

Avant de fusionner une migration de SharedPreferences vers DataStore : le nom de l’ancien fichier de préférences est vérifié contre l’appel getSharedPreferences() réel dans le code, SharedPreferencesMigration est branché pour chaque clé actuellement utilisée, un test crée l’ancien format et vérifie que chaque valeur survit, et l’ancien fichier reste intact jusqu’à ce que la télémétrie confirme que la migration s’est exécutée sur la base installée. C’est un peu de soin en plus pour un changement que les utilisateurs ne devraient jamais remarquer du tout — ce qui, pour une migration de réglages, est exactement le but.