sockeqwe / mosby

A Model-View-Presenter / Model-View-Intent library for modern Android apps
http://hannesdorfmann.com/mosby/
Apache License 2.0
5.49k stars 841 forks source link

MVI: How to handle navigation that depends on business logic to occur #218

Closed carlos-snow closed 7 years ago

carlos-snow commented 7 years ago

I've been using the Mosby MVI library and I'm quite happy with it, but I got stuck in a situation that I don't know how to properly handle.

I have a screen that contains a form and a button. By touching the button, the form should be validated and, if valid, navigate to a new activity.

The form validation depends on the current state of the screen (FormViewState) and some business rules, therefore it should happen in the interactor and return the result to the view through the render() call.

I tried to handle this by adding a boolean in the FormViewState:

class FormViewState {
    boolean navigateToNextScreen;
    ...
}

The view would receive the state with navigateToNextScreen==true and start the new activity. The problem is that now the state should be updated by settingnavigateToNextScreen to false (through an intent) so that if the user returns back to the first activity, it doesn't incorrectly 'trigger' the navigation again.

I don't feel like this process of updating the attribute back to false is ok, because it seems to be adding a reasonable amount of [maybe unintuitive] code only to solve this uninteresting situation.

If it was only an isolated case, it should be ok, but I see this as a very common situation (another instance of the same problem would be if, instead of starting a new activity, a dialog was presented).

I don't know if I made myself clear or if I'm doing something wrong, but I already made some 'fairly complex' screens following the MVI pattern and, for now, this is the only situation that is bothering me.

I would like to know if someone happens to have a good approach to resolve this problem.

sockeqwe commented 7 years ago

This is an excellent question! I'm afraid, I don't have a good answer for that.

Nevertheless, from what I can tell you from your probelm description is that I have made 2 observations that might help you solving your problem:

  1. is the Form really needed after having clicked on the button? I mean, couldn't just this Form-View be closed when the next screen appears so that this problem (user comes back to this Form-View from the backstack) can be avoided from the very beginning.
  2. It seems that navigation is a core element of your business logic. So probably, Navigation should be part of your business logic. I'm not sure how this could be implemented the best way, but probably something like this (Interactor that interacts with a "Navigator" and "FormValidator" could work (just to give you an idea):
class FormInteractor {
    private Navigator navigator;
    private FormValidator validator;

    Observable<FormViewState> validateForm(String fieldA, String fieldB, ...) {
         return validator.validate(fieldA, fieldB, ... )
                     .doOnNext(state -> if (state.isValid()) navigator.navigateToNextScreen() )
    }
}

Also, a flag like navigateToNextScreen in a ViewState seems a little bit strange because "navigating to the next screen" is actually not effecting the current Form-View at all, right? I see this as a hint that this should not be part of FormViewState.


Some additional thoughts about Navigation:

Short version: I think navigation is out of scope of such patterns like MVP, MVVM and MVI. They are only about separating View from Model. Usually I handle navigation in the view layer. i.e if a user clicks a button to start FooActivity then I would directly call startActivity(FooActivity.class) in View layer. Usually, there is no need to let this information sink down to your business logic through an MVI intent() since this will not change the state of any MVI view or business logic at all, right? But your case is slightly different.

Long version: Well, in theory navigation could be handled pretty well with MVI since you could have one "Model" containing your navigation back stack. Whenever you want to navigate to another screen, the navigation back stack model would be changed (via an intent() of a View( and at the end of the observable chain there would be a "Router" which render(newNavigationStack) method would be called. But instead of rendering UI widgets it would "just" update the navigation stack. Again, in theory this would work well.

In practice, this is not trivial and hard to implement because android handles navigation stack by its own (i.e. Activity back stack), fragments are handled and restored by the fragment manager etc. So there are a lot of ugly "side effects", state handling and restoration that android internally does which makes inplementing such a "Router" with render(newNavigationStack) cumbersome and unpractical. Maybe this would work in an single Activity Application and using ViewGroups (instead of fragments etc) where you would be able to just swap out views when "Router" tells the single MainActivity to "render(newNavigationStack)"