Model-View-ViewModel (MVVM) Pattern
Bind UI components declaratively to a ViewModel that exposes data and commands, enabling automatic synchronization between view and state.
Overview
The Model-View-ViewModel (MVVM) pattern splits an application into three layers: the Model handles data and business logic, the View is the UI layout, and the ViewModel is the middle layer that exposes state and behavior the View can bind to. The View connects to the ViewModel declaratively, so changes in the ViewModel flow back into the View automatically.
Most reactive UI frameworks are built around some form of MVVM. WPF (.NET 8+), Vue 3.4+, Angular 17+, and Jetpack Compose 1.6+ each put their own spin on it. The big win is keeping the View as a thin declarative layer while the ViewModel holds all the testable presentation logic. If you’ve worked with the Observer pattern or the Command pattern, you’ll recognize the building blocks — MVVM composes them into a full UI architecture.
When to Use
MVVM is worth it when your UI has to react automatically to state changes. It fits nicely when the view layer supports data binding (XAML, Vue templates, Angular templates), when you want the View to be a pure declarative mapping of the ViewModel, and when several views need to show the same ViewModel data in different ways.
When to Avoid
For simple UIs with little interactivity or state, MVVM is usually overkill. It also gets awkward without a data-binding framework — without binding, you’re stuck writing boilerplate that a framework would handle for you. Watch out when a single ViewModel is getting too complex trying to serve unrelated views, or when you’re in a performance-critical UI where binding overhead isn’t acceptable.
Solution
Python
from dataclasses import dataclass, field
from typing import List, Callable
@dataclass
class TodoItem:
id: int
text: str
done: bool = False
# Model
class TodoRepository:
def __init__(self):
self._items = []
self._next_id = 1
def add(self, text: str) -> TodoItem:
item = TodoItem(id=self._next_id, text=text)
self._items.append(item)
self._next_id += 1
return item
def toggle(self, item_id: int):
for item in self._items:
if item.id == item_id:
item.done = not item.done
def all(self) -> List[TodoItem]:
return list(self._items)
# ViewModel
class TodoViewModel:
def __init__(self, repository: TodoRepository):
self._repo = repository
self._listeners: List[Callable] = []
def add_todo(self, text: str):
self._repo.add(text)
self._notify()
def toggle(self, item_id: int):
self._repo.toggle(item_id)
self._notify()
@property
def items(self) -> List[TodoItem]:
return self._repo.all()
@property
def completed_count(self) -> int:
return sum(1 for item in self.items if item.done)
def subscribe(self, listener: Callable):
self._listeners.append(listener)
def _notify(self):
for listener in self._listeners:
listener()
# View (Console)
class TodoConsoleView:
def __init__(self, view_model: TodoViewModel):
self.view_model = view_model
self.view_model.subscribe(self.render)
def render(self):
print("\n--- Todo List ---")
for item in self.view_model.items:
status = "[x]" if item.done else "[ ]"
print(f"{status} {item.text}")
print(f"Completed: {self.view_model.completed_count}")
def on_add(self, text: str):
self.view_model.add_todo(text)
def on_toggle(self, item_id: int):
self.view_model.toggle(item_id)
# Usage
repo = TodoRepository()
vm = TodoViewModel(repo)
view = TodoConsoleView(vm)
view.on_add("Buy groceries")
view.on_add("Walk the dog")
view.on_toggle(1)
Java
import java.util.*;
class TodoItem {
private final int id;
private final String text;
private boolean done;
public TodoItem(int id, String text) {
this.id = id;
this.text = text;
}
public int getId() { return id; }
public String getText() { return text; }
public boolean isDone() { return done; }
public void setDone(boolean done) { this.done = done; }
}
class TodoRepository {
private final List<TodoItem> items = new ArrayList<>();
private int nextId = 1;
public TodoItem add(String text) {
TodoItem item = new TodoItem(nextId++, text);
items.add(item);
return item;
}
public void toggle(int id) {
items.stream().filter(i -> i.getId() == id).findFirst().ifPresent(i -> i.setDone(!i.isDone()));
}
public List<TodoItem> all() { return new ArrayList<>(items); }
}
class TodoViewModel {
private final TodoRepository repository;
private final List<Runnable> listeners = new ArrayList<>();
public TodoViewModel(TodoRepository repository) {
this.repository = repository;
}
public void addTodo(String text) {
repository.add(text);
notifyListeners();
}
public void toggle(int id) {
repository.toggle(id);
notifyListeners();
}
public List<TodoItem> getItems() { return repository.all(); }
public int getCompletedCount() {
return (int) repository.all().stream().filter(TodoItem::isDone).count();
}
public void subscribe(Runnable listener) { listeners.add(listener); }
private void notifyListeners() { listeners.forEach(Runnable::run); }
}
class TodoConsoleView {
private final TodoViewModel viewModel;
public TodoConsoleView(TodoViewModel viewModel) {
this.viewModel = viewModel;
this.viewModel.subscribe(this::render);
}
public void render() {
System.out.println("\n--- Todo List ---");
for (TodoItem item : viewModel.getItems()) {
System.out.println((item.isDone() ? "[x] " : "[ ] ") + item.getText());
}
System.out.println("Completed: " + viewModel.getCompletedCount());
}
public void onAdd(String text) { viewModel.addTodo(text); }
public void onToggle(int id) { viewModel.toggle(id); }
}
// Usage
TodoRepository repo = new TodoRepository();
TodoViewModel vm = new TodoViewModel(repo);
TodoConsoleView view = new TodoConsoleView(vm);
view.onAdd("Buy groceries");
view.onAdd("Walk the dog");
view.onToggle(1);
JavaScript
class TodoItem {
constructor(id, text) {
this.id = id;
this.text = text;
this.done = false;
}
}
class TodoRepository {
constructor() {
this.items = [];
this.nextId = 1;
}
add(text) {
const item = new TodoItem(this.nextId++, text);
this.items.push(item);
return item;
}
toggle(id) {
const item = this.items.find(i => i.id === id);
if (item) item.done = !item.done;
}
all() {
return this.items;
}
}
class TodoViewModel {
constructor(repository) {
this.repository = repository;
this.listeners = [];
}
addTodo(text) {
this.repository.add(text);
this.notify();
}
toggle(id) {
this.repository.toggle(id);
this.notify();
}
get items() {
return this.repository.all();
}
get completedCount() {
return this.items.filter(i => i.done).length;
}
subscribe(listener) {
this.listeners.push(listener);
}
notify() {
this.listeners.forEach(l => l());
}
}
class TodoConsoleView {
constructor(viewModel) {
this.viewModel = viewModel;
this.viewModel.subscribe(() => this.render());
}
render() {
console.log('\n--- Todo List ---');
for (const item of this.viewModel.items) {
console.log(`${item.done ? '[x]' : '[ ]'} ${item.text}`);
}
console.log(`Completed: ${this.viewModel.completedCount}`);
}
onAdd(text) {
this.viewModel.addTodo(text);
}
onToggle(id) {
this.viewModel.toggle(id);
}
}
// Usage
const repo = new TodoRepository();
const vm = new TodoViewModel(repo);
const view = new TodoConsoleView(vm);
view.onAdd('Buy groceries');
view.onAdd('Walk the dog');
view.onToggle(1);
Explanation
At the heart of MVVM is data binding. The Model keeps data and business rules and knows nothing about the UI. The ViewModel exposes observable properties and commands, transforming Model data into formats the View can use. The View then binds to those properties declaratively, so when the ViewModel changes, the View updates automatically.
In frameworks like Vue or WPF, that binding is handled for you. The examples above use manual subscription so the mechanic is visible, but in a real framework the plumbing is hidden.
Variants
| Variant | Direction | Use case | Example |
|---|---|---|---|
| One-way | VM → View | Read-only displays, dashboards, live tickers | <span>{{ price }}</span> in Vue |
| Two-way | VM ↔ View | Forms, input fields, editable grids | <input v-model="name"> in Vue |
| Command | View → VM | Button clicks, menu actions, navigation | Command={Binding Save} in WPF |
| Computed | Derived | Totals, filtered lists, formatted labels | get completedCount() in JS |
If you’re coming from MVC, think of one-way binding as the controller pushing data to the view — except in MVVM there’s no controller, just the ViewModel exposing properties.
Best Practices
- Keep the ViewModel framework-agnostic. It shouldn’t import UI toolkit classes, so you can unit test it without a browser or device.
- Use observable properties. The ViewModel has to notify the View whenever state changes, otherwise the View stays stale.
- Avoid business logic in the ViewModel. Delegate to the Model or a service layer.
- Pair one ViewModel with one View. Sharing one across unrelated screens usually couples them.
- Expose commands, not callbacks. Have the View call
viewModel.submit()instead of passing a function around.
Common Mistakes
- Putting view logic in the ViewModel. Colors, fonts, and layout decisions belong in the View.
- Forgetting to notify. If the ViewModel changes and nobody is notified, the View stays stale.
- Letting the ViewModel directly manipulate the View. The ViewModel should only expose state; the View binds to it.
- Creating two-way binding loops. A change in the View updates the ViewModel, which updates the View, which updates the ViewModel again.
- Building monster ViewModels. Once a ViewModel has a huge list of properties, it becomes a maintenance headache. Break it apart by feature or screen — once a ViewModel has 20+ properties, it’s time to split.
Real-World Examples
WPF / .NET 8+
WPF’s XAML uses {Binding Path=UserName} to wire a UI control directly to a ViewModel property.
INotifyPropertyChanged then triggers the update. .NET 8+ added source generators that reduce boilerplate
for INotifyPropertyChanged — check out the [ObservableProperty] attribute from CommunityToolkit.Mvvm.
Vue.js 3.4+
In Vue 3.4+, templates bind to reactive data: <input v-model="message">. The data() or setup() function
plays the ViewModel role, and the template is the View. Vue’s reactivity system (Proxy-based since 3.0) handles
the binding plumbing for you.
Android Jetpack Compose 1.6+
ViewModel + LiveData (or StateFlow) + Data Binding form Android’s MVVM stack. The ViewModel survives
configuration changes like screen rotation. With Compose 1.6+, XML data binding becomes optional — just call
collectAsState() and you’re observing StateFlow right inside your composables.
React 18+
React 18+ doesn’t call itself MVVM, but the mapping is close: custom hooks (useState, useReducer, useContext)
act as ViewModels, and JSX is the View. Libraries like Jotai and Zustand formalize this split with stores that
components subscribe to.
Summary
- MVVM splits UI into Model (data + business logic), ViewModel (observable state + commands), and View (declarative UI).
- Data binding is the core mechanic — without it, you’re closer to MVP.
- One-way binding for read-only displays, two-way for forms, commands for actions, computed for derived values.
- Keep the ViewModel framework-agnostic so you can unit test it without a browser or device.
- Supported across WPF (.NET 8+), Vue 3.4+, Angular 17+, Jetpack Compose 1.6+, and React 18+ (via hooks).
- Watch out for binding loops, monster ViewModels, and view logic leaking into the ViewModel.
Companion examples (Python, Java, JavaScript with tests) are available in the companion repository.
See Also
Frequently Asked Questions
What is the difference between MVVM and MVP?
In MVP the View calls explicit methods through an interface. MVVM uses declarative data binding where the ViewModel exposes properties that the View observes.
Does MVVM require a binding framework?
Strictly speaking, yes. Without binding, you're basically back to MVP. That said, a simple manual subscription can approximate binding if you don't have a framework handy.
Can I use MVVM with React?
React's hooks (useState, useReducer) and context API implement MVVM concepts. Custom hooks often serve as
ViewModels.
Is this pattern suitable for small projects?
With only a few components, MVVM is usually overkill. Start simple and let the pattern appear once the code is actually asking for it.
How do I test a ViewModel without a View?
That's the whole point of MVVM — the ViewModel has no UI dependencies, so you instantiate it in a unit test,
call methods, and assert on properties. In Python, I'd reach for pytest with a fake repository. In Java,
JUnit plus a mock TodoRepository does the job. In JS, Jest with a stub is all you need. No browser, no
device, no DOM — just pure logic tests.
What's the difference between MVVM and Flux/Redux?
Redux runs a single store with unidirectional data flow: action → reducer → state → view. MVVM gives you one ViewModel per screen, each holding its own state, with two-way binding. Redux tends to be more predictable for complex state; MVVM feels more natural when you're building form-heavy UIs. Some teams run both — Redux for global state, MVVM for local screen state.
Can I partially apply this pattern?
Yes. Many teams adopt patterns incrementally — start with the core idea and add sophistication only where it matters. Treat MVVM as a rough guide rather than a blueprint you've got to follow word for word.
Related Resources
Model-View-Presenter (MVP) Pattern
Separate presentation logic from the view by introducing a presenter that intermediates between the model and a passive view, enabling testable UI code.
PatternMVC Pattern
Separate application into Model, View, and Controller components. An architectural design pattern for organized, maintainable code.
PatternObserver Pattern
Define a subscription mechanism to notify multiple objects about events. A behavioral design pattern for event-driven communication.
PatternDependency Injection Pattern
Supply dependencies from outside rather than creating them internally. An architectural pattern for decoupled, testable code.
PatternCommand Pattern
Encapsulate a request as an object, letting you parameterize clients with queues, logs, and undoable operations. A behavioral design pattern.
PatternComposite Pattern for UI Component Trees in React
Use the Composite pattern to compose objects into tree structures, letting clients treat individual objects and compositions uniformly in UI component hierarchies