# Sircle Android — RxJava vs RxSwift, retour sur le portage d'une app réactive > Après la version iOS de Sircle en RxSwift, on a attaqué le portage Android en Java 8 avec RxJava 2. Cet article compare les deux implémentations : gestion du threading, bus d'événements, moteur physique, et les pièges spécifiques à chaque plateforme. Date : 15/05/2017 Auteur : Aurélien N. Tags : Android, RxJava, Java, Architecture, iOS --- Dans l'[article sur la version iOS](/blog/sircle-app-sociale-rxswift-realm-geolocalisation), j'ai détaillé l'architecture RxSwift de Sircle : les bulles animées, le `GeolocationService` réactif, le ViewModel qui combine des streams Realm en temps réel. Maintenant qu'on attaque le portage Android, c'est l'occasion de comparer les deux approches. Même app, même API backend, même spec fonctionnelle. Mais deux plateformes, deux langages, et deux façons assez différentes de faire du réactif. L'équipe Android (Bastien principalement) a fait des choix qui divergent de l'iOS sur plusieurs points, et c'est intéressant de comprendre pourquoi. ## Le stack Android On cible Android 4.4+ (API 19) en production, avec un fast-build flavor en API 21 pour le dev. Java 8 avec le compilateur Jack (pas de Kotlin sur ce projet, c'est encore tôt pour l'adopter en production). Le build Gradle est standard. Les dépendances principales : ```groovy // Reactive implementation 'io.reactivex.rxjava2:rxjava:2.0.1' implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' // Networking implementation 'com.squareup.retrofit2:retrofit:2.2.0' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.2.0' implementation 'com.squareup.retrofit2:converter-gson:2.2.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.6.0' // UI implementation 'com.github.Jawnnypoo:PhysicsLayout:2.1.0' implementation 'com.jakewharton:butterknife:8.8.1' implementation 'com.squareup.picasso:picasso:2.5.2' // Social & Chat implementation 'com.facebook.android:facebook-android-sdk:4.+' implementation 'com.twitter.sdk.android:twitter:2.3.2' implementation 'com.sendbird.sdk:sendbird-android-sdk:3.0.18' implementation 'com.google.firebase:firebase-messaging:10.2.0' ``` ## Le réseau : Retrofit vs Moya Côté iOS, on utilise Moya, une abstraction au-dessus d'Alamofire qui modélise l'API comme un `enum` Swift. Côté Android, c'est Retrofit 2, qui fait la même chose avec une interface Java et des annotations : ```java public interface SircleApiService { @POST("/api/v1/auth/facebook/login") Observable loginFacebook(@Body FacebookLogin facebookLogin); @GET("api/v1/users/me") Observable getMe(@Query("is_sign_up") String is_sign_up); @PATCH("api/v1/users/me") Observable patchMyLocation(@Body UserLocation user); @GET("api/v1/users/me/whos_around_me") Observable> getWhosAroundMe(); @GET("api/v1/users//posts") Observable> getUserPosts(@Path("otheruserid") String userId); @POST("api/v1/posts//like") Observable likePost(@Path("postId") String postId); } ``` Le `RxJava2CallAdapterFactory` fait que chaque endpoint retourne un `Observable`, exactement comme `Moya/RxSwift` côté iOS. Le `SircleClient` configure le tout : OkHttp avec un intercepteur pour le token Bearer, GSON pour la sérialisation, et des timeouts de 40 secondes : ```java public static SircleClient getInstance() { if (SIRCLE_REST_CLIENT == null) { synchronized (SircleClient.class) { if (SIRCLE_REST_CLIENT == null) } } return SIRCLE_REST_CLIENT; } ``` Le double-checked locking pour le singleton, c'est du Java classique. En Swift on a un `let` qui fait le boulot en une ligne. C'est un des points où la verbosité de Java se fait sentir. En iOS, Moya impose un `enum` par API avec un `switch` pour chaque propriété (path, method, parameters). C'est type-safe au compilateur : impossible d'oublier un case. Retrofit prend l'approche inverse : une interface avec des annotations. C'est moins strict (une typo dans le path compile sans erreur) mais c'est plus lisible quand l'API a beaucoup d'endpoints. ## Le vrai sujet : le threading C'est là que les deux mondes divergent le plus. En RxSwift, le `Driver` garantit qu'on émet sur le main thread. On ne se pose plus la question. En RxJava, il faut être explicite à chaque chaîne : ```java // Chaque appel réseau suit ce pattern mCompositeDisposable.add( SircleClient.getInstance().getApiService().getMe("") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::setupUser, this::handleRetrofitError) ); ``` `subscribeOn(Schedulers.io())` exécute la requête sur un thread du pool IO. `observeOn(AndroidSchedulers.mainThread())` switch sur le main thread pour le callback. Si tu oublies le `observeOn`, tu te retrouves à manipuler des vues depuis un thread background, et ça crash. Android est strict là-dessus : toucher une `View` hors du main thread, c'est une `CalledFromWrongThreadException` immédiate. En iOS, c'est plus permissif. UIKit ne crash pas si tu modifies une vue depuis un thread secondaire, il affiche un warning (depuis Xcode 9). Du coup on peut être plus laxiste, et le `Driver` de RxSwift corrige ça de manière élégante : c'est un `Observable` qui garantit main thread + pas d'erreur + replay de la dernière valeur. RxJava n'a pas d'équivalent direct. ## Le bus d'événements : RxManager vs Realm + RxRealm C'est la divergence architecturale la plus nette entre les deux versions. En iOS, la source de vérité est Realm. On écrit dans la base, RxRealm émet les changements, le ViewModel combine et filtre. C'est unidirectionnel : API → Realm → Observable → Driver → UI. En Android, pas de Realm. L'app utilise SharedPreferences pour persister l'utilisateur courant (un JSON sérialisé avec GSON), et un `RxManager` maison pour la communication entre composants : ```java public class RxManager { private static RxManager instance; private static UserLocationUpdate userLocationUpdate; private static VenueUpdate venueSelected; private static UserUpdate userUpdate; private static EmptyToken emptyToken; private static FirebaseTokenUpdate firebaseToken; private static SendBirdConnect sendBirdConnect; public static RxManager getInstance() { if (instance == null) return instance; } } ``` Chaque canal est un wrapper autour d'un `PublishSubject` : ```java public class UserLocationUpdate { private PublishSubject subject = PublishSubject.create(); public void setLocation(Location latLng) public Observable getUserLocation() } ``` C'est un bus d'événements typé. Le `LocationService` publie les positions, les fragments s'abonnent. Le `SessionManager` publie les changements de profil, les écrans se mettent à jour. Chaque `PublishSubject` a son type : `Location`, `Boolean`, `SircleUser`, `String`. Comparé à la version iOS, c'est plus direct mais moins robuste. RxRealm nous donnait la persistance et la réactivité en un seul mécanisme. Ici, les données transitent en mémoire via les subjects et sont persistées séparément dans SharedPreferences. Si l'app est tuée par le système, les subjects sont perdus. C'est un compromis : plus simple à mettre en place, moins de dépendances, mais pas de source de vérité unique. ## La géolocalisation : Bound Service vs singleton réactif Sur iOS, le `GeolocationService` est un singleton avec des `Observable` qui combinent CoreLocation et le venue pinning Foursquare. Tout est réactif de bout en bout. Sur Android, c'est un `Service` au sens Android du terme, bindé au cycle de vie de l'activity : ```java public class LocationService extends Service implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, LocationListener { private GoogleApiClient mGoogleApiClient; private LocationRequest mLocationRequest; private CompositeDisposable mCompositeDisposable; private Boolean isVenueSelected = false; @Override public void onCreate() @Override public void onLocationChanged(Location location) { if (previouslocation == null || !isVenueSelected || previouslocation.distanceTo(location) > 100) } } ``` Plusieurs choses intéressantes ici. D'abord, le service implémente trois interfaces de callback (`ConnectionCallbacks`, `OnConnectionFailedListener`, `LocationListener`). C'est le pattern Google Play Services : on connecte un `GoogleApiClient`, on attend le callback `onConnected`, et là on démarre les updates. Trois interfaces, cinq callbacks, pour faire ce que CoreLocation fait avec un `startUpdatingLocation()`. Ensuite, le pattern hybride. Le service reçoit les locations via un callback classique (`onLocationChanged`), puis publie via RxJava (`RxManager.getRxLocationUpdate().setLocation()`). C'est le pont entre l'ancien monde (callbacks) et le nouveau (Rx). Sur iOS, on avait écrit une extension `CLLocationManager.rx` pour tout garder en Rx. Ici, la couche Google Play Services est trop complexe pour être wrappée proprement, et le `Service` Android a son propre cycle de vie qui ne se prête pas bien à du pur Rx. Et le `CompositeDisposable` qu'il faut penser à `dispose()` dans `onDestroy()`. En iOS, le `DisposeBag` est libéré automatiquement par ARC quand l'objet qui le possède est désalloué. En Android, il faut le faire à la main. C'est une source de leaks si on oublie. ## Les bulles : JBox2D vs UIDynamicAnimator Sur iOS, les bulles utilisent `UIDynamicAnimator`, le moteur physique intégré à UIKit. Sur Android, pas d'équivalent natif. On a intégré [PhysicsLayout](https://github.com/Jawnnypoo/PhysicsLayout), une librairie qui wrappe JBox2D (un portage Java de Box2D) pour faire de la physique dans un `FrameLayout` : ```java public class CustomPhysicsFrameLayout extends FrameLayout implements Target { private CustomPhysics physics; private void init(AttributeSet attrs) @Override protected void onDraw(Canvas canvas) @Override public boolean onTouchEvent(@NonNull MotionEvent event) } ``` Le `CustomPhysics` encapsule le monde JBox2D. La simulation tourne de manière synchrone dans `onDraw()`, ce qui veut dire que chaque calcul physique bloque le main thread le temps du frame. Sur iOS, `UIDynamicAnimator` gère ça en interne avec `CADisplayLink` et c'est optimisé par Apple. Ici, quand il y a 30+ bulles avec des collisions, on sent la différence. On a dû limiter le nombre de bulles visibles plus agressivement que sur iOS. L'autre différence : sur iOS, chaque bulle a un `UISnapBehavior` qui l'attire vers le centre avec du damping. JBox2D n'a pas de concept équivalent, il faut simuler ça avec des forces appliquées à chaque step de la simulation. C'est plus de code, et le comportement n'est pas tout à fait le même : les bulles Android sont un peu moins "organiques" dans leur mouvement. ## Ce qui est pareil, ce qui ne l'est pas Le tableau honnête : | | iOS (Swift / RxSwift) | Android (Java / RxJava 2) | |---|---|---| | API réseau | Moya (enum typesafe) | Retrofit (interface annotée) | | Appels → Observable | Moya/RxSwift | RxJava2CallAdapterFactory | | Persistance locale | Realm + RxRealm | SharedPreferences + GSON | | Bus événements | Realm notifications | RxManager (PublishSubject) | | Thread safety | Driver (implicite) | observeOn/subscribeOn (explicite) | | Cleanup mémoire | DisposeBag (ARC) | CompositeDisposable (manuel) | | View binding | Code (SnapKit) | ButterKnife annotations | | Moteur physique | UIDynamicAnimator (natif) | JBox2D via PhysicsLayout | | Image loading | Kingfisher | Picasso | | Social login | SDK natifs | SDK natifs (identique) | | Chat | SendBird SDK | SendBird SDK (identique) | Le réseau et le chat sont quasi identiques. Les SDK SendBird et les SDK sociaux (Facebook, Twitter, LinkedIn) ont les mêmes APIs sur les deux plateformes, ce qui facilite le portage. L'écart se creuse sur la couche données (Realm vs SharedPrefs), le threading (implicite vs explicite), et le moteur physique (natif vs tiers). ## Les leçons du portage La plus grosse surprise, c'est la gestion du cycle de vie. Sur iOS, un `ViewController` est créé et détruit, point. Sur Android, un `Fragment` peut être créé, détruit, recréé par le système (rotation d'écran, mémoire faible), et il faut gérer le `SavedInstanceState` pour ne pas perdre l'état. Combiné avec RxJava, ça oblige à re-souscrire aux observables à chaque recréation, et à bien penser au moment où on dispose les subscriptions. Le pattern `RxManager` fonctionne, mais il montre ses limites quand les fragments se multiplient. Chaque fragment qui s'abonne doit penser à se désabonner dans `onDestroy()`. Sur iOS, le `DisposeBag` attaché au ViewController gère ça automatiquement. Ici, un oubli et c'est un leak mémoire. On a eu quelques cas en test. L'absence de Realm côté Android est un choix qu'on referait différemment. On l'a évité pour ne pas ajouter une dépendance lourde sur un projet qui devait aller vite. Mais sans source de vérité persistée, la gestion de l'état est fragile. Si l'app est tuée en arrière-plan et relancée, les listes d'utilisateurs WAM sont perdues et il faut refaire les appels API. Côté iOS, Realm conserve tout. Le compilateur Jack, on attend de pouvoir s'en débarrasser. Il est lent, il ne supporte pas toutes les features de Java 8, et Google pousse déjà D8/R8 comme remplaçant. Sur le prochain projet, si on passe à Kotlin, le problème disparaît. Malgré tout ça, RxJava reste le bon choix pour ce type d'app. Sans Rx, la coordination entre le service de localisation, les appels API, les mises à jour UI en temps réel et le chat SendBird serait un enfer de callbacks imbriqués. Le coût d'entrée est réel (le `subscribeOn`/`observeOn` à écrire partout, le `CompositeDisposable` à gérer), mais c'est le prix d'une base de code qui reste lisible quand on a six sources d'événements asynchrones.