Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper way to abstract Realm in Android apps

I'm trying out Realm.io in an Android app, though, to stay on the safe side, I would like to abstract the DB layer so that, in case of need, I can switch back to a standard SQLite based DB without rewriting most of the app.

I'm however finding it difficult to properly abstract Realm due to it's particular nature:

  • When tied to a realm, RealmObjects are proxies so I cannot pass them around as they were POJOs.
  • All Realm instances need to be properly opened and closed for every thread they are used in.

I've resorted to using the recent Realm.copyFromRealm() API instead of passing around RealmObjects tied to a Realm to get around these limitations but this way I think I'm loosing all the benefits of using realm (am I?).

Any suggestions?

like image 994
Marco Romano Avatar asked Dec 26 '15 17:12

Marco Romano


3 Answers

With the latest Google I/O 2017 announcement for Android Architectural Components, the proper way to abstract Realm in Android apps is:

1.) Realm instance lifecycle is managed by ViewModel class, and it is closed in onCleared() method

2.) RealmResults is a MutableLiveData<List<T>>, so you can create a RealmLiveData<T> class which wraps a RealmResults<T>.

Therefore, you can create a view model like this:

// based on  https://github.com/googlesamples/android-architecture-components/blob/178fe541643adb122d2a8925cf61a21950a4611c/BasicSample/app/src/main/java/com/example/android/persistence/viewmodel/ProductListViewModel.java
public class ProductListViewModel {
    private final MutableLiveData<List<ProductEntity>> observableProducts = new MutableLiveData<>();

    Realm realm;
    RealmResults<ProductEntity> results;
    RealmChangeListener<RealmResults<ProductEntity>> realmChangeListener = (results) -> {
        if(results.isLoaded() && results.isValid()) { // you probably don't need this, just making sure.
            observableProducts.setValue(results);
        }
    };

    public ProductListViewModel() {
        realm = Realm.getDefaultInstance();             
        results = realm.where(ProductEntity.class).findAllSortedAsync("id"); 
          // could use a Realm DAO class here
        results.addChangeListener(realmChangeListener);

        observableProducts.setValue(null); // if using async query API, the change listener will set the loaded results.
    }

    public LiveData<List<ProductEntity>> getProducts() {
        return observableProducts;
    }

    @Override
    protected void onCleared() {
        results.removeChangeListener(realmChangeListener);
        realm.close();
        realm = null;
    }
}

or you can separate them into a realm viewmodel and a realm livedata based on this article:

public class LiveRealmData<T extends RealmModel> extends LiveData<RealmResults<T>> {

    private RealmResults<T> results;
    private final RealmChangeListener<RealmResults<T>> listener = 
        new RealmChangeListener<RealmResults<T>>() {
            @Override
            public void onChange(RealmResults<T> results) { setValue(results);}
    };

    public LiveRealmData(RealmResults<T> realmResults) {
        results = realmResults;
    }

    @Override
    protected void onActive() {
        results.addChangeListener(listener);
    }

    @Override
    protected void onInactive() {
        results.removeChangeListener(listener);
    }
}

public class CustomResultViewModel extends ViewModel {

    private Realm mDb;
    private LiveData<String> mLoansResult;

    public CustomResultViewModel() {
        mDb = Realm.getDefaultInstance();
        mLoansResult = RealmUtils.loanDao(mDb).getAll();
    }

    public LiveData<String> getLoansResult() {
        return mLoansResult;
    }

    @Override
    protected void onCleared() {
        mDb.close();
        super.onCleared();
    }
}

Either way, you've wrapped Realm's auto-updating and lazy-loaded result set into a LiveData and ViewModel, separate from the fragments/adapters:

// based on https://github.com/googlesamples/android-architecture-components/blob/178fe541643adb122d2a8925cf61a21950a4611c/BasicSample/app/src/main/java/com/example/android/persistence/ProductListFragment.java
public class ProductListFragment extends LifecycleFragment {
    private ProductAdapter productAdapter;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
        //...
        productAdapter = new ProductAdapter(mProductClickCallback);
        //...
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final ProductListViewModel viewModel =
                ViewModelProviders.of(this).get(ProductListViewModel.class); // <-- !

        subscribeUi(viewModel);
    }

    private void subscribeUi(ProductListViewModel viewModel) {
        // Update the list when the data changes
        viewModel.getProducts().observe(this, (myProducts) -> {
            if (myProducts == null) {
                // ...
            } else {
                productAdapter.setProductList(myProducts);
                //...
            }
        });
    }
}

But if you are not using Android Architectural Components, even then what one needs to keep in mind is that:

RealmResults is a list of proxy objects that mutates in place, and it has change listeners.

So what you need is either wrapping it as Flowable with LATEST backpressure, akin to

