3

I'm using the new Mosby MVI library for a new demo app. When defining intents in a presenter it is inconsistent when the intent is triggered/emitted when the view is attached.

For example: Let's define very simple intent in an activity

public Observable<Boolean> intentLoadData(){
  return Observable.just(true);
}

The presenter binds the intent like:

@Override
protected void bindIntents() {
  Observable<MailListViewState> loadData = intent(ExampleViewContract::intentLoadData).flatMap(interactor::loadData)
            .observeOn(AndroidSchedulers.mainThread());
  subscribeViewState(loadData, ExampleViewContract::render);
}

This intent works just fine. When navigating to a different activity (detail view) and navigating back, bindIntents()is called the the intent is recreated. intentLoadData()doesn't emit a new item and the MviBasePresenter will provide the previous ViewState using the internal BehaviorSubject.

My problem is: When I slightly adjust the Intent (for reloading the data). The intent starts to emit an item when the View is reattached.

So lets change the intent to:

private PublishSubject<Boolean> mReloadDataSubject = PublishSubject.create();

private void reloadData(){
  mReloadDataSubject.onNext(true);
}

public Observable<Boolean> intentLoadData(){
  return mReloadDataSubject.startWith(true);
}

No when navigating to a new activity and back. The intent emits a new item when the view is reattached. In my case this results in a new APU call to the backend to reload the data, rather than reusing the last ViewState. This happens even when reloadData() is never called.

This behavior feels very inconsistent. How can I feel more in control when an intent is triggered during reattaching the view?

Update: For me even more interesting is, how do I avoid the automatic emitting of the intents when reattaching, without completing the Observable. With the introduction of a PublishSubject, the activity will reload the entire data even when just rotating.

TobiasRe
  • 111
  • 6

2 Answers2

2

To answer my own question and to wrap up the comments, this is my solution:

At first we have to understand how Mosby3 MVI restores a view, e.g.: after rotation, navigating forth and back to different views. Mosby3 retains the instance of the presenter. When a new instance of the view is created, the presenter will be restored and will be attached to the view. onStart()of the new view, the presenter will update the intents. Hence, the new view creates new intents and the presenter will subscribe to them using PublishSubjects.

If the intent of the previous view emitted onComplete()the PublishSubjectis completed as well and the stream closes. The (interactor) logic bound to this intent will be unsubscribed. Therefore, this intent can't be triggered by view anymore.

In the example of the original question. Observable.just(true) closes the stream. Even when the view and it's intents are recreated (after rotation), there is no new item emitted. mReloadDataSubject.startWith(true) instead doesn't emit onComplete() and the stream isnt closed. When the presenter resubscribes to that intent (after rotation), the intent emits thestartsWith(true)`. In the example, this is causing a complete reload of the data on every rotation.

In order to trigger the intents on a conditional reloading RxNavi can be very helpful.

public Observable<Boolean> intentReloadData() {
     //check if the data needs a reload in onResume()
     return RxNavi.observe(this, Event.RESUME)
                  .filter(ignored -> mNeedsReload == true)
                  .map(ignored -> true);
}
TobiasRe
  • 111
  • 6
1

Mosby MVI respects the Reactive Streams contract. Take a look at intentLoadData()

public Observable<Boolean> intentLoadData(){
  return Observable.just(true);
}

Observable.just(true) not only calls onNext(true) but also calls onCompleted(). Once a Reactive stream is completed, no further item can be emitted through the stream. After onComplete() the observable stream is closed permanently.

Using a PublishSubject is perfectly fine in that case, but for better readability I would suggest to not use startWith() but rather do something like this:

public class MyActivity extends MviActivity<MyView, MailListViewState> {
  private PublishSubject<Boolean> mReloadDataSubject = PublishSubject.create();

  public void onResume(){
    super.onResume();
   // Triggers on screen orientation changes and
   // when navigating back to this screen from back stack
    mReloadDataSubject.onNext(true);
  }

  public Observable<Boolean> intentLoadData(){
    return mReloadDataSubject;
  }

}

Btw. you could also use libraries like Navi from Trello which offers Observable stream for lifecycle events, but keep in mind that Navi emits a onCompleted() event if the activity is destroyed (i.e. during screen orientation changes) so you end up in the same situation: you have to ensure that onCompleted() is not called if you want to fire the intent again later.

sockeqwe
  • 15,574
  • 24
  • 88
  • 144
  • My problem is more the other way around. Using a PublishSubject helps to trigger intents subsequently, but how do I stop it from emitting in `bindIntents()` when reattaching the view? – TobiasRe Feb 20 '17 at 16:26
  • What do you mean with "reattaching the view"? Can you give me a concrete example? – sockeqwe Feb 20 '17 at 16:28
  • I use a PublishSubject definition for loading data from a web service like in my original question. The activity starts and the data will be loaded. When I rotate the device the loading intent is triggered again during `bindIntentActually()` in the MviBasePresenter. I want to avoid this unnecessary reloading. `bindIntentActually()` is called in `attachView()` with `viewAttachedFirstTime`set to false in this case. Thats why I'm referring to re-attach. – TobiasRe Feb 20 '17 at 16:43
  • So you only want to fire once? Then complete after having emitted once the intent, for example `Observable.just(true)` – sockeqwe Feb 20 '17 at 16:55
  • My intention was to use the same intent for initial loading and and user intended reloading (with a PublishSubject). Seems like I need two intents. `public Observable intentLoadData(){ return Observable.just(true); }` and `public Observable intentReloadData(){ return mReloadPublishSubject; }` – TobiasRe Feb 20 '17 at 17:02
  • 1
    Something like this: `Observable.merge(initialLoadingIntent, reloadingIntent)`, then initialLoadingIntent can be complete after having emited once i.e. by using `Observable.just(true)` ... I would put `Observable.merge(...)` into the Presenter and let the view offer two intents. The reason why I would put that into Presenter is that otherwise View does a little bit of computation (the merging stuff) which makes the view a little bit less dump. Also having this in Presenter allows you to test this with a simple android unit test, otherwise you would have to write a Android Instrumentation test – sockeqwe Feb 20 '17 at 17:38