I'm using RxJava and Realm database in an android project. But sometimes pressing a button is unresponsive and you have to do that many times for it to work sometime, and android log is saying xxx frame skipped. I know it has something to do with misusing UI thread. Here's some of my request, can someone tell me what's wrong with them? Realm wants me to perform IO request on the same thread I'm using the response(not too sure though).
public Flowable<List<ClothingItem>> getClothingItemsLocal() {
return Flowable.just(dbProvider.getClothingItems(mSortType));
}
public Flowable<List<ClothingItem>> getClothingItemsRemote() {
return clothingService.getAll("Bearer " + preferencesManager.getToken())
.map(response -> response.items)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(clothingItems -> {
dbProvider.clearClothingItems();
dbProvider.saveOrUpdateClothingItems(clothingItems);
})
.toFlowable()
.map(remoteItems -> dbProvider.getClothingItems(mSortType));
}
public Flowable<ClothingItem> getClothingItem(#NonNull final String id) {
return getClothingItemRemote(id)
.startWith(dbProvider.getClothingItem(id))
.onErrorReturn(throwable -> dbProvider.getClothingItem(id));
}
getAll method with retrofit.
#GET(BuildConfig.BASE_API_PATH + "clothing_items")
Single<GetClothingItemsResponseModel> getAll(#Header("Authorization") String token);
Realm provider methods:
public void saveOrUpdateEvents(List<Event> data) {
realmInstance.executeTransaction(realm -> {
for (Event event : data) {
if (!TextUtils.isEmpty(event.date)) {
Date date = DateUtils.getFullDate(event.date);
Timber.d("date %s", date.toString());
event.timestamp = date;
}
Event cashedEvent = getEvent(event.id);
if (cashedEvent.id != null) {
event.eventClothingItems = cashedEvent.eventClothingItems;
event.tags = cashedEvent.tags;
event.location = cashedEvent.location;
}
}
realm.delete(Event.class);
realm.insertOrUpdate(data);
});
}
public void clearClothingItems() {
realmInstance.executeTransaction(realm -> {
realm.delete(ClothingItem.class);
});
}
Try this:
public Flowable<List<ClothingItem>> getClothingItemsRemote() {
return clothingService.getAll("Bearer " + preferencesManager.getToken())
.subscribeOn(Schedulers.io())
.map(response -> response.items)
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(clothingItems -> {
dbProvider.clearClothingItems();
dbProvider.saveOrUpdateClothingItems(clothingItems);
})
.observeOn(Schedulers.computation())
.toFlowable()
.map(remoteItems -> dbProvider.getClothingItems(mSortType));
}
Related
I have a Observable with a chained Completeable with a chained Single that is called at an interval of 5 seconds.
public void getCoinPrices() {
disposable = Observable
.interval(5, TimeUnit.SECONDS)
.flatMapCompletable(n -> {
Timber.d("Called flatmap completeable: " + n);
boolean isFirstTime = sharedPrefManager.isFirstTimeOpeningApp();
if (isFirstTime) {
Timber.d("Is first time.");
return insertFavoritesUseCase.insertStartingCoins()
.andThen(Completable.fromAction(() -> {
sharedPrefManager.setFirstTimeOpeningApp(false);
}));
} else {
Timber.d("Not first time.");
return Completable.complete();
}
})
.andThen(getFavoritesUseCase.getFavoriteCoins())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(listFavorites -> {
Timber.d("List favorites called...");
return getCoinsUseCase.getCoinPrices(listFavorites);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(listCoinsWithPrice -> {
Timber.d("Called.......");
}, err -> {
Timber.e("Failed to get price of coins: " + err);
});
}
I can't figure out why the .andThen(getFavoritesUseCase.getFavoriteCoins()) is never fired.
public Single<List<CoinStatus>> getFavoriteCoins() {
Timber.d("Get Favorite Coins Fired");
return localRepository.getFavoriteCoins();
}
I have tested the flag isFirstTime with true and false, but the .andThen() is still never fired.
What am I doing incorrectly here?
I got it the interval to work by using akarnokd's advice removing the Completeable. I used a Single instead and continued the chain flow by passing a value with Single.just(). Please let me know if there's a better way or cleaner way to do it!
disposable = Observable.interval(0, 15, TimeUnit.SECONDS)
.flatMapSingle(n -> {
boolean isFirstTime = sharedPrefManager.isFirstTimeOpeningApp();
if (isFirstTime) {
return insertFavoritesUseCase.insertStartingCoins().doOnSuccess(insertResult -> {
sharedPrefManager.setFirstTimeOpeningApp(false);
});
} else {
return Single.just("Did not insert coins, continuing stream");
}
})
.flatMapSingle(insertResult -> getFavoritesUseCase.getFavoriteCoins())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMapSingle(listFavorites -> {
//update ui
return getCoinsUseCase.getCoinPrices(listFavorites);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(listCoinsWithPrice -> {
//update ui
}, err -> {
Timber.e("Failed to get price of coins: " + err);
});
I have sophisticated scenario where a set of mutually dependent coroutine flows depends on each other and chained:
viewModelScope.launch {
repository.cacheAccount(person)
.flatMapConcat { it->
Log.d(App.TAG, "[2] create account call (server)")
repository.createAccount(person)
}
.flatMapConcat { it ->
if (it is Response.Data) {
repository.cacheAccount(it.data)
.collect { it ->
// no op, just execute the command
Log.d(App.TAG, "account has been cached")
}
}
flow {
emit(it)
}
}
.catch { e ->
Log.d(App.TAG, "[3] get an exception in catch block")
Log.e(App.TAG, "Got an exception during network call", e)
state.update { state ->
val errors = state.errors + getErrorMessage(PersonRepository.Response.Error.Exception(e))
state.copy(errors = errors, isLoading = false)
}
}
.collect { it ->
Log.d(App.TAG, "[4] collect the result")
updateStateProfile(it)
}
}
cache an account on the local disk
create an account on the backend
in positive scenario, cache the newly create account in the local disk
Now I have to add more calls to a new API endpoint and the scenario become even more sophisticated. This endpoint is a ethereum chain.
4a. In the positive scenario, put in the local disk (cache) initiated transaction cacheRepository.createChainTx()
4b. In the negative scenario, just emit further the response from the backend
4a.->5. Register user on the 2nd endpoint repository.registerUser()
The response from 2nd endpoint put in the cache by updating existing row. Even negative case except of exception should be cached to update status of tx.
viewModelScope.launch {
lateinit var newTx: ITransaction
cacheRepository.createChainTxAsFlow(RegisterUserTransaction(userWalletAddress = userWalletAddress))
.map { it ->
newTx= it
repository.registerUserOnSwapMarket(userWalletAddress)
}
.onEach { it -> preProcessResponse(it, newTx) }
.flowOn(backgroundDispatcher)
.collect { it -> processResponse(it) }
}
This a scenario which should be integrated into the 1st Flow chain.
The issue is I do not see how to do it clear in Flow chain. I can rewrite code without chaining, but it also bring variety if else statements.
How would you do this scenario in human readable way?
I'll ended up with this code for transition period:
viewModelScope.launch(backgroundDispatcher) {
try {
var cachedPersonProfile = repository.cacheAccount(person)
var createAccountResponse = repository.createAccount(person)
when(createAccountResponse) {
is Response.Data -> {
repository.cacheAccount(createAccountResponse.data)
val cachedTx = cacheRepository.createChainTx(RegisterUserTransaction(userWalletAddress = person.userWalletAddress))
val chainTx = walletRepository.registerUserOnSwapMarket(userWalletAddress = person.userWalletAddress)
when(chainTx) {
is ru.home.swap.core.network.Response.Data -> {
if (chainTx.data.isStatusOK()) {
cachedTx.status = TxStatus.TX_MINED
} else {
cachedTx.status = TxStatus.TX_REVERTED
}
}
is ru.home.swap.core.network.Response.Error.Message -> {
cachedTx.status = TxStatus.TX_EXCEPTION
}
is ru.home.swap.core.network.Response.Error.Exception -> {
cachedTx.status = TxStatus.TX_EXCEPTION
}
}
cacheRepository.createChainTx(cachedTx)
withContext(Dispatchers.Main) {
state.update { state ->
if (cachedTx.status == TxStatus.TX_MINED) {
state.copy(
isLoading = false,
profile = createAccountResponse.data,
status = StateFlagV2.PROFILE
)
} else {
val txError = "Failed register the profile on chain with status ${TxStatus.TX_MINED}"
state.copy(
isLoading = false,
errors = state.errors + txError
)
}
}
}
}
else -> { updateStateProfile(createAccountResponse) }
}
} catch (ex: Exception) {
withContext(Dispatchers.Main) {
state.update { state ->
val errors = state.errors + getErrorMessage(PersonRepository.Response.Error.Exception(ex))
state.copy(errors = errors, isLoading = false)
}
}
}
}
If you have a better alternative, please share it in the post as an answer.
I have to hit 3 API's to update the same screen so for this i think RxJava is the fastest way to do that in parallel. While i was searching for the implementation i came across Observable.zip(...) function as it can perform multiple API hits in parallel.
I am using Retrofit for calling API's and have already created Pojo class with gson annotation.
Sample Pojo classes:
data class ResponseGetFCData(
#SerializedName("End")
val end: String,
#SerializedName("Uni")
val uni: String,
#SerializedName("Y")
val y: Double
)
data class ResponseAK(
#SerializedName("End")
val end: String,
#SerializedName("Manu")
val manu: String,
#SerializedName("Start")
val start: String,
#SerializedName("TY")
val tY: Double
)
Sample Api Interface:
interface Api{
#GET("GetUniPI")
fun getFCdata(#Query("pi") pi: String
, #Query("uni") uni: String): Observable<ResponseGetFCData>
}
Objective : From the response of 2 out of 3 API's I have to compute some mathematical calculation and the third API response will carry data for recycler view. Here i have to compute (y * ty)/100 by taking y from API 1 and ty from API 2 and such similar computations.
MyCode: In activity onCreate(....):
val requests = ArrayList<Observable<*>>()
val backendApi = WinRetrofitHelper.winApiInstance()
requests.add(backendApi.getFCdata("","","",""))
requests.add(backendApi.getAKCountry())
requests.add(backendApi.getRecyclerData("","",""))
Observable
.zip(requests) {
}
)
.subscribe({
Log.e("Exe Summary","******************Success*******************")
}) {
Log.e("Exe Summary",it.stackTrace.toString())
}
So here i am not getting how to fetch the response from these 3 API's and how and where to compute the maths and how will i update the data in recyclerview adapter from 3rd API response.
Please help me to understand this with a better approach.
Or you can give coroutines a try. It has simple syntax easy to understand
fun toDoWorkConcurrent() {
job2 = launch {
try {
val work1 = async { getThingsDone(43) }
val work2 = async { getThingsDoneAgain(123) }
val result = computeResult(work1.await(), work2.await())
withContext(UI) {
tvResult1.text = result.toString()
}
} catch (exception: Exception) {
exception.printStackTrace()
}
}
}
private fun computeResult(await: Int, await1: Int): Int {
return await + await1
}
Edit: Source
Try using below:
Observable.zip(
backendApi.getFCdata("","","",""),
backendApi.getAKCountry(),
backendApi.getRecyclerData("","",""),
Function3<ResponseGetFCData, ResponseAK, List<ResponseMarket>, List<ResponseMarket>> {
fcData, akCountry, recyclerData ->
// Your operation here
return recyclerData
})
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { /* Loading Start */ }
.doOnTerminate { /* Loading End */ }
.subscribe(
{ /* Successfully Synced */ },
{ /* Having error */ }
)
Please try like this
Observable.zip(yourobservalelist, new Function<Object[], Object>() {
#Override
public Object apply(Object[] objects) throws Exception {
return objects;
}
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(new Consumer<Disposable>() {
#Override
public void accept(Disposable disposable) throws Exception {
}
})
.doOnTerminate(new Action() {
#Override
public void run() throws Exception {
}
})
.subscribe(new Consumer<Object>() {
#Override
public void accept(Object o) throws Exception {
//Do something on successful completion of allrequests
//}
}
},
// Will be triggered if any error during requests will happen
new Consumer<Throwable>() {
#Override
public void accept(Throwable e) throws Exception {
//Do something on error completion of requests
e.printStackTrace();
}
});
}
Hello
The App: I have a very basic app which have some Machines (name, id, total_income) and some Incomes (id, money, note, machines_id). On the first screen I allow the users to add a machine from a FAB which then I display it in a RecyclerView. When the user clicks any machine I navigate them to the second screen, where the user can see the name of the machine, total income and a RecyclerView with its corresponding Income; There's a FAB which enables them to add the income for that machine and refreshes the RecyclerView.
The problem: I been failing to translate from the conventional world to RxJava.
I have managed to make it work using Room .allowMainThreadQueries() and conventional methods.
MachineViewModel
public long updateByID(long id, double total_income){
return machinesDB.getMachineDAO().updateMachineByID(id, total_income);
}
MachineDAO
#Query("update machines set total_income = :total_income where id = :id")
int updateMachineByID(long id, double total_income);
MachineInfo Activity
disposable.add(incomeViewModel.getIncomeOfMachine(id)
.defaultIfEmpty(0.0)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(total_amount -> {
if (total_amount != null) {
// This works correctly using .allowMainThreadQueries() and conventional methods
machineViewModel.updateByID(id, total_amount);
DecimalFormat formatter = new DecimalFormat("$#,##0.000");
String formatted = formatter.format(total_amount);
mMoney.setText(formatted);
Toast.makeText(this, "MachineInfo: " + String.valueOf(machineViewModel.updateByID(id, total_amount)), Toast.LENGTH_SHORT).show();
showMenu = true;
} else {
mMoney.setText("$0.0");
}
}, throwable -> Log.e(TAG, "MachineInfo: ERROR", throwable )));
I know Room needs to be instantiated in a background thread and thats why I am using RxJava. But when I try to translate the above methods into RxJava World like the below methods I'm failing to update but not crashing, "Return Value of the method is never used".
MachineViewModel
public Completable updateByID(long id, double total_income){
return Completable.fromAction(() -> machinesDB.getMachineDAO().updateMachineByID(id, total_income));
}
MachineDAO
#Query("update machines set total_income = :total_income where id = :id")
int updateMachineByID(long id, double total_income);
Try # 1: Failure
private PublishSubject<Double> publishSubject = PublishSubject.create();
private Observable<Double> clickEvent = publishSubject;
/*
/ other stuff in here
*/
disposable.add(incomeViewModel.getIncomeOfMachine(id)
.defaultIfEmpty(0.0)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(total_amount -> {
if (total_amount != null) {
publishSubject.onNext(total_amount);
DecimalFormat formatter = new DecimalFormat("$#,##0.000");
String formatted = formatter.format(total_amount);
mMoney.setText(formatted);
showMenu = true;
} else {
mMoney.setText("$0.0");
}
}, throwable -> Log.d(TAG, "MachineInfo: ERROR")));
disposable.add(clickEvent.subscribe(
total_amount -> machineViewModel.updateByID(id, total_amount)));
Try # 2: Failure
MachineViewModel
public Completable updateByID(long id, double total_income){
return Completable.fromCallable(() -> machinesDB.getMachineDAO().updateMachineByID(id, total_income));
}
MachineDAO
#Query("update machines set total_income = :total_income where id = :id")
int updateMachineByID(long id, double total_income);
MachineInfo Activity
disposable.add(incomeViewModel.getIncomeOfMachine(id)
.defaultIfEmpty(0.0)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(total_amount -> {
if (total_amount != null) {
// Completable.fromCallable()
machineViewModel.updateByID(id, total_amount);
DecimalFormat formatter = new DecimalFormat("$#,##0.000");
String formatted = formatter.format(total_amount);
mMoney.setText(formatted);
Toast.makeText(this, "MachineInfo: " + String.valueOf(machineViewModel.updateByID(id, total_amount)), Toast.LENGTH_SHORT).show();
showMenu = true;
} else {
mMoney.setText("$0.0");
}
}, throwable -> Log.e(TAG, "MachineInfo: ERROR", throwable )));
What do you mean by failing? Is the database not getting updated or are you getting some exceptions?
Anyway, the main problem I see is that you are not subscribing to your Completable objects - without this, they won't be executed.
So change:
machineViewModel.updateByID(id, total_amount);
to for example:
machineViewModel.updateByID(id, total_amount).subscribeOn(Schedulers.io()).observeOn(Schedulers.io()).subscribe();
and it will work (of course, you might wanna add specific Subscriber to subscribe method).
Okay, so thanks to Michael I managed to find the hole in the problem.
So apparently you can Add Disposables inside Disposables.
You need to initialize the Observable and add its .observeOn(etc).subscribeOn(etc).subscribe(etc)"
Add the disposable inside the disposable like this:
disposable.add(incomeViewModel.getIncomeOfMachine(id)
.defaultIfEmpty(0.0)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(total_amount -> {
if (total_amount != null) {
disposable.add(machineViewModel.updateByID(id, total_amount)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
() -> Log.d(TAG, "MachineInfo: COMPLETED"),
throwable -> Log.e(TAG, "MachineInfo: ERROR", throwable )));
DecimalFormat formatter = new DecimalFormat("$#,##0.000");
String formatted = formatter.format(total_amount);
mMoney.setText(formatted);
showMenu = true;
} else {
mMoney.setText("$0.0");
}
}, throwable -> Log.d(TAG, "MachineInfo 2: ERROR")));
Im note sure if is a clean answer but it works.
I am using retorift to hit getAricle api and get list of articles related to the user. getArticle api will throw error if token passed is expired if so then I have to call refreshToken api to get new token then again I have to call the getArticle api
ApiController.createRx().getArticle(token)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response -> toast(response.body().url) }, { e ->
println(e.printStackTrace())
if(e is HttpException && e.code() in arrayOf(401,403)){
//Here I want to call refresh tolken api
toast("Auth error")
}
else
toast(R.string.something_went_wrong)
})
Edit
Even though given answers showed some direction but those are not a direct answer to my question. This is how solved it but I feel this can be refactored into much better code
ApiController.createRx().getArticle(Preference.getToken())
.flatMap { value ->
if (value.code() in arrayOf(403, 401)) {
ApiController.refreshToken()
ApiController.createRx().getArticle(Preference.getToken())
} else Observable.just(value)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response -> println("Success") }, { e ->
e.printStackTrace()
toast(R.string.something_went_wrong)
})
fun refreshToken() {
val token:String?=ApiController.createRx().refreshToken(Preferences.getRefreshToken()).blockingFirst()?.body()?.token
if (token != null) Preferences.setAuthToken(token)
}
EDIT
I refactored my code to little more cleaner version
Observable.defer { ApiController.createRx().getArticle(Preferences.getToken()) }
.flatMap {
if (it.code() in arrayOf(401, 403)) {
ApiController.refreshToken()
Observable.error(Throwable())
} else Observable.just(it)
}
.retry(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({println("Success") }, {
it.printStackTrace()
toast(R.string.something_went_wrong)
})
fun refreshToken() {
var token: String? = null
try {
token = createRx().refreshToken(Preferences.getRefreshToken()).blockingFirst().body()!!.token
} catch (e: Exception) {
throw e
}
println("saving token")
if (token != null) Preferences.setAuthToken(token)
}
EDIT
Please check my answer for the final refactored code
I have implemented this exact thing. Here is a slightly modified version of that code:
private Observable<Object> refreshTokenIfNotAuthorized(Observable<? extends Throwable> errors) {
final AtomicBoolean alreadyRetried = new AtomicBoolean(false);
return errors.flatMap(error -> {
boolean isAuthorizationError = /* some logic analyzing each error*/ ;
if (isAuthorizationError && !alreadyRetried.get()) {
try {
alreadyRetried.set(true);
String newToken = federatedTokenRefresher.refreshToken()
.toBlocking()
.first();
setLogin(newToken);
return Observable.just(null);
} catch (Exception e) {
return Observable.error(error);
}
}
return Observable.error(error);
});
}
You can use this method like so:
doSomethingRequiringAuth().retryWhen(this::refreshTokenIfNotAuthorized);
What kind of error you will received?. It´s seems like you could use onErrorResumeNext operator.
This operator once that receive a throwable, allow you to return an Observable instead the throwable in the onError
#Test
public void observableOnErrorResumeException() {
Integer[] numbers = {0, 1, 2, 3, 4, 5};
Observable.from(numbers)
.doOnNext(number -> {
if (number > 3) {
try {
throw new IllegalArgumentException();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
})
.onErrorResumeNext(t -> Observable.just(666))
.subscribe(System.out::println);
}
You can see more examples here https://github.com/politrons/reactive/blob/master/src/test/java/rx/observables/errors/ObservableExceptions.java
I will give you another option using groupBy operator
/**
* In this example we create a response code group.
*/
#Test
public void testGroupByCode() {
Observable.from(Arrays.asList(401,403, 200))
.groupBy(code -> code)
.subscribe(groupByCode -> {
switch (groupByCode.getKey()) {
case 401: {
System.out.println("refresh token");
processResponse(groupByCode);
break;
}
case 403: {
System.out.println("refresh token");
processResponse(groupByCode);
break;
}
default: {
System.out.println("Do the toast");
processResponse(groupByCode);
}
}
});
}
private void processResponse(GroupedObservable<Integer, Integer> groupByCode) {
groupByCode.asObservable().subscribe(value -> System.out.println("Response code:" + value));
}
I solved my problem after reading more about RxJava and this is how I implemented it.
First of all will retrofit throw 4xx error to onError or onNext\onSuccess depends on how we define it.
Ex:
#GET("content")
fun getArticle(#Header("Authorization") token: String):Single<Article>
this will throw all the 4xx errors to onError and instead of Single<Article> if you define it as Single<Response<Article>> then all the response from server including 4xx will go to onNext\onSuccess
Single.defer { ApiController.createRx().getArticle(Preferences.getAuthToken())}
.doOnError {
if (it is HttpException && it.code() == 401)
ApiController.refreshToken()
}
.retry { attempts, error -> attempts < 3 && error is HttpException && error.code() == 401 }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({println("Success") }, {
it.printStackTrace()
toast(R.string.something_went_wrong)
})
I am using defer as a wrapper around my actual Observable because I want to recreate the article fetch observable on retry after token refresh because I want Preferences.getAuthToken() to be called again as my refresh token code stores newly fetched token in preference.
retry returns true if the HttpException is 401 and not attempted retry more than 2 times