I got the following code using Mosby.
Fragment:
#Override
public Observable<CardInfo> loadCardIntent() {
return Observable.just(new CardInfo(cardselected, PreferenceManager.getDefaultSharedPreferences(getContext())
.getBoolean(PreferencesVariables.SHOW_BACK_CARD.toString(), false)))
//.delay(500, TimeUnit.MILLISECONDS)
.doOnNext(showBack -> Log.d(TAG, "Show card back: " + showBack));
}
#Override
public Observable<CardInfo> loadFrontIntent() {
return RxView.clicks(cardBackImageView)
.map(showFront -> new CardInfo(cardselected, false))
.doOnNext(showFront -> Log.d(TAG, "Show card front"));
}
#Override
public Observable<Boolean> hideCardIntent() {
return clicks(cardFrontImageView)
.map(ignored -> true)
.doOnNext(close -> Log.d(TAG, "Close card activity"));
}
Presenter:
#Override
protected void bindIntents() {
Observable<CardViewState> showSelectedCard = intent(CardView::loadCardIntent)
.switchMap(cardInfo -> interactor.getCard(cardInfo))
.doOnError(error -> System.out.print(error.getMessage()));
Observable<CardViewState> showFront = intent(CardView::loadFrontIntent)
.switchMap(cardInfo -> interactor.getCard(cardInfo))
.doOnError(error -> System.out.print(error.getMessage()));
Observable<CardViewState> hideCard = intent(CardView::hideCardIntent)
.switchMap(ignored -> interactor.hideCard());
Observable<CardViewState> intents = Observable.merge(showSelectedCard, showFront, hideCard)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
subscribeViewState(intents, CardView::render);
}
Fragment:
#Override
public void render(CardViewState viewState) {
if (viewState instanceof CardViewState.CardBackState) {
renderCard(R.raw.planningpoker_rueckseite, cardBackImageView);
renderCard(((CardViewState.CardBackState) viewState).card, cardFrontImageView);
showCardBack();
} else if (viewState instanceof CardViewState.CardFrontState) {
renderCard(R.raw.planningpoker_rueckseite, cardBackImageView);
renderCard(((CardViewState.CardFrontState) viewState).card, cardFrontImageView);
showCardFront();
} else if (viewState instanceof CardViewState.CardCloseState) {
getActivity().finish();
}
}
Interactor:
Observable<CardViewState> getCard(CardInfo cardInfo) {
return cardInfo.showBack ? Observable.just(new CardViewState.CardBackState(CARDS[cardInfo.card])) :
Observable.just(new CardViewState.CardFrontState(CARDS[cardInfo.card]));
}
Observable<CardViewState> hideCard() {
return Observable.just(new CardViewState.CardCloseState());
}
Without the delay in loadCardIntent() the render()-method does not get triggered with the CardBackState. But I don't want to use a arbitrary delay to ensure the right methods get triggered.
Is there any other way to ensure that all events get emitted?
Thanks for the help.
Hm, is your code available on github somewhere? So far everything seems to be ok. Maybe it is an internal mosby bug. Is it working if you add subscribeOn(schdulers.io()) to loadCardIntent() in presenters bind() method.
The only difference I see with or without delay() is that your code runs sync (on main UI thread) whereas delay() switches the execution of your code to a background thread. Are you sure your interactor.getCardInfo() is meant to run on androids main UI thread? I.e. if it runs on main thread, but you are doing a http request (on main UI thread) an exception is thrown. Do you catch exceptions in interactor?
This was a mosby internal issue and has been fixed now.
See https://github.com/sockeqwe/mosby/issues/242
Please use latest snapshot:
com.hannesdorfmann.mosby3:mvi:3.0.4-SNAPSHOT
(see README) to verify everything works now as intended.
Please comment on the linked github issue if it fixes your problem or not.
Thanks.
My solution for now is to use Schedulers.trampoline(). It is not ideal and in no way sufficient but it allows me to get rid of the delay that is more of a hassle.
The problem that Schedulers.trampoline() seem to be solving is that the change onto another thread takes a short amount of time. And that causes the event to get lost. So staying on the same thread fixes this.
Related
I am trying to observe my workers but they are always in queued state or sometime it's RUNNING but never SUCCEED or FAILED.
is workStatus.state from return in doWork() or it's different?
this is my worker script:
package com.mockie.daikokuten.sync.workers
import androidx.work.Worker
class TestWorker:Worker()
{
override fun doWork():Worker.Result
{
return Worker.Result.SUCCESS
}
}
this is script to observe the workers :
val test = PeriodicWorkRequest.Builder(
TestWorker::class.java,
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS)
.addTag("test_worker")
.build()
WorkManager.getInstance()?.enqueueUniquePeriodicWork("test_worker", ExistingPeriodicWorkPolicy.KEEP, test)
WorkManager.getInstance()!!.getStatusesByTag("test_worker")
.observe(this, Observer { workStatus ->
if (workStatus != null)
{
for(ws in workStatus)
{
Log.d(":dump2 id ", ws.id.toString())
Log.d(":dump2 tag", ws.tags.toString())
Log.d(":dump2 state", ws.state.toString())
}
}
})
this is the result in Logcat:
07-23 17:12:30.901 29740-29740/com.mockie.daikokuten D/:dump2 id: 5c6297f7-11d8-4f2f-a327-773672a7435c
07-23 17:12:30.901 29740-29740/com.mockie.daikokuten D/:dump2 tag: [test_worker, com.mockie.daikokuten.sync.workers.TestWorker]
07-23 17:12:30.901 29740-29740/com.mockie.daikokuten D/:dump2 state: ENQUEUED
For your periodic work request you should see
ENQUEUED - RUNNING - ENQUEUED
where the latter ENQUEUED is the state of the next work request.
You might get very briefly a SUCCEEDED between RUNNING and ENQUEUED, but I have never seen that.
For a onetime work request you see
ENQUEUED - RUNNING - SUCCEEDED
or whatever you return in doWork().
(Android 8.1 API 27, 1.0.0-alpha04)
This is for anyone who is having trouble getting their output data from periodic work.
It's more like a hack.
In your Worker, just define a static mutable Live Data.
At the place where you observe your work's state, observe this live data when your state turns to "RUNNING".
Here's a template :
The actual Worker:
public class SomeWorker extends Worker{
//This live data can be of any type. I'm setting Boolean
Public static MutableLiveData<Boolean> outputObservable = new MutableLiveData();
private boolean output_boolean;
try{
//Do you work here post your result to the live data
output_boolean = SomeTaskThatReturnsABoolean();
outputObservable.postValue(output_boolean);
return Result.Success();
}catch(Exception e){
e.printStackTrace();
outputObservable.postValue(!output_boolean);
return Result.Failure();
}
}
Your activity that observes this worker's info:
//In YourActivity class inside OnCreate
mWorkManager.getWorkInfoForUniqueWorkLiveData(YOUR_TAG).observe (this,
new Observer<List<WorkInfo>>(){
#Override
public void onChanged(#Nullable List<WorkInfo> workInfos) {
if(workInfos!=null && (!(workInfos.isEmpty()))) {
WorkInfo info = workInfos.get(0);
switch(info.getState()){
case ENQUEUED:
break;
case RUNNING:
SomeWorker.outputObservable.observe(YourActivity.this,
new Observer<Boolean>(){
#Override
public void onChanged(#Nullable Boolean aBoolean) {
//EDIT: Remove the observer of the worker otherwise
//before execution of your below code, the observation might switch
mWorkManager.getWorkInfoForUniqueWorkLiveData(YOUR_TAG).removeObservers(YourActivity.this);
if(aBoolean)
//Do whatever you have to if it's true
else
//Do whatever you have to if it's false
}
}
);
}
}
}
}
);
In this way you can observe your results when the state of the work is under running, before it gets switched back to enqueued.
The above answer is correct. For PeriodicWork you should see:
ENQUEUED -> RUNNING -> ENQUEUED
However, there is a bug in alpha04 which causes PeriodicWork to not run on API >= 23. This will be fixed in alpha05. For more info take a look at https://issuetracker.google.com/issues/111195153.
IMPORTANT: As of a couple of days ago: alpha05 has shipped. This bug is fixed.
I'm trying to create a form, send-only view that has a SignaturePad which once the user clicks on the save button, a save intent gets fired and do some background processing.
I have the following:
Presenter:
#Override
protected void bindIntents() {
Observable<SignatureViewState> observable =
intent(SignatureView::saveSignature)
.switchMap(intent -> Observable.fromCallable(() ->
storage.createFile(intent.getFullPath(), intent.getName(), intent.getImage()))
.subscribeOn(Schedulers.from(threadExecutor)))
.map(SignatureViewState.SuccessState::new)
.cast(SignatureViewState.class)
.startWith(new SignatureViewState.LoadingState())
.onErrorReturn(SignatureViewState.ErrorState::new)
.observeOn(postExecutionThread.getScheduler());
subscribeViewState(observable, SignatureView::render);
}
SignatureFragment:
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
saveButtonClickObservable = RxView.clicks(saveBtn)
.share()
.map(bla -> true);
compositeDisposable.add(saveButtonClickObservable.subscribe());
...
#Override
public Observable<SaveSignatureIntent> saveSignature() {
Observable<SaveSignatureIntent> saveSignatureIntentObservable =
Observable.just(new SaveSignatureIntent(savePath, bookingId + ".png", null));
Observable<SaveSignatureIntent> saveSignatureIntent =
Observable.zip(signatureBitmapObservable, saveSignatureIntentObservable,
(signature, intent) -> new SaveSignatureIntent(intent.fullPath, intent.name, signature));
return saveButtonClickObservable
.flatMap(bla -> saveSignatureIntent);
}
#Override
public void render(SignatureViewState state) {
if(state instanceof SignatureViewState.LoadingState)
renderLoading();
else if (state instanceof SignatureViewState.SuccessState)
renderSuccess();
if(state instanceof SignatureViewState.ErrorState)
renderError();
}
Lastly my view:
public interface SignatureView extends MvpView {
Observable<SignatureFragment.SaveSignatureIntent> saveSignature();
void render(SignatureViewState state);
}
The problem is, once my frag gets created, the .startWith gets fired, without the user clicking the button.
After that, if the user clicks on the button, the loading state never gets called (.startwith again) but only the success (or error).
What am I missing here ?
Thanks again !
Edit:
signatureBitmapObservable = Observable.fromCallable(() -> signaturePad.getTransparentSignatureBitmap(true))
.subscribeOn(Schedulers.io())
.startWith(bla -> renderLoading());
Another process is getting a transparent Bitmap, but after adding startWith, my callable never gets called. If I take it out, it works like a charm.
This is just a RxJava mistake, not really Mosby related.
put .startWith(new SignatureViewState.LoadingState()) (and maybe .onErrorReturn(SignatureViewState.ErrorState::new)) in the observable returned from switchMap(). Like this:
intent(SignatureView::saveSignature)
.switchMap(intent -> Observable.fromCallable(() ->
storage.createFile(intent.getFullPath(), intent.getName(), intent.getImage()))
.subscribeOn(Schedulers.from(threadExecutor))
.map(SignatureViewState.SuccessState::new)
.cast(SignatureViewState.class)
.startWith(new SignatureViewState.LoadingState())
.onErrorReturn(SignatureViewState.ErrorState::new)
) // end of switchMap
.observeOn(postExecutionThread.getScheduler());
The observable returned from switchMap only start once the user clicks on your button (because switchMap only triggers after intent has been fired).
startWith() means "before you do the real work, emit this first". If you apply startWith() as you did in your original code snipped, obviously it the observable start with loading, but what you really want is "before saving the signature, start with loading state". Therefore startWith() must be part of the "save signature" observable, and not of the "main" observable per se.
Hope you guys are doing well,
I have been working on a personal Android Project using RxJava and Retrofit. It's a search request for GitHub issues, I am getting input from the user when he clicks Search Button and using a PublishSubject object to emit the search text.
button.setOnClickListener(view -> {
publishSubject.onNext(editText.getText().toString());
});
and I am mapping this emit to an Observable using retrofit like this
publishSubject.concatMap(dataModel::getIssues)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::loadData, this::onError);
public Observable<List<Issue>> getIssues(String queryText) {
String[] query_params = queryText.split("/");
return gitHubApiService.getIssues(query_params[0], query_params[1], "open");
}
In result I am expecting List of Issues
public void loadData(List<Issue> issues) {
mProgressDialog.setVisibility(View.INVISIBLE);
if( issues.size() == 0) {
noIssueText.setVisibility(View.VISIBLE);
} else {
mRecyclerView.setVisibility(View.VISIBLE);
mIssuesList.clear();
mIssuesList.addAll(issues);
mAdapter.notifyDataSetChanged();
}
}
But my code seems to have some implementation issue Since it never emits anything from the network, not even on error is called.
I have tested the same example with the Observable I get from Retrofit API, so there is no retrofit error and so I think there is some problem with my concatMap logic.
Any help will be much appreciated
On first parse, I think that you might be making the network call in the main thread. Have you tried the following?
public Observable<List<Issue>> getIssues(String queryText) {
String[] query_params = queryText.split("/");
return gitHubApiService.getIssues(query_params[0], query_params[1], "open")
.subscribeOn(Schedulers.io());
}
Thing is, your onClickListener callback runs on the main thread, and there's no other context switch in the observable pipeline.
Following the topic discussed here. I'm coding an Android App using the Clean Architecture. I've an Interactor that takes care of retriving the User's feed data. The flow is like this:
I must fetch the Feed data from the a Repository which calls a Retrofit's service to do the API call.
If something goes wrong I've to fetch the feed data from a FeedCache that internally works with Sqlite.
I've to merge this feed collection with another bunch of feeds from another cache called PendingPostCache. This cache contains all the articles that the user couldn't post (because something went wrong, didn't had internet connection, etc.)
My FeedCache and PendingPostCache both work with Sqlite. Botch can throw DBExceptions if something went wrong. My FeedRepository the ones that makes the requests against the server-side can also throw exceptions if something goes wrong (ServerSideException).
Here's the whole code from my Interactor:
mFeedRepository.getFeed(offset, pageSize) //Get items from the server-side
.onErrorResumeNext(mFeedCache.getFeed(userSipid)) //If something goes wrong take it from cache
.mergeWith(mPendingPostCache.getAllPendingPostsAsFeedItems(user)) //Merge the response with the pending posts
.subscribe(new DefaultSubscriber<List<BaseFeedItem>>() {
#Override
public void onNext(List<BaseFeedItem> baseFeedItems) {
callback.onFeedFetched(baseFeedItems);
}
#Override
public void onError(Throwable e) {
if (e instanceof ServerSideException) {
//Handle the http error
} else if (e instanceof DBException) {
//Handle the database cache error
} else {
//Handle generic error
}
}
});
I don't like having those instanceof. I'm thinking on creating a custom subscriber, something called like MyAppSubscriber, which implements the onError method, makes those instanceof comparations, and execute some methods called onServerSideError(), onDBError(). That way the code is going te be a lot cleaner and I can spare writing that instanceof boilerplate code. Has someone a better idea about how to approach this issue? Some way to avoid the custom Subscriber?
Just use composition:
public <T,E> Function<Throwable, Observable<T>> whenExceptionIs(Class<E> what, Function<E, Observable<T>> handler) {
return t -> {
return what.isInstance(t) ? handler.apply(what.cast(t)) : Observable.error(t);
};
}
Then you use it normally :
Observable.from(...).flatMap(...)
.onErrorResumeNext(whenExceptionIs(ServerSideException.class, e-> Observable.empty()))
.onErrorResumeNext(whenExceptionIs(DBException.class, e-> ...))
You can even abstract all that in one method:
public <T> Transformer<T, T> errorHandling() {
return src -> src
.onErrorResumeNext(whenExceptionIs(ServerSideException.class, e-> Observable.empty()))
.onErrorResumeNext(whenExceptionIs(DBException.class, e-> ...));
}
Observable.from(...).flatMap(...)
.compose(errorHandling())
.subscribe();
So I have this code:
public Observable<AbstractXMPPConnection> connect(final AbstractXMPPConnection connection) {
return Observable.<AbstractXMPPConnection>create(subscriber -> {
try {
AbstractXMPPConnection connection2 = connection.connect();
if (connection2.isConnected()) {
subscriber.onNext(connection2);
subscriber.onCompleted();
}
} catch (SmackException | IOException | XMPPException e) {
e.printStackTrace();
subscriber.onError(e);
}
})
.doOnError(throwable -> LOGI("111", "Connection OnError called"));
}
public Observable<AbstractXMPPConnection> connectWithRetry(final AbstractXMPPConnection connection) {
return connect(connection)
.retryWhen(attempts -> attempts.zipWith(Observable.range(1, MAX_CONNECTION_TRIES), (throwable, integer) -> new Pair<>(throwable, integer))
.flatMap(pair -> {
if (pair.second == MAX_LOGIN_TRIES)
return Observable.error(pair.first);
return Observable.timer(pair.second, TimeUnit.SECONDS);
}));
}
public void connect() {
assertTrue("To start a connection to the server, you must first call init() method!",
this.connectionConfig != null);
connectionHelper.connectWithRetry(connection)
.observeOn(Schedulers.newThread())
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<AbstractXMPPConnection>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
LOGI(TAG, "ConnectionHelper Connection onError\n");
/**{#link LoginActivity#onConnectionFailure(OnConnectionFailureEvent)} */
MainApplication.getInstance().getBusInstance().post(new OnConnectionFailureEvent());
}
#Override
public void onNext(AbstractXMPPConnection connection) {
LOGI(TAG, "ConnectionHelper Connection onNext");
// onConnected();
}
});
}
I have some questions about chaining observables. Imagining this scenario, in which I have a connect Observable, which sometimes I use, but I use mainly the connectWithRetry() Observable.
My question is, what would happen if a added this:
.observeOn(Schedulers.newThread())
.subscribeOn(AndroidSchedulers.mainThread())
To both the connect() and connectWithRetry()? In this scenario, when I call
public void connect and specify a scheduler, the previous ones are ignored?
And why am I getting NetworkOnMainThreadException? The explicit observeOn(Schedulers.newThread()) is there, it shouldnt be giving me that error
I'll address your NetworkOnMainThread issue first.
observeOn(Schedulers.newThread()) means the output will be observed on a new thread - that is, the code in your subscriber (onComplete/Error/Next) will be run on that thread.
subscribeOn(AndroidSchedulers.mainThread() means subscription will happen on the main thread - the code in your created observable (connection.connect() etc) is what is run when subscription happens.
So simply swap the schedulers:
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
So to address your first question, they're not ignored, they're just being used incorrectly. Hopefully from this you can see what would happen if you moved similar calls in to the chain inside your methods that return observables: nothing different to what you've already done. The calls would simply be in a different place.
So where to put the scheduler selection? That's up to you. You may gain increased clarity by not having the subscribeOn call inside the methods for creating your observables:
connectionHelper.connectWithRetry(connection)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
However, if you feel like you're calling this everywhere for no reason, you can instead move the subscribeOn call inside your methods:
return connect(connection)
.retryWhen(...)
.flatMap(...)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
Note that these don't have to be bundled up together like this - you could subscribeOn inside your method, but leave observeOn up to any callers that want their results on a specific thread.
Please try Schedulers.io() may be issue resolve.