private io.reactivex.Flowable<RealmResults<T>> realmResults() {
    return io.reactivex.Flowable.create(new FlowableOnSubscribe<RealmResults<T>>() {
        @Override
        public void subscribe(FlowableEmitter<RealmResults<T>> emitter)
                throws Exception {
            Realm observableRealm = Realm.getDefaultInstance();
            RealmResults<T> results = realm.where(clazz)./*...*/.findAllSortedAsync("field", Sort.ASCENDING);
            final RealmChangeListener<RealmResults<T>> listener = _results -> {
                if(!emitter.isDisposed()) {
                    emitter.onNext(_results);
                }
            };
            emitter.setDisposable(Disposables.fromRunnable(() -> {
                observableRealm.removeChangeListener(listener);
                observableRealm.close();
            }));
            observableRealm.addChangeListener(listener);
            emitter.onNext(observableRealm);
        }
    }, BackpressureStrategy.LATEST).subscribeOn(scheduler).unsubscribeOn(scheduler);

Or creating your own MutableLiveList interface.

public interface MutableLiveList<T> extends List<T> { 
     public interface ChangeListener {
         void onChange(MutableLiveList<T> list);
     }

     void addChangeListener(ChangeListener listener);
     void removeChangeListener(ChangeListener listener);
}
like image 60
EpicPandaForce Avatar answered Nov 15 '22 17:11

EpicPandaForce


I would try to answer your first confusion. There is really no need to pass around RealmObjects via intents, or in other terms no need of them being parcelable or serializable

What you should do is to pass around particular primary id or other parameter of that particular RealmObject via intent, so that you can query that RealmObject again in next activity.

For example suppose you pass primaryId while starting an activity,

Intent intent = new Intent(this, NextActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_PRIMARY_ID, id);

Now inside NextActivity get id from intent and simply query RealmObject, i.e.,

int primaryId = getIntent().getIntExtra(Constants.INTENT_EXTRA_PRIMARY_ID, -1);
YourRealmObject mObject = realm.where(YourRealmObject.class).equalTo("id", primaryId).findFirst();

See, you got your object in new activity, rather than worrying about passing the object around.

I don't know what particular problem you're getting into regarding opening and closing realm objects. Did you try Realm's Transaction blocks? You don't need to rely on opening and closing if you use that.

I hope this helps.

Update

Since you're looking for abstraction,

Just create a class like DbUtils and have a method there like getYourObjectById and then pass above id and retrieve your object in return. That makes it abstract in a way. You can then keep that class and method, and just change method content if you ever switch to another database solution.

like image 24
Jigar Avatar answered Nov 15 '22 15:11

Jigar


Providing a different opinion, since I down-voted the other answer. I would like to first discuss your objective

in case of need, I can switch back to a standard SQLite based DB without rewriting most of the app

Realm is not equivalent to a standard SQLite-based DB. What you are looking for in that case is something like StorIO. Realm is designed in such a way that you have to use RealmObject as base for your models. If you just want to retrieve data from a table, Realm is not for you.

Now, after the Parse debacle, the need for abstracting an third-party library became apparent. So, if that's your motivation, then you really have to wrap every single thing you use from Realm. Something like this:

public class Db {

  public class Query<M extends RealmObject> {

    private final RealmQuery<M> realmQuery;

    Query(Db db, Class<M> clazz) {
        this.realmQuery = RealmQuery.createQuery(db.realmInstance, clazz);
    }

    public Query<M> equalTo(String field, Integer value) {
        realmQuery.equalTo(field, value);
        return this;
    }

    public Results<M> findAll() {
        return new Results<>(realmQuery.findAll());
    }

    public M findFirst() {
        return realmQuery.findFirst();
    }
  }

  public class Results<M extends RealmObject> extends AbstractList<M> {

    private final RealmResults<M> results;

    Results(RealmResults<M> results) {
        this.results = results;
    }

    public void removeLast() {
        results.removeLast();
    }

    @Override
    public M get(int i) {
        return results.get(i);
    }

    @Override
    public int size() {
        return results.size();
    }
  }

  public interface Transaction {
    void execute();

    abstract class Callback {
        public abstract void onSuccess();

        public void onError(Throwable error) {
        }
    }
  }

  private Realm realmInstance;

  Db() {
    realmInstance = Realm.getDefaultInstance();
  }

  public void executeTransaction(@NonNull Db.Transaction dbTransaction) {
    realmInstance.executeTransaction(realm -> dbTransaction.execute());
  }

  public void close() {
    realmInstance.close();
  }
}

At this point, you just define whichever operations you are using in your app on the Db class. And, if you are like me, you are probably not using all of them, so you should be fine.

Is this pretty? No. Is it necessary? Depends on what you want to achieve.

Edit: for bonus point, have Db implement AutoCloseable:

try(Db db = new Db()) {
     db.executeTransaction(() -> /*...*/);
}
like image 30
verybadalloc Avatar answered Nov 15 '22 16:11

verybadalloc