0

I am trying to combine retrofit 2 with RxJava and Realm, by saving the response of the service call returned by the retrofit observable to local database using realm. So i get an exception saying that Realm access from incorrect thread. Here is my code :

Trail 1:

restApi.userEntityList()
            .map(userEntityDataMapper::transformAllToRealm)
            .doOnNext(userRealmModels -> {
                if (userRealmModels != null){
                    mRealm = Realm.getInstance(mContext);
                    mRealm.asObservable()
                            .map(realm -> mRealm.copyToRealmOrUpdate(userEntity))
                            .subscribe(new Subscriber<Object>() {
                                @Override
                                public void onCompleted() {

                                }

                                @Override
                                public void onError(Throwable e) {
                                    e.printStackTrace();
                                }

                                @Override
                                public void onNext(Object o) {
                                    Log.d("RealmManager", "user added!");
                                }
                            });
                }})
            .map(userEntityDataMapper::transformAll)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Subscriber<List<User>>() {
                 @Override
                 public void onCompleted() {
                     hideViewLoading();
                 }

                 @Override
                 public void onError(Throwable e) {
                     hideViewLoading();
                     showErrorMessage(new DefaultErrorBundle((Exception) e));
                     showViewRetry();
                 }

                 @Override
                 public void onNext(List<User> users) {
                     showUsersCollectionInView(users);
                 }
            });

Trail 2:

restApi.userEntityList()
            .map(userEntityDataMapper::transformAllToRealm)
            .doOnNext(userRealmModels -> {
                if (userRealmModels != null) {
                    mRealm = Realm.getInstance(mContext);
                    mRealm.beginTransaction();
                    mRealm.copyToRealmOrUpdate(userEntity);
                    mRealm.commitTransaction();
                }
            })
            .map(userEntityDataMapper::transformAll)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Subscriber<List<User>>() {
                 @Override
                 public void onCompleted() {
                     hideViewLoading();
                 }

                 @Override
                 public void onError(Throwable e) {
                     hideViewLoading();
                     showErrorMessage(new DefaultErrorBundle((Exception) e));
                     showViewRetry();
                 }

                 @Override
                 public void onNext(List<User> users) {
                     showUsersCollectionInView(users);
                 }
            });

Logcat :

W/System.err: java.lang.IllegalStateException: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.
at io.realm.BaseRealm.checkIfValid(BaseRealm.java:349)
at io.realm.BaseRealm.commitTransaction(BaseRealm.java:291)
at io.realm.Realm.commitTransaction(Realm.java:108)
at com.zeyad.cleanarchitecturet.data.db.RealmManagerImpl.put(RealmManagerImpl.java:66)
at com.zeyad.cleanarchitecturet.data.db.RealmManagerImpl.putAll(RealmManagerImpl.java:91)
at com.zeyad.cleanarchitecturet.data.repository.datasource.CloudUserDataStore$2.call(CloudUserDataStore.java:36)
at com.zeyad.cleanarchitecturet.data.repository.datasource.CloudUserDataStore$2.call(CloudUserDataStore.java:32)
at rx.Observable$11.onNext(Observable.java:4445)
at rx.internal.operators.OperatorDoOnEach$1.onNext(OperatorDoOnEach.java:80)
at rx.internal.operators.OperatorMap$1.onNext(OperatorMap.java:54)
at rx.internal.operators.OperatorMerge$MergeSubscriber.emitScalar(OperatorMerge.java:477)
at rx.internal.operators.OperatorMerge$MergeSubscriber.tryEmit(OperatorMerge.java:435)
at rx.internal.operators.OperatorMerge$MergeSubscriber.onNext(OperatorMerge.java:228)
at rx.internal.operators.OperatorMerge$MergeSubscriber.onNext(OperatorMerge.java:142)
at rx.internal.operators.OperatorMap$1.onNext(OperatorMap.java:54)
at retrofit.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:113)
at retrofit.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:88)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable$2.call(Observable.java:162)
at rx.Observable$2.call(Observable.java:154)
at rx.Observable.unsafeSubscribe(Observable.java:8098)
at rx.internal.operators.OperatorSubscribeOn$1$1.call(OperatorSubscribeOn.java:62)
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
at rx.schedulers.ExecutorScheduler$ExecutorSchedulerWorker.run(ExecutorScheduler.java:98)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Caused by: rx.exceptions.OnErrorThrowable$OnNextValue: OnError while emitting onNext value: java.util.ArrayList.class
at rx.exceptions.Exceptions.throwOrReport(Exceptions.java:187)
at rx.internal.operators.OperatorDoOnEach$1.onNext(OperatorDoOnEach.java:82)
... 29 more
Tom Sabel
  • 3,935
  • 33
  • 45
