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).