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