État local utilisateur — DataStore¶
Contexte¶
La base lmelp.db est en lecture seule : elle est générée par export MongoDB et embarquée dans l'APK (ou poussée via ADB). Elle ne peut pas stocker d'état propre à l'utilisateur (préférences, annotations, épingles).
Pour tout état local qui doit persister entre les sessions sans modifier la base Room, l'application utilise Jetpack DataStore Preferences.
UserPreferencesRepository¶
app/src/main/java/com/lmelp/mobile/data/repository/UserPreferencesRepository.kt
Dépôt unique pour toutes les préférences utilisateur locales. Il implémente l'interface PinnedReadingStorage (voir ci-dessous).
Clés DataStore actuelles¶
| Clé | Type | Usage |
|---|---|---|
show_hors_masque |
Boolean |
Afficher/masquer les livres hors Masque dans Mon Palmarès |
pinned_reading |
Set<String> |
IDs des livres épinglés "en cours de lecture" dans Sur ma liseuse |
Ajouter une nouvelle préférence¶
private val MA_PREF = stringPreferencesKey("ma_pref") // ou booleanPreferencesKey, intPreferencesKey…
val maPref: Flow<String> = context.dataStore.data
.map { prefs -> prefs[MA_PREF] ?: "valeur_par_defaut" }
suspend fun setMaPref(value: String) {
context.dataStore.edit { prefs -> prefs[MA_PREF] = value }
}
Pattern testabilité — Interface extraite¶
UserPreferencesRepository dépend de Context (Android), ce qui empêche son utilisation directe dans les tests JVM purs (sans émulateur).
Solution : extraire une interface pour la partie à tester.
Exemple : PinnedReadingStorage¶
// Dans UserPreferencesRepository
interface PinnedReadingStorage {
val pinnedReading: Flow<Set<String>>
suspend fun togglePinnedReading(livreId: String)
suspend fun removePinned(livreId: String)
}
class UserPreferencesRepository(context: Context) : PinnedReadingStorage {
// implémentation DataStore réelle
}
// Dans le fichier de test
class FakeUserPreferencesRepository : UserPreferencesRepository.PinnedReadingStorage {
private val _pinned = MutableStateFlow<Set<String>>(emptySet())
override val pinnedReading: Flow<Set<String>> = _pinned
override suspend fun togglePinnedReading(livreId: String) {
val current = _pinned.value
_pinned.value = if (livreId in current) current - livreId else current + livreId
}
override suspend fun removePinned(livreId: String) {
_pinned.value = _pinned.value - livreId
}
}
Le ViewModel reçoit l'interface, pas la classe concrète :
class OnKindleViewModel(
private val repository: OnKindleRepository,
private val pinnedStorage: UserPreferencesRepository.PinnedReadingStorage? = null
) : ViewModel()
Ainsi, les tests instancient FakeUserPreferencesRepository sans aucune dépendance Android.
Cas d'usage : épingles "en cours de lecture"¶
La fonctionnalité d'épinglage (issue #75) illustre le pattern complet :
- Stockage :
stringSetPreferencesKey("pinned_reading")dans DataStore - Lecture initiale :
init { pinnedStorage?.pinnedReading?.first() }dans le ViewModel - Auto-nettoyage : au chargement, les livres épinglés dont
calibre_lu = truesont automatiquement désépinglés viaremovePinned()— cela couvre la mise à jour DB vialmelp-update-mobile - Ordre : les épinglés sont placés en tête dans
loadOnKindle()après annotationisPinned
Référence¶
- Jetpack DataStore Preferences
app/src/test/java/com/lmelp/mobile/OnKindlePinTest.kt— exemple complet de tests avec fake