mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-08-17 08:24:38 +00:00
docs: update model-view-intent
This commit is contained in:
+164
-182
@@ -3,243 +3,225 @@ title: Model-View-Intent
|
|||||||
category: Architectural
|
category: Architectural
|
||||||
language: en
|
language: en
|
||||||
tags:
|
tags:
|
||||||
- Decoupling
|
- Abstraction
|
||||||
- Encapsulation
|
- Decoupling
|
||||||
|
- Presentation
|
||||||
|
- Reactive
|
||||||
|
- State tracking
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Also known as
|
||||||
|
|
||||||
|
* MVI
|
||||||
|
|
||||||
## Intent
|
## 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
|
To create a unidirectional and cyclical data flow between the Model, View, and Intent components, enhancing predictability and maintainability in user interface logic.
|
||||||
which translates user input events into model updates.
|
|
||||||
|
|
||||||
## Explanation
|
## Explanation
|
||||||
|
|
||||||
> MVI is a Reactive Architecture Pattern which is short for Model -View-Intent.
|
Real-world example
|
||||||
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.
|
|
||||||
|
|
||||||
* [Stateful Android Apps With MVI (MODEL — VIEW — INTENT)](https://medium.com/huawei-developers/stateful-android-apps-with-mvi-architecture-model-view-intent-d106b09bd967)
|
> Consider a real-world analogy of the Model-View-Intent (MVI) pattern using the scenario of ordering at a fast-food restaurant.
|
||||||
|
>
|
||||||
|
> In this analogy:
|
||||||
|
> - **Model:** It's like the kitchen of the restaurant, where the current state of your order is managed. As you choose items, the kitchen updates the order status and ingredients used.
|
||||||
|
> - **View:** This represents the menu and the digital display board where you see your current order summary. It reflects the current state of your order, showing what items you've added or removed.
|
||||||
|
> - **Intent:** Think of this as your decision-making process when you interact with the menu. Each choice you make (like adding a burger or removing a drink) sends a specific intention to the system (kitchen).
|
||||||
|
>
|
||||||
|
> When you decide to add an item to your order (Intent), the kitchen (Model) processes this request, updates the state of your order, and then the display (View) updates to show the latest status of your order. This cycle continues until you finalize your order, demonstrating the unidirectional and cyclical flow characteristic of MVI. This ensures that every change in the order is predictably and accurately reflected in the customer's view, similar to how UI components update in response to state changes in software using MVI.
|
||||||
|
|
||||||
## Class diagram
|
In plain words
|
||||||

|
|
||||||
|
> The Model-View-Intent (MVI) pattern is a reactive architectural approach where user actions (Intent) modify the application state (Model), and the updated state is then reflected back in the user interface (View) in a unidirectional and cyclical data flow.
|
||||||
|
|
||||||
**Programmatic Example**
|
**Programmatic Example**
|
||||||
|
|
||||||
CalculatorAction defines our Intent in MVI for user interactions. It has to be an interface
|
The Model-View-Intent (MVI) pattern is a modern approach to structuring your application's logic. It's a variation of the Model-View-Presenter (MVP) and Model-View-ViewModel (MVVM) patterns, but with a more streamlined flow of data and events.
|
||||||
instead of enum, so that we can pass parameters to certain children.
|
|
||||||
```java
|
|
||||||
public interface CalculatorAction {
|
|
||||||
|
|
||||||
/**
|
In MVI, the View sends user events to the Intent. The Intent translates these events into a state change in the Model. The Model then pushes this new state to the View, which updates itself accordingly. This creates a unidirectional data flow, which can make your code easier to understand and debug.
|
||||||
* Makes identifying action trivial.
|
|
||||||
*
|
First, we have the `App` class, which serves as the entry point for the application. It creates the View and ViewModel, and then simulates a series of user interactions with the calculator.
|
||||||
* @return subclass tag.
|
|
||||||
* */
|
```java
|
||||||
String tag();
|
public final class App {
|
||||||
|
private static final double RANDOM_VARIABLE = 10.0;
|
||||||
|
|
||||||
|
public static void main(final String[] args) {
|
||||||
|
var view = new CalculatorView(new CalculatorViewModel());
|
||||||
|
var variable1 = RANDOM_VARIABLE;
|
||||||
|
|
||||||
|
view.setVariable(variable1);
|
||||||
|
view.add();
|
||||||
|
view.displayTotal();
|
||||||
|
|
||||||
|
variable1 = 2.0;
|
||||||
|
view.setVariable(variable1);
|
||||||
|
view.subtract();
|
||||||
|
view.divide();
|
||||||
|
view.multiply();
|
||||||
|
view.displayTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private App() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
CalculatorModel defines the state of our view or in out case, variable and output of the calculator.
|
The `CalculatorView` class represents the View in MVI. It receives user events (in this case, simulated by the `App` class), and sends them to the ViewModel. It also updates its display when it receives a new state from the ViewModel.
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@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
|
|
||||||
```java
|
|
||||||
@Slf4j
|
|
||||||
public class CalculatorView {
|
public class CalculatorView {
|
||||||
|
private CalculatorViewModel viewModel;
|
||||||
|
|
||||||
/**
|
public CalculatorView(CalculatorViewModel viewModel) {
|
||||||
* View model param handling the operations.
|
this.viewModel = viewModel;
|
||||||
* */
|
|
||||||
private final CalculatorViewModel viewModel = new CalculatorViewModel();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display current view model output with logger.
|
|
||||||
* */
|
|
||||||
void displayTotal() {
|
|
||||||
LOGGER.info(
|
|
||||||
"Total value = {}",
|
|
||||||
viewModel.getCalculatorModel().getOutput().toString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void setVariable(double variable) {
|
||||||
* Handle addition action.
|
viewModel.process(new SetVariableEvent(variable));
|
||||||
* */
|
|
||||||
void add() {
|
|
||||||
viewModel.handleAction(new AdditionCalculatorAction());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void add() {
|
||||||
* Handle subtraction action.
|
viewModel.process(new AddEvent());
|
||||||
* */
|
|
||||||
void subtract() {
|
|
||||||
viewModel.handleAction(new SubtractionCalculatorAction());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void subtract() {
|
||||||
* Handle multiplication action.
|
viewModel.process(new SubtractEvent());
|
||||||
* */
|
|
||||||
void multiply() {
|
|
||||||
viewModel.handleAction(new MultiplicationCalculatorAction());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void divide() {
|
||||||
* Handle division action.
|
viewModel.process(new DivideEvent());
|
||||||
* */
|
|
||||||
void divide() {
|
|
||||||
viewModel.handleAction(new DivisionCalculatorAction());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void multiply() {
|
||||||
* Handle setting new variable action.
|
viewModel.process(new MultiplyEvent());
|
||||||
*
|
}
|
||||||
* @param value -> new calculator variable.
|
|
||||||
* */
|
public void displayTotal() {
|
||||||
void setVariable(final Double value) {
|
System.out.println("Total: " + viewModel.getState().getTotal());
|
||||||
viewModel.handleAction(new SetVariableCalculatorAction(value));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, ViewModel handles the exposed events with the handleAction(event) method, which delegates
|
The `CalculatorViewModel` class represents the ViewModel in MVI. It receives events from the View, updates the Model's state accordingly, and then pushes the new state to the View.
|
||||||
the specific handling to private methods. Initially calculator output and variable are equal to 0.
|
|
||||||
```java
|
```java
|
||||||
public final class CalculatorViewModel {
|
public class CalculatorViewModel {
|
||||||
|
private CalculatorModel model;
|
||||||
|
|
||||||
/**
|
public CalculatorViewModel() {
|
||||||
* Current calculator model (can be changed).
|
this.model = new CalculatorModel();
|
||||||
*/
|
|
||||||
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 -> {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void process(UserEvent event) {
|
||||||
* Getter.
|
event.apply(model);
|
||||||
*
|
}
|
||||||
* @return current calculator model.
|
|
||||||
*/
|
public CalculatorModel getState() {
|
||||||
public CalculatorModel getCalculatorModel() {
|
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
/**
|
The `CalculatorModel` class represents the Model in MVI. It holds the current state of the calculator, and provides methods for updating that state.
|
||||||
* Set new calculator model variable.
|
|
||||||
*
|
```java
|
||||||
* @param variable -> value of new calculator model variable.
|
public class CalculatorModel {
|
||||||
*/
|
private double total;
|
||||||
private void setVariable(final Double variable) {
|
private double variable;
|
||||||
model = new CalculatorModel(
|
|
||||||
variable,
|
public void setVariable(double variable) {
|
||||||
model.getOutput()
|
this.variable = variable;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void add() {
|
||||||
* Add variable to model output.
|
total += variable;
|
||||||
*/
|
|
||||||
private void add() {
|
|
||||||
model = new CalculatorModel(
|
|
||||||
model.getVariable(),
|
|
||||||
model.getOutput() + model.getVariable()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void subtract() {
|
||||||
* Subtract variable from model output.
|
total -= variable;
|
||||||
*/
|
|
||||||
private void subtract() {
|
|
||||||
model = new CalculatorModel(
|
|
||||||
model.getVariable(),
|
|
||||||
model.getOutput() - model.getVariable()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void divide() {
|
||||||
* Multiply model output by variable.
|
total /= variable;
|
||||||
*/
|
|
||||||
private void multiply() {
|
|
||||||
model = new CalculatorModel(
|
|
||||||
model.getVariable(),
|
|
||||||
model.getOutput() * model.getVariable()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void multiply() {
|
||||||
* Divide model output by variable.
|
total *= variable;
|
||||||
*/
|
}
|
||||||
private void divide() {
|
|
||||||
model = new CalculatorModel(
|
public double getTotal() {
|
||||||
model.getVariable(),
|
return total;
|
||||||
model.getOutput() / model.getVariable()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Finally, the `UserEvent` interface and its implementations represent the different types of user events that can occur. Each event knows how to apply itself to the Model.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface UserEvent {
|
||||||
|
void apply(CalculatorModel model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SetVariableEvent implements UserEvent {
|
||||||
|
private double variable;
|
||||||
|
|
||||||
|
public SetVariableEvent(double variable) {
|
||||||
|
this.variable = variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(CalculatorModel model) {
|
||||||
|
model.setVariable(variable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar classes would be created for AddEvent, SubtractEvent, DivideEvent, and MultiplyEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
This example demonstrates the key aspects of the MVI pattern: unidirectional data flow, clear separation of concerns, and the use of events to drive changes in the Model's state.
|
||||||
|
|
||||||
|
## Class diagram
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Applicability
|
## Applicability
|
||||||
Use the Model-View-Intent pattern when
|
|
||||||
|
|
||||||
* You want to clearly separate the domain data from its user interface representation
|
* Useful in applications with complex user interfaces that require a clear separation of concerns and predictable state management.
|
||||||
* You want to minimise the public api of the view model
|
* Often applied in reactive programming environments to ensure a smooth data flow and state consistency.
|
||||||
|
|
||||||
## Known uses
|
## Tutorials
|
||||||
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)
|
|
||||||
|
|
||||||
## Related patterns
|
|
||||||
MVC:
|
|
||||||
* [Trygve Reenskaug - Model-view-controller](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)
|
|
||||||
* [J2EE Design Patterns](https://www.amazon.com/gp/product/0596004273/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596004273&linkCode=as2&tag=javadesignpat-20&linkId=48d37c67fb3d845b802fa9b619ad8f31)
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
* [Model View Intent: a new Android Architecture Pattern](https://apiumacademy.com/blog/model-view-intent-pattern/)
|
* [Model View Intent: a new Android Architecture Pattern](https://apiumacademy.com/blog/model-view-intent-pattern/)
|
||||||
* [MVI Architecture for Android Tutorial](https://www.kodeco.com/817602-mvi-architecture-for-android-tutorial-getting-started)
|
* [MVI Architecture for Android Tutorial](https://www.kodeco.com/817602-mvi-architecture-for-android-tutorial-getting-started)
|
||||||
|
|
||||||
|
## Known uses
|
||||||
|
|
||||||
|
* Widely adopted in reactive and event-driven Java applications, particularly those using frameworks like RxJava or Project Reactor.
|
||||||
|
* Used in Android development, especially with libraries that support reactive programming such as RxJava and LiveData.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
|
||||||
|
* Enhances the predictability of the UI by establishing a clear and cyclical data flow.
|
||||||
|
* Improves testability due to well-defined separation between components.
|
||||||
|
* Supports better state management by centralizing the state within the Model.
|
||||||
|
|
||||||
|
Trade-offs:
|
||||||
|
|
||||||
|
* Increases complexity for simple UIs due to the structured and cyclical flow.
|
||||||
|
* Requires familiarity with reactive programming paradigms.
|
||||||
|
* Can lead to boilerplate code if not managed properly.
|
||||||
|
|
||||||
|
## Related patterns
|
||||||
|
|
||||||
|
[Model-View-ViewModel (MVVM)](https://java-design-patterns.com/patterns/model-view-viewmodel/): Shares a similar goal of separating the view from the model but differs as MVI introduces a cyclical data flow.
|
||||||
|
[Model-View-Controller (MVC)](https://java-design-patterns.com/patterns/model-view-controller/): MVI can be seen as an evolution of MVC, focusing more on reactive programming and unidirectional data flow.
|
||||||
|
[Observer](https://java-design-patterns.com/patterns/observer/): Essential in MVI to observe changes in the Model and update the View accordingly.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
* [Functional and Reactive Domain Modeling](https://amzn.to/4adghJ8)
|
||||||
|
* [Reactive Programming with RxJava: Creating Asynchronous, Event-Based Applications](https://amzn.to/4dxwawC)
|
||||||
|
|||||||
Reference in New Issue
Block a user