← Retour

Realm, Android, Thread et Coroutines

~5 min

Quelques bonnes pratiques Realm et Java sur Android pour ne pas devenir FOU !

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

  1. Isoler toutes les opérations Realm sur un thread unique.
  2. Toujours fermer les instances Realm (use / try-with-resources).
  3. Centraliser le dispatcher dédié à Realm pour réutiliser le même thread.
  4. 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 :

  • use garantit la fermeture de l’instance Realm même en cas d’exception.
  • En exposant RealmDispatcher.dispatcher vous é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 mapper permet 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 :

  • collect se déroule sur le scope du ViewModel. Le flowOn en amont garantit que les callbacks Realm ont été exécutés sur le bon thread.
  • Mettre _users.value = list est 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 RealmObject en 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 les RealmObject en DTO immuables.
  • StateFlow et SharedFlow s’intègrent très bien si vous alimentez/émettez depuis un contexte Realm sûr (helpers ci-dessus).