Zeyad Gasser
  • 1,516
  • 21
  • 39
  • What does the methods `userEntityDataMapper::transformAllToRealm` and `userEntityDataMapper::transformAll` do? Also `showUsersCollectionInView(users);` doesn't seem to use any value from your stream? – Christian Melchior Mar 07 '16 at 09:56
  • The map(userEntityDataMapper::transformAllToRealm) is to convert the server response to a realm object to be saved localy. In the doOnNext() this realm object is saved. In the map(userEntityDataMapper::transformAll) the realm object is converted back to a POJO and sent to presentation layer. Should be received in the subscribe method to be added to the recyclerView in the showUsersCollectionInView(users). The problem is that realm throws an exception java.lang.IllegalStateException: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created – Zeyad Gasser Mar 07 '16 at 16:31
  • can you include the logcat? – Shmuel Mar 07 '16 at 17:27
  • It looks like the same issue as this one: http://stackoverflow.com/questions/35835858/realm-access-from-incorrect-thread-exception-while-a-copy-was-sent-using-copyfro . Using outer Realm instances inside an Observable operator is a bit dangerous as it is to easy to access it from the wrong thread. In your case you are accessing the UI mRealm on the `schedulers.io()` thread. – Christian Melchior Mar 08 '16 at 08:01

2 Answers2

1

I found out how to fix it. To save server responses from retrofit 2 to realm then pass to the UI. I made retrofit return realm objects by overriding the Gson Builder like this:

Retrofit.Builder()
            .baseUrl(RestApi.API_BASE_URL)
            .client(okHttpClient)
            .callbackExecutor(new JobExecutor())
            .addConverterFactory(GsonConverterFactory.create(new GsonBuilder()
                    .setExclusionStrategies(new ExclusionStrategy() {
                        @Override
                        public boolean shouldSkipField(FieldAttributes f) {
                            return f.getDeclaringClass().equals(RealmObject.class);
                        }

                        @Override
                        public boolean shouldSkipClass(Class<?> clazz) {
                            return false;
                        }
                    }).create()))
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .build();

Then:

restApi.userRealmList()
    .doOnNext(userRealmModels -> {
        if (userRealmModels != null) {
            Realm realm = Realm.getInstance(mContext);
            realm.beginTransaction();
            realm.copyToRealmOrUpdate(userRealmModels);
            realm.commitTransaction();
            realm.close();
        }})
    .map(userEntityDataMapper::transformAll)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Subscriber<List<User>>() {
         @Override
         public void onCompleted() {
             hideViewLoading();
         }

         @Override
         public void onError(Throwable e) {
             hideViewLoading();
             showErrorMessage(new DefaultErrorBundle((Exception) e));
             showViewRetry();
         }

         @Override
         public void onNext(List<User> users) {
             showUsersCollectionInView(users);
         }
    });
Zeyad Gasser
  • 1,516
  • 21
  • 39
0

I think you have created Realm objects using main thread and you are using it in another or background thread. If so, then use them in the same thread they are created. i.e.

((Activity)mContext).runOnUiThread(new Runnable() {
   @Override
   public void run() {
        // You code of realm goes here
        if (userRealmModels != null) {
               mRealm = Realm.getInstance(mContext);
               mRealm.beginTransaction();
               mRealm.copyToRealmOrUpdate(userEntity);
               mRealm.commitTransaction();
         }
   }
});
Brijesh Chopda
  • 195
  • 2
  • 11