Skip to main content

Model-View-Intent

ArchitecturalAbout 2 min

Intent

MVI is a derivation of the original MVC architectural pattern. Instead of working with a
proactive controller MVI works with the reactive component called intent: it's a component
which translates user input events into model updates.

Explanation

MVI is a Reactive Architecture Pattern which is short for Model -View-Intent.
It introduces two new concepts: the intent and the state.
UI might have different states — Loading State, Fetch Data State, Error State,
and user events are submitted in the form of an Intent.

Class diagram

alt text
Model-View-Intent

Programmatic Example

CalculatorAction defines our Intent in MVI for user interactions. It has to be an interface
instead of enum, so that we can pass parameters to certain children.

public interface CalculatorAction {

  /**
   * Makes identifying action trivial.
   *
   * @return subclass tag.
   * */
  String tag();
}

CalculatorModel defines the state of our view or in out case, variable and output of the calculator.

@Data
public class CalculatorModel {

  /**
   * Current calculator variable used for operations.
   **/
  final Double variable;

  /**
   * Current calculator output -> is affected by operations.
   **/
  final Double output;
}

CalculatorView will serve as a mock view which will expose potential user actions and
display calculator state -> output and current variable

@Slf4j
public class CalculatorView {

  /**
   * View model param handling the operations.
   * */
  private final CalculatorViewModel viewModel = new CalculatorViewModel();

  /**
   * Display current view model output with logger.
   * */
  void displayTotal() {
    LOGGER.info(
        "Total value = {}",
        viewModel.getCalculatorModel().getOutput().toString()
    );
  }

  /**
   * Handle addition action.
   * */
  void add() {
    viewModel.handleAction(new AdditionCalculatorAction());
  }

  /**
   * Handle subtraction action.
   * */
  void subtract() {
    viewModel.handleAction(new SubtractionCalculatorAction());
  }

  /**
   * Handle multiplication action.
   * */
  void multiply() {
    viewModel.handleAction(new MultiplicationCalculatorAction());
  }

  /**
   * Handle division action.
   * */
  void divide() {
    viewModel.handleAction(new DivisionCalculatorAction());
  }

  /**
   * Handle setting new variable action.
   *
   * @param value -> new calculator variable.
   * */
  void setVariable(final Double value) {
    viewModel.handleAction(new SetVariableCalculatorAction(value));
  }
}

Finally, ViewModel handles the exposed events with the handleAction(event) method, which delegates
the specific handling to private methods. Initially calculator output and variable are equal to 0.

public final class CalculatorViewModel {

  /**
   * Current calculator model (can be changed).
   */
  private CalculatorModel model =
      new CalculatorModel(0.0, 0.0);

  /**
   * Handle calculator action.
   *
   * @param action -> transforms calculator model.
   */
  void handleAction(final CalculatorAction action) {
    switch (action.tag()) {
      case AdditionCalculatorAction.TAG -> add();
      case SubtractionCalculatorAction.TAG -> subtract();
      case MultiplicationCalculatorAction.TAG -> multiply();
      case DivisionCalculatorAction.TAG -> divide();
      case SetVariableCalculatorAction.TAG -> {
        SetVariableCalculatorAction setVariableAction =
            (SetVariableCalculatorAction) action;
        setVariable(setVariableAction.getVariable());
      }
      default -> {
      }
    }
  }

  /**
   * Getter.
   *
   * @return current calculator model.
   */
  public CalculatorModel getCalculatorModel() {
    return model;
  }

  /**
   * Set new calculator model variable.
   *
   * @param variable -> value of new calculator model variable.
   */
  private void setVariable(final Double variable) {
    model = new CalculatorModel(
        variable,
        model.getOutput()
    );
  }

  /**
   * Add variable to model output.
   */
  private void add() {
    model = new CalculatorModel(
        model.getVariable(),
        model.getOutput() + model.getVariable()
    );
  }

  /**
   * Subtract variable from model output.
   */
  private void subtract() {
    model = new CalculatorModel(
        model.getVariable(),
        model.getOutput() - model.getVariable()
    );
  }

  /**
   * Multiply model output by variable.
   */
  private void multiply() {
    model = new CalculatorModel(
        model.getVariable(),
        model.getOutput() * model.getVariable()
    );
  }

  /**
   * Divide model output by variable.
   */
  private void divide() {
    model = new CalculatorModel(
        model.getVariable(),
        model.getOutput() / model.getVariable()
    );
  }
}

Applicability

Use the Model-View-Intent pattern when

  • You want to clearly separate the domain data from its user interface representation
  • You want to minimise the public api of the view model

Known uses

A popular architecture pattern in android. The small public api is particularly powerful
with the new Android Compose UI, as you can pass a single method (viewModel::handleEvent)
to all Composables(parts of UI) as a callback for user input event.

Consequences

Pros:

  • Encapsulation
  • Separation of concerns
  • Clear list of all possible user events

Cons:

  • More boilerplate code compared to alternatives (especially in Java)

MVC:

Credits