Jetpack DataStore : une nouvelle aventure de Google dans les solutions de stockage Android
Pendant longtemps, SharedPreferences a été un choix évident pour gérer les préférences sur Android. Aujourd’hui, les usages évoluent, les attentes aussi. Et si c’était le moment de repenser nos habitudes avec une approche plus moderne, plus fluide… mais pas forcément sans concessions ?

Pendant des années, nous avons utilisé SharedPreferences et EncryptedSharedPreferences pour stocker des données sous forme clé-valeur sur Android. Aujourd’hui, Google commence à déprécier certaines de ces API et nous pousse doucement vers une alternative plus “moderne” : Jetpack DataStore. Adopter des outils à la pointe de la technologie a toujours du sens, mais encore faut-il qu’ils ne fassent pas l’impasse sur des fonctionnalités essentielles. Jetons un œil de plus près.
Adieu SharedPreferences, bonjour DataStore
Avec les annonces de dépréciation qui s’accumulent (ici et là), nous avons décidé de tester DataStore. Cette solution s’appuie sur Kotlin Coroutines et Flow, ce qui la rend plus performante et moins sujette aux fameux “Application Not Responding” que l’ancienne approche avec SharedPreferences. Au lieu d’un fonctionnement synchrone et bloquant, DataStore effectue les lectures et écritures de manière asynchrone — une véritable amélioration dans bien des cas.
SharedPreferences vs DataStore en pratique
Voici un petit comparatif pour illustrer les différences entre les deux :
SharedPreferences
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("key", "value").apply()
val value = sharedPreferences.getString("key", "default")
DataStore
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
suspend fun saveValue(value: String) {
context.dataStore.edit { preferences ->
preferences[stringPreferencesKey("key")] = value
}
}
val valueFlow: Flow<String?> = context.dataStore.data
.map { preferences -> preferences[stringPreferencesKey("key")] }
Bien que DataStore propose une approche plus structurée et réactive, SharedPreferences reste plus simple à mettre en place pour un stockage clé-valeur basique.
Les deux variantes de DataStore
DataStore se décline en deux implémentations, chacune adaptée à des cas d’usage spécifiques :
- Preferences DataStore : c’est le remplacement le plus direct de SharedPreferences. Il permet de stocker des paires clé-valeur sans avoir besoin de définir un schéma à l’avance. Léger et facile à utiliser, il est parfait pour enregistrer des préférences simples ou des indicateurs d’état.
- Proto DataStore : cette version repose sur les Protocol Buffers pour définir un schéma structuré de vos données. Elle garantit une sécurité de type (type safety) et convient mieux aux cas d’usage plus complexes ou évolutifs. Elle nécessite en revanche de créer des fichiers .proto et de générer les classes Kotlin associées. Pour un exemple concret, jette un œil au codelab officiel sur Proto DataStore.
Pourquoi DataStore est (techniquement) meilleur
Google affirme que DataStore est plus moderne, plus sûr et plus scalable. Il offre notamment :
- Des lectures/écritures asynchrones : contrairement à SharedPreferences qui fonctionne de manière synchrone (et donc potentiellement bloquante), DataStore exploite pleinement les Coroutines et Flow de Kotlin pour un traitement non bloquant.
- Une sécurité de type renforcée : avec Proto DataStore, vous définissez un schéma clair via Protocol Buffers, ce qui permet de fortement typer vos données et d’éviter des bugs liés à de mauvaises clés ou valeurs.
- Une intégration fluide avec Jetpack : en tant que composant Jetpack, DataStore s’intègre naturellement avec les autres briques de l’architecture Android comme ViewModel, LiveData ou Navigation.
- Une migration facilitée depuis SharedPreferences : Google fournit un mécanisme intégré pour transférer facilement vos données existantes vers DataStore.
Les limites de DataStore
Et le chiffrement, alors ?
L’un des gros points faibles de DataStore, c’est l’absence totale de chiffrement natif, là où EncryptedSharedPreferences le proposait par défaut. Sur ce sujet, Google reste désespérément silencieux, malgré des années de retours de la communauté (voir le bug tracker). Le fait que cette demande ne soit toujours pas prise en compte est franchement frustrant pour celles et ceux qui gèrent des données sensibles.
Même avec les nouvelles versions des systèmes d’exploitation, le chiffrement par élément reste nécessaire pour de nombreuses applications manipulant des informations sensibles. Le chiffrement basé sur les fichiers (FBE) par défaut d'Android, introduit avec Android 10, ne protège les données qu'au repos. Une fois les données chargées en mémoire, elles sont entièrement exposées, sauf si un chiffrement au niveau de chaque élément est appliqué.
En réponse, des devs ont pris les choses en main, comme avec ce projet open source : EncryptedDataStore. Ça fonctionne, mais devoir passer par une dépendance externe pour une fonctionnalité aussi basique donne quand même l’impression qu’il manque quelque chose d’essentiel.
Et la dépréciation de la librairie crypto ?
Google a aussi déprécié sa librairie Jetpack Security, celle-là même qui est utilisée par EncryptedSharedPreferences. Résultat : encore plus d’incertitudes quant à l’avenir de cette solution pour du stockage sécurisé. Cela dit, même si la librairie est officiellement obsolète, les applications qui l’utilisent fonctionnent toujours normalement aujourd’hui.
Multiplateforme, mais à quel prix ?
L’un des arguments en faveur de DataStore, c’est son support multiplateforme : on peut l’utiliser dans des projets iOS ou desktop en plus d’Android. Idéal pour les équipes qui visent un codebase unifié. Mais attention, ce n’est pas sans compromis :
- Stockage basé sur des fichiers : DataStore écrit les données dans un fichier en utilisant des coroutines. Sur Android, c’est logique. Sur iOS, en revanche, c’est assez inhabituel. Là-bas, on s’appuie plutôt sur UserDefaults ou le Keychain pour les préférences et les données sensibles. Utiliser un fichier directement peut poser des problèmes d’intégration avec les comportements natifs (sauvegarde, sécurité, etc.).
- Sécurité et attentes iOS : sur iOS, on attend d’une solution de stockage qu’elle utilise les mécanismes sécurisés de l’OS (comme le Keychain). L’absence de chiffrement dans DataStore crée une incohérence de sécurité entre les plateformes — sauf si tu implémentes toi-même un chiffrement personnalisé partout.
En résumé : le côté multiplateforme de DataStore est prometteur, mais il faut être conscient des limites. C’est une solution intéressante si tu es déjà engagé dans un projet Kotlin Multiplatform. Sinon, pour ceux qui veulent rester proches des usages natifs et garder les choses simples, les mécanismes de stockage propres à chaque OS restent probablement plus adaptés.
Faut-il abandonner SharedPreferences ?
TL;DR : Pas d’urgence. Vous n'avez peut-être même pas besoin de changer. Même si SharedPreferences semble un peu dépassé, c’est toujours une option valable. Ce n’est pas déprécié, et ça ne le sera peut-être jamais. Et nous avons préparé des solutions pour contourner les dépréciations et adopter une approche plus moderne.
SharedPreferences Sécurisé
Pourquoi la dépréciation ?
EncryptedSharedPreferences repose directement sur le Android Keystore, qui présente de sérieuses limites : comportement incohérent selon les appareils, support fragile de modes de chiffrement comme AES-GCM, et aucune solution propre pour mettre à jour les schémas de chiffrement une fois déployés. Ces faiblesses créent des risques difficiles à corriger sans casser la compatibilité. Google a donc déprécié les API cryptographiques de Jetpack Security et recommande désormais de migrer vers Tink.
Tink fournit une couche de cryptographie cohérente, sécurisée, et évolutive, indépendante du comportement spécifique des appareils Android. Elle permet d'utiliser des standards modernes de chiffrement sans se soucier des détails bas niveau ou de la fragmentation des plateformes, et est largement utilisée dans les produits Google.
Une alternative moderne avec Tink
Sachant cela, nous avons préparé un remplaçant léger : SecureStorage. Le code :
- Chiffre à la fois les clés et les valeurs avec le chiffrement AEAD de Tink.
- Sauvegarde les données dans des fichiers standard SharedPreferences.
- N’a aucune dépendance à des bibliothèques dépréciées.
- S’intègre facilement avec votre code existant.
Exemple d'utilisation :
val securePrefs = SecureSharedPreferences(context, "secure_prefs")
securePrefs.putString("user_token", "abc123")
val token = securePrefs.getString("user_token")
Des SharedPreferences asynchrones ?
Un point clé : SharedPreferences fonctionne en mémoire, et l'utilisation de .apply() enregistre les données sur le disque de manière asynchrone, en arrière-plan. Pour de nombreuses applications avec des besoins simples, SharedPreferences reste un choix simple et efficace.
Si tu veux moderniser un peu les choses (observation réactive, typage fort, etc.), tu peux aussi encapsuler SharedPreferences dans une surcouche structurée. Par exemple, avec KSP, tu peux générer un wrapper observable et asynchrone. Le code présenté ici n’est pas complet, mais donne une bonne idée de ce qui nous semble être une solution de contournement efficace.
internal data object PreferencesKeys {
@Preference(type = PreferencesType.String, secure = true, observable = false)
const val SESSION_TOKEN: String = "session_token"
@Preference(type = PreferencesType.Boolean, secure = false, observable = true)
const val DARK_MODE: String = "dark_mode"
}
Nous avons défini trois paramètres dans l’annotation pour guider le processeur :
- type : spécifie le type de données. Les types pris en charge sont String, Boolean, Int, Long et Float, comme dans SharedPreferences.
- secure : indique si la donnée doit être stockée dans un SharedPreferences classique ou dans un EncryptedSharedPreferences.
- observable : permet de générer un Flow pour observer les changements de la préférence — uniquement si nécessaire.
Le code généré te fournira à la fois des accesseurs classiques et des StateFlow observables :
// sessionToken
internal var Preferences.sessionToken: String?
get() = getString(key = PreferencesKeys. SESSION_TOKEN, secure = true)
set(value) {
setString(key = PreferencesKeys. SESSION_TOKEN, secure = true, value = value)
}
// darkMode
private val Preferences.darkModeStateFlow: MutableStateFlow<Boolean?>
get() {
if (darkModeField == null) {
darkModeField = MutableStateFlow(
getBoolean(key = PreferencesKeys.DARK_MODE, secure = false)
)
}
return darkModeField!!
}
private var darkModeField: MutableStateFlow<Boolean?>? = null
internal val Preferences.darkModeFlow: Flow<Boolean?>
get() = darkModeStateFlow
internal var Preferences.darkMode: Boolean?
get() = darkModeStateFlow.value
set(value) {
setBoolean(key = PreferencesKeys.DARK_MODE, secure = false, value = value)
darkModeStateFlow.value = value
}
Cela fait le lien entre l’ancien et le nouveau — on garde la compatibilité tout en adoptant les principes réactifs. Voici l’interface Preferences minimale que tu implémenterais en coulisses :
internal interface Preferences {
fun getString(key: String, secure: Boolean): String?
fun setString(key: String, secure: Boolean, value: String?)
// Same for Boolean, Int, Long, Float
}
Cette approche a été pensée pour une application multiplateforme, où l’implémentation iOS/macOS de cette interface pourrait s’appuyer sur UserDefaults et Keychain.
SharedPreferences avec typage fort
En utilisant la même stratégie de génération de code, tu peux aussi sérialiser des data classes complètes pour les stocker sous forme de chaînes de caractères. Par exemple :
data class UserProfile(val id: String, val email: String)
val Preferences.userProfile: UserProfile?
get() = getString("user_profile", secure = true)?.let { json.decodeFromString(it) }
set(value) {
setString("user_profile", secure = true, value = value?.let { json.encodeToString(it) })
}
Conclusion : DataStore progresse, mais des lacunes subsistent
DataStore est une grande amélioration par rapport à SharedPreferences en termes d’architecture, de scalabilité et de pratiques de développement modernes. Pour les nouvelles applications qui n’ont pas besoin de stockage chiffré, c’est un excellent choix, solide et pérenne. Cependant, pour des équipes comme la nôtre, l'absence de chiffrement intégré reste une limitation majeure.
Nous y avons été confrontés lors d'un pentest, où le stockage local chiffré des tokens sensibles était une exigence stricte. Des solutions comme le chiffrement manuel des valeurs ou l'utilisation de bibliothèques comme encrypted-datastore fonctionnent bien. Mais selon nous, le chiffrement ne devrait pas être une responsabilité manuelle laissée aux développeurs. Il devrait faire partie du système de stockage lui-même, comme c’était le cas avec EncryptedSharedPreferences.
Aujourd’hui, SharedPreferences reste un choix valable, surtout lorsqu’il est correctement sécurisé avec une solution comme notre SecureStorage, basé sur Tink. Nous avons également construit des outils autour de SharedPreferences, utilisant la génération de code pour reproduire une grande partie des avantages de DataStore, comme l’accès asynchrone et une forte sécurité des types. Cela nous permet d’offrir une expérience de développement moderne sans sacrifier les garanties de chiffrement.
Cependant, si Google veut que les développeurs adoptent pleinement DataStore — et améliorent ainsi la sécurité des applications — ils doivent proposer une solution de chiffrement officielle et facile à utiliser. Sinon, beaucoup d'équipes continueront d'utiliser EncryptedSharedPreferences par nécessité et par praticité, même si les API de chiffrement sous-jacentes deviennent de plus en plus obsolètes.