Quand on dĂ©veloppe avec Realm sur Android, il faut garder une vĂ©ritĂ© simple en tĂȘte :
Realm est strictement lié au thread sur lequel une instance est ouverte. Si vous ouvrez
un Realm sur un thread, vous ne pouvez pas le réutiliser sur un autre. Cela entre en
conflit apparent avec lâesprit des coroutines (suspend/resume), qui peuvent ĂȘtre
reprises sur différents threads.
Ce billet prĂ©sente des bonnes pratiques et des helpers simples pour intĂ©grer Realm dans une architecture moderne (coroutines, Flow, ViewModel) sans se prendre la tĂȘte.
â ïž Principe fondamental
- Une instance Realm doit rester confinĂ©e au thread qui lâa ouverte.
- Les coroutines peuvent changer de thread â il faut donc les contraindre quand on manipule Realm.
â RĂšgles gĂ©nĂ©rales
- Isoler toutes les opérations Realm sur un thread unique.
- Toujours fermer les instances Realm (use / try-with-resources).
- Centraliser le dispatcher dĂ©diĂ© Ă Realm pour rĂ©utiliser le mĂȘme thread.
- Pour exposer des données réactives, utiliser des Flow bien conçus (callbackFlow, flowOn) ou mettre à jour des StateFlow/SharedFlow depuis un contexte Realm sécurisé.
Dispatcher Realm : créer une instance unique
Nâutilisez pas Executors.newSingleThreadExecutor().asCoroutineDispatcher() inline Ă
chaque appel : vous créeriez plusieurs threads. Créez plutÎt un dispatcher partagé
quelque part (singleton / object) :
// RealmDispatcher.kt
object RealmDispatcher {
// Une seule instance partagée pour toute l'app
val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
// Optionnel : appeler Ă l'arrĂȘt de l'app
fun shutdown() {
(dispatcher as ExecutorCoroutineDispatcher).close()
}
}
Utilisez RealmDispatcher.dispatcher partout pour garantir la mĂȘme thread-affinitĂ©.
Helpers Kotlin pour éviter la douleur
Ces deux helpers encapsulent la gestion dâinstance et la contrainte de thread.
inline fun CoroutineScope.launchWithRealm(
dispatcher: CoroutineDispatcher = RealmDispatcher.dispatcher,
realmConfiguration: RealmConfiguration = RealmHelper.currentUserRealmConfiguration,
crossinline block: suspend (Realm, CoroutineScope) -> Unit
) = launch(dispatcher) {
RealmHelper.getMonitoredInstance(realmConfiguration).use { realm ->
block(realm, this)
}
}
suspend fun <T> withRealmContext(
dispatcher: CoroutineDispatcher = RealmDispatcher.dispatcher,
realmConfiguration: RealmConfiguration = RealmHelper.currentUserRealmConfiguration,
block: suspend (Realm) -> T
): T = withContext(dispatcher) {
RealmHelper.getMonitoredInstance(realmConfiguration).use { realm ->
block(realm)
}
}
Remarques :
usegarantit la fermeture de lâinstance Realm mĂȘme en cas dâexception.- En exposant
RealmDispatcher.dispatchervous évitez la création répétée de threads.
Patterns avec Flow â StateFlow & SharedFlow
Lâutilisation de StateFlow et SharedFlow est courante en architecture moderne.
Voici des patterns sûrs pour les alimenter avec des données Realm.
Observables Realm -> Flow (callbackFlow)
Si vous voulez exposer des résultats Realm réactifs (par ex. RealmResults),
utilisez callbackFlow pour attacher un RealmChangeListener sur le thread Realm
et émettre des listes immuables dans le flow. Important : fermez le Realm dans
awaitClose.
fun <T : RealmObject, R> observeRealmList(
query: () -> RealmResults<T>,
mapper: (T) -> R
): Flow<List<R>> = callbackFlow {
val realm = RealmHelper.getMonitoredInstance(RealmHelper.currentUserRealmConfiguration)
val results = query()
val listener = RealmChangeListener<RealmResults<T>> { snapshot ->
// Convertir en liste immuable et mapper en objets domaine
val list = snapshot.map { mapper(it) }
trySend(list).isSuccess
}
results.addChangeListener(listener)
// On s'assure que tout l'upstream tourne sur le dispatcher Realm
awaitClose {
results.removeChangeListener(listener)
realm.close()
}
}.flowOn(RealmDispatcher.dispatcher)
Explications :
flowOn(RealmDispatcher.dispatcher)force les emissions/Ă©coute Ă se produire sur le thread Realm (lâupstream), ce qui Ă©vite les erreurs liĂ©es au changement de thread cĂŽtĂ© Realm.- Le
mapperpermet de transformer le modĂšle Realm en modĂšle immuable/domaine.
Exposer dans un ViewModel en StateFlow
Dans un ViewModel, exposez un StateFlow immuable pour lâUI. Mettez Ă jour le
MutableStateFlow depuis un contexte Realm (ex : withRealmContext ou launchWithRealm).
class UsersViewModel : ViewModel() {
private val _users = MutableStateFlow<List<UserDto>>(emptyList())
val users: StateFlow<List<UserDto>> = _users.asStateFlow()
init {
// Exemple : une observation continue
viewModelScope.launch {
observeRealmList(
query = {
val realm = RealmHelper.getMonitoredInstance(RealmHelper.currentUserRealmConfiguration)
realm.where(User::class.java).findAllAsync()
},
mapper = { it.toDto() }
).collect { list ->
_users.value = list
}
}
}
// Exemple d'action qui modifie la DB
fun addUser(data: UserDto) {
viewModelScope.launchWithRealm { realm, _ ->
realm.executeTransaction { r ->
r.insertOrUpdate(data.toRealm())
}
}
}
}
Notes :
collectse déroule sur le scope du ViewModel. LeflowOnen amont garantit que les callbacks Realm ont été exécutés sur le bon thread.- Mettre
_users.value = listest thread-safe pour StateFlow si lâĂ©criture se fait depuis le main thread ; ici la collection peut se produire sur le thread courant du collecteur (souvent Main), mais les Ă©missions proviennent du dispatcher Realm.
SharedFlow pour les événements (one-shot)
Pour des événements ponctuels (navigation, snackbars), utilisez MutableSharedFlow
(ou Channel) et émettez depuis withRealmContext ou launchWithRealm.
class SomeViewModel : ViewModel() {
private val _events = MutableSharedFlow<UiEvent>()
val events = _events.asSharedFlow()
fun doSomething() {
viewModelScope.launchWithRealm { realm, _ ->
// modification DB
realm.executeTransaction { it.insertOrUpdate(...) }
// émettre un événement une fois la transaction réussie
_events.emit(UiEvent.ShowMessage("OK"))
}
}
}
Conseil : utilisez emit/tryEmit depuis la coroutine en cours. Si vous émettez
depuis le dispatcher Realm et que le collector attend sur Main, câest OK â
SharedFlow nâimpose pas le confinement de thread comme Realm, mais assurez-vous
que lâaccĂšs Ă Realm reste sur le dispatcher dĂ©diĂ©.
PiÚges fréquents et bonnes pratiques
- Ne crĂ©ez pas de dispatcher Realm Ă chaque appel â centralisez-le.
- Nâessayez pas de partager une instance Realm entre threads.
- Quand vous transformez
RealmObjecten modĂšles immuables, faites la copie sur le thread Realm (ou juste aprĂšs lâobtention) pour Ă©viter dâaccĂ©der aux objets live de Realm depuis un autre thread. - Si vous avez besoin dâun snapshot immuable : mappez les champs dans des DTO (objet data) et utilisez ces DTO hors du thread Realm.
- Attention aux opérateurs Flow qui peuvent changer le contexte. Si la logique
repose sur des callbacks Realm, appliquez
flowOn(RealmDispatcher.dispatcher)sur le flow qui crée/écoute les résultats.
Exemple complet (récapitulatif)
// Dispatcher partagé
val realmDispatcher = RealmDispatcher.dispatcher
// Helper usage
viewModelScope.launchWithRealm {
realm, _ ->
val users = realm.where(User::class.java).findAll()
// copier en DTO
}
// Observation
observeRealmList(
query = { realm.where(User::class.java).findAllAsync() },
mapper = { it.toDto() }
).onEach { list ->
// mettre Ă jour StateFlow
}.launchIn(viewModelScope)
En résumé
- â Isolez Realm sur un thread unique.
- â Centralisez le dispatcher pour Ă©viter la prolifĂ©ration de threads.
- â
Fermez toujours vos instances Realm (
use/close). - â
Pour du réactif : utilisez
callbackFlow+flowOn(realmDispatcher)et convertissez lesRealmObjecten DTO immuables. - â
StateFlowetSharedFlowsâintĂšgrent trĂšs bien si vous alimentez/Ă©mettez depuis un contexte Realm sĂ»r (helpers ci-dessus).