Rxjava - isolation of logic - android

Basically , I want to check if i have data in my DB, and if i dont have, make an api call. I'm using this logic for making the request to the API:
private void requestDataToApi() {
mSubscribe = createRequest()
.delay(DELAY_SPLASH_SCREEN_SECONDS, TimeUnit.SECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(categoryModels -> {
writeDataToDb(categoryModels);
}, (throwable -> {
dealError();
}));
}
And this logic to verify if there any data stored:
if (mRealm.where(CategoryModel.class).findAll().size() == 0) {
requestDataToApi();
} else {
getView().openMainActivity(readDataFromDb());
}
There is any way to join this both logics? Basically, be the dispose verifying the db and just make the call if needed?

You can use filter and switchIfEmpty operator
#Test
public void ifEmpty() throws InterruptedException {
Observable.just(getDataFromDatabase())
.filter(value -> !value.isEmpty())
.switchIfEmpty(Observable.just("No data in database so I go shopping"))
.subscribe(System.out::println);
}
private String getDataFromDatabase() {
if(new Random().nextBoolean()){
return "data";
}
return "";
}
You can learn more from reactive world here https://github.com/politrons/reactive

Looks like you need the Repository Pattern
What this pattern does it isolate the business logic from the data origin. So you just ask for data and don'r care where this data come from. So you could hava something like:
public class CategoryModelRepo {
public Observable<CategoryModel> getAll() {
return Observable.defer(() -> {
List<CategoryModel> fromRealm = mRealm.where(CategoryModel.class).findAll();
if (fromRealm.size() == 0) {
return requestDataToApi()
.onNext(dataList -> storeDataInRealm(dataList))
} else {
return Observable.just(fromRealm);
}
}
}
// This code fragment could be improved by applying a DAO pattern
// http://www.oracle.com/technetwork/java/dataaccessobject-138824.html
private Observable<CategoryModel> requestDataToApi() {
return createRequest()
.delay(DELAY_SPLASH_SCREEN_SECONDS, TimeUnit.SECONDS)
}
So from your business layer (or, in your case, view layer) you can load the data to ensure it has been stored locally.
Don't forget to use .subscribeOn(...) and .observeOn(...) where necessary

If you are willing to add one more dependency to your project, Store is a (very) nice solution. Otherwise, I would recommend using concepts from the Repository pattern.

Related

Chaining and mapping kotlin flow

I'm trying to get a result from a flow, that retrieves a list from a room database, and then trying to map the list with another flow inside from another database operation, but I don't know if it is possible and if it is, how to make it, at this time I'm trying to make something like this
fun retrieveOperationsWithDues(client: Long): Flow<List<ItemOperationWithDues>> {
return database.operationsDao.getOperationCliente(client)
.flatMapMerge {
flow<List<ItemOperationWithDues>> {
it.map { itemOperation ->
database.duesDao.cuotasFromOperation(client, itemOperation.id).collectLatest { listDues ->
itemOperation.toItemOperationWithDues(listDues)
}
}
}
}
}
but looks like is not retrieving anything from the collect. Thanks in advice for any help
I think you don't need to use flow builder in flatMapMerge block. For each itemOperation you can call the cuotasFromOperatio() function from the Dao, which returns Flow and use combine() to combine retrieved flows:
fun retrieveOperationsWithDues(client: Long): Flow<List<ItemOperationWithDues>> {
return database.operationsDao.getOperationCliente(client)
.flatMapMerge {
val flows = it.map { itemOperation ->
database.duesDao.cuotasFromOperation(client, itemOperation.id).map { listDues ->
itemOperation.toItemOperationWithDues(listDues)
}
}
combine(flows) { flowArray -> flowArray.toList() }
}
}

Is there a better way to implement single source of truth with rxjava in android

In my app I have database which uses Room and a network service using retrofit. I have a requirement where if there is no data in local database I need to query the network and show a progress bar. If the network returns empty data then I need to show a empty view. One of the problem is that I need to ignore the empty data from the room and only consider empty data from the server so that when the user doesn't have any data he just sees a loading view and after the server returns empty data he will see empty view.
I have implemented this using a publish subject. Lce(loading content error) is wrapper object around data.
val recentPublish = PublishSubject.create<Lce<List<RecentMessage>>>()
fun loadRecentMessages() {
loadMessageFromDB()
loadRecentMessageFromServer()
}
private fun loadMessageFromDB() {
disposable = recentMessageDao.getRecentMessages() // this is a flowable
.subscribeOn(Schedulers.io())
.subscribe({
Timber.d("recent message from db size ${it.size}")
handleMessageFromDB(it)
}, {
it.printStackTrace()
Timber.e("error on flowable from db!")
})
}
protected fun handleMessageFromDB(messages: List<RecentMessage>) {
// only publish if the data is not empty
if (messages.isNotEmpty()) {
recentPublish.onNext(Lce.Content(messages))
}
}
private fun loadRecentMessageFromServer() {
recentPublish.onNext(Lce.Loading())
networkService.getLatestMessage() // this is a single
.subscribe({
val parsedMessages =
DtoConverter.convertRecentPrivateMessageResponse(it, user.id!!)
handleMessageFromServer(parsedMessages)
}, {
it.printStackTrace()
recentPublish.onNext(Lce.Error(it))
Timber.w("failed to load recent message for private chat from server")
})
}
private fun handleMessageFromServer(recentMessages: List<RecentMessage>) {
Timber.i("recent messages from server ${recentMessages.size}")
if (recentMessages.isEmpty()) {
recentPublish.onNext(Lce.Content(arrayListOf()))
} else {
recentMessageDao.saveAll(recentMessages)
}
}
In the above code I am only passing the empty data from server and ignoring the empty data from room. This solution works but I wonder if there is some better functional approach to solve this problem. I am a beginner to Rxjava and any help will be appreciated. Thank you.
After some research and the comment from #EpicPandaForce, I came up with this approach. I learned quite few things and it just clicked on me, about how to correctly use rxjava. Here is my approach, any comment will be appreciated.
fun getMessages(): Observable<Lce<List<RecentMessage>>> {
return Observable.mergeDelayError(getMessagesFromDB(), getMessagesFromNetwork()) // even if network fails, we still want to observe the DB
}
private fun getMessagesFromDB(): Observable<Lce.Content<List<RecentMessage>>> {
return recentMessageDao.getRecentMessages()
.filter {
it.isNotEmpty() // only forward the data from db if it's not empty
}.map {
Lce.Content(it)
}
}
private fun getMessagesFromNetwork(): Observable<Lce<List<RecentMessage>>> {
// first show a loading , then request for data
return Observable.concat(Observable.just(Lce.Loading()), profileService.getLatestMessage()
.flatMap {
processServerResponse(it) // store the data to db
}.onErrorReturn {
Lce.Error(it)
}.filter {
(it as Lce.Content).packet.isEmpty() // only forward data if it's empty
})
}
private fun processServerResponse(response: RecentMessageResponse): Observable<Lce<List<RecentMessage>>> {
return Observable.create {
val parsedMessages =
DtoConverter.convertRecentPrivateMessageResponse(response, user.id!!)
handleMessageFromServer(parsedMessages)
it.onComplete() // we use single source of truth so don't return anyting
}
}

Best way to get List from Observable in Rxjava

I'm just exploring Rxjava in one of my android application, and got stuck at one place, honestly speaking I'm very new to this library so don't mind if my question frustrate someone;-)
So I'm trying to access the Room Database using RxJava where I'm returning the Observable List, once I get this Observable I'm trying to use map operator to get a list of ids & query again the database, which again returns me the Observable List but the map operator expects List as a return type. How can I tackle this please suggest?
Below is the code snippet:
private void getAllPcbs() {
isLoading.setValue(true);
getCompositeDisposable().add(
getRepositoryManager().loadAllPcbDetails()
.flatMap((Function<List<PcbDetails>, ObservableSource<?>>) pcbDetails -> {
List<Long> pcbList = new ArrayList<>();
for (PcbDetails details : pcbDetails)
pcbList.add(details.getPcbId());
return getRepositoryManager().loadAllPcbs(pcbList);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onSuccess, this::onError)
);
}
private void onError(Throwable throwable) {
isLoading.setValue(false);
}
private void onSuccess(Object o) {
isLoading.setValue(false);
pcbList.setValue((List<Pcb>) o);
}
public interface DbHelper {
Observable<List<PcbDetails>> loadAllPcbDetails();
Observable<List<Pcb>> loadAllPcbs(List<Long> pcbIdList);
}
Go like
getRepositoryManager().loadAllPcbDetails()
.flatMapIterable {
listPcbDetail-> listPcbDetail
// listPcbDetail is ArrayList<PcbDetails>
// Converts your list of ids into an Observable
// which emits every item in the list
}
.flatMap { pcbDetail ->
// pcbDetail is PcbDetails
getRepositoryManager().loadAllPcbs(pcbDetail.pcbIdList)
}.subscribe { listPcb ->
// listPcb is ArrayList<Pcb>
}

Android Room with RxJava handle empty query result

Trying to test new Android Room librarty with RxJava adapter. And I want to handle result if my query returns 0 objects from DB:
So here is DAO method:
#Query("SELECT * FROM auth_info")
fun getAuthInfo(): Flowable<AuthResponse>
And how I handle it:
database.authDao()
.getAuthInfo()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.switchIfEmpty { Log.d(TAG, "IS EMPTY") }
.firstOrError()
.subscribe(
{ authResponse -> Log.d(TAG, authResponse.token) },
{ error -> Log.d(TAG, error.message) })
My DB is empty, so I expect .switchIfEmty() to work, but none of handling methods is firing. Neither .subscribe() nor .switchIfEmpty()
Db Flowables are observable (so they keep dispatching if database changes) so it never completes. You can try returning List<AuthResponse>. We've considered back porting an optional but decided not to do it, at least for now. Instead, we'll probably add support for Optional in different known libraries.
In version 1.0.0-alpha5, room added support of Maybe and Single to DAOs, so now you can write something like
#Query("SELECT * FROM auth_info")
fun getAuthInfo(): Maybe<AuthResponse>
You can read more about it here
switchIfEmpty takes as parameter a Publisher<AuthResponse>. Through SAM-conversion your given anonymous function is turned into this class. However it does not follow the behavior expected from a Publisher so it will not work as expected.
Replace it with a correct implementation like Flowable.empty().doOnSubscribe { Log.d(TAG, "IS EMPTY") } and it should work.
You could use some wrapper for result. For example:
public Single<QueryResult<Transaction>> getTransaction(long id) {
return createSingle(() -> database.getTransactionDao().getTransaction(id))
.map(QueryResult::new);
}
public class QueryResult<D> {
public D data;
public QueryResult() {}
public QueryResult(D data) {
this.data = data;
}
public boolean isEmpty(){
return data != null;
}
}
protected <T> Single<T> createSingle(final Callable<T> func) {
return Single.create(emitter -> {
try {
T result = func.call();
emitter.onSuccess(result);
} catch (Exception ex) {
Log.e("TAG", "Error of operation with db");
}
});
}
And use it like 'Single' in this case you will get result in any case. Use:
dbStorage.getTransaction(selectedCoin.getId())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(r -> {
if(!r.isEmpty()){
// we have some data from DB
} else {
}
})

Correct flow in RxJava with Retrofit and Realm

I'm implementing network API with the combination of RxJava and Retrofit, and I use Realm as my database. I got it pretty much working but I'm wondering if it is the correct approach and flow of events. So, here is the RetrofitApiManager.
public class RetrofitApiManager {
private static final String BASE_URL = "***";
private final ShopApi shopApi;
public RetrofitApiManager(OkHttpClient okHttpClient) {
// GSON INITIALIZATION
Retrofit retrofit = new Retrofit.Builder()
.client(okHttpClient)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl(BASE_URL)
.build();
shopApi = retrofit.create(ShopApi.class);
}
public Observable<RealmResults<Shop>> getShops() {
return shopApi.getShops()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(response -> {
Realm realm = Realm.getDefaultInstance();
realm.executeTransaction(realm1 ->
realm1.copyToRealmOrUpdate(response.shops));
realm.close();
})
.flatMap(response -> {
Realm realm = Realm.getDefaultInstance();
Observable<RealmResults<Shop>> results = realm.where(Shop.class)
.findAllAsync()
.asObservable()
.filter(RealmResults::isLoaded);
realm.close();
return results;
});
}
}
And here is the call to get RealmResults<Shop> inside a Fragment.
realm.where(Shop.class)
.findAllAsync()
.asObservable()
.filter(RealmResults::isLoaded)
.first()
.flatMap(shops ->
shops.isEmpty() ? retrofitApiManager.getShops() : Observable.just(shops))
.subscribe(
shops -> initRecyclerView(),
throwable -> processError(throwable));
Here are my questions:
Is it a correct approach to chain events like in the example above or should I manage them in a different way?
Is it OK to useRealm instance in getShops() method and close i there or would it be better to pass it as an argument and then manage it somehow? Although, this idea seems to be a bit problematic with threads and calling Realm.close() always at the right time.
1) I would try to do as much as possible on the background thread, right now you are doing a lot of the work on the UI thread.
2)
public Observable<RealmResults<Shop>> getShops() {
return shopApi.getShops()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(response -> {
try(Realm realm = Realm.getDefaultInstance()) {
realm.executeTransaction(realm1 ->
realm1.insertOrUpdate(response.shops));
} // auto-close
})
.flatMap(response -> {
try(Realm realm = Realm.getDefaultInstance()) {
Observable<RealmResults<Shop>> results = realm.where(Shop.class)
.findAllAsync()
.asObservable()
.filter(RealmResults::isLoaded);
} // auto-close
return results;
});
}
All Realm data is lazy-loaded, so it is only available while the Realm instance is open, so closing it after retrieving it has a high chance of not working. In your case though you are flat-mapping on the main thread, so most likely there is already an open instance there.
If you want you can use copyFromRealm() to get unmanaged data out that can be moved across threads and are not connected to Realm anymore, but they will also loose their live update features and take up more memory.
It would probably do this instead:
public Observable<RealmResults<Shop>> getShops() {
return shopApi.getShops()
.subscribeOn(Schedulers.io())
.doOnNext(response -> {
try(Realm realm = Realm.getDefaultInstance()) {
realm.executeTransaction(realm1 ->
realm1.copyToRealmOrUpdate(response.shops));
} // auto-close
})
.observeOn(AndroidSchedulers.mainThread())
.flatMap(response -> {
Observable<RealmResults<Shop>> results = realm.where(Shop.class)
.findAllAsync()
.asObservable()
.filter(RealmResults::isLoaded);
return results;
});
Alternatively you can treat the network request as a side-effect and just depend on Realm notifying you when there is changes (better approach IMO as you separate network from DB access which is e.g. what the Repository pattern is about)
public Observable<RealmResults<Shop>> getShops() {
// Realm will automatically notify this observable whenever data is saved from the network
return realm.where(Shop.class).findAllAsync().asObservable()
.filter(RealmResults::isLoaded)
.doOnNext(results -> {
if (results.size() == 0) {
loadShopsFromNetwork();
}
});
}
private void loadShopsFromNetwork() {
shopApi.getShops()
.subscribeOn(Schedulers.io())
.subscribe(response -> {
try(Realm realm = Realm.getDefaultInstance()) {
realm.executeTransaction(r -> r.insertOrUpdate(response.shops));
} // auto-close
});
}
What Christian Melchior mentioned in his answer, makes perfect sense, and should solve the problem you are having at your hand, but down the line this approach may introduce other issue(s).
In a good architecture, all the major modules(or libraries) should be isolated from rest of the code. Since Realm, RealmObject or RealmResult can not be passed across threads it is even more important to make Realm & Realm related operations isolated from rest of the code.
For each of your jsonModel class, you should have a realmModel class and a DAO (Data Access Object). Idea here is that other than DAO class none of the class must know or access realmModel or Realm. DAO class takes jsonModel, converts to realmModel, performs read/write/edit/remove operations, for read operations DAO converts realmModel to jsonModel and returns with it.
This way it is easy to maintain Realm, avoid all Thread related issues, easy to test and debug.
Here is an article about Realm best practices with a good architechture https://medium.com/#Viraj.Tank/realm-integration-in-android-best-practices-449919d25f2f
Also a sample project demonstrating Integration of Realm on Android with MVP(Model View Presenter), RxJava, Retrofit, Dagger, Annotations & Testing. https://github.com/viraj49/Realm_android-injection-rx-test
In my case, I seem to have defined a query for the RealmRecyclerViewAdapter like this:
recyclerView.setAdapter(new CatAdapter(getContext(),
realm.where(Cat.class).findAllSortedAsync(CatFields.RANK, Sort.ASCENDING)));
And otherwise defined a condition for Retrofit with RxJava to download more stuff when the condition is met:
Subscription downloadCats = Observable.create(new RecyclerViewScrollBottomOnSubscribe(recyclerView))
.filter(isScrollEvent -> isScrollEvent || realm.where(Cat.class).count() <= 0)
.switchMap(isScrollEvent -> catService.getCats().subscribeOn(Schedulers.io())) // RETROFIT
.retry()
.subscribe(catsBO -> {
try(Realm outRealm = Realm.getDefaultInstance()) {
outRealm.executeTransaction((realm) -> {
Cat defaultCat = new Cat();
long rank;
if(realm.where(Cat.class).count() > 0) {
rank = realm.where(Cat.class).max(Cat.Fields.RANK.getField()).longValue();
} else {
rank = 0;
}
for(CatBO catBO : catsBO.getCats()) {
defaultCat.setId(catBO.getId());
defaultCat.setRank(++rank);
defaultCat.setSourceUrl(catBO.getSourceUrl());
defaultCat.setUrl(catBO.getUrl());
realm.insertOrUpdate(defaultCat);
}
});
}
}, throwable -> {
Log.e(TAG, "An error occurred", throwable);
});
And this is for example a search based on an edit text's input:
Subscription filterDogs = RxTextView.textChanges(editText)
.switchMap((charSequence) ->
realm.where(Dog.class)
.contains(DogFields.NAME, charSequence.toString())
.findAllAsyncSorted(DogFields.NAME, Sort.ASCENDING)
.asObservable())
.filter(RealmResults::isLoaded)
.subscribe(dogs -> realmRecyclerAdapter.updateData(dogs));

Categories

Resources