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:
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?
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);
}
I would try to answer your first confusion.
There is really no need to pass around RealmObject
s 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.
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(() -> /*...*/);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With