MVVM is a pattern that separates view, data, and presentation logic.
When an application grows, separating files is not enough: you also need to decide what responsibility belongs to each piece.
In Flutter, the undisputed king (thanks to its similarity with how Provider works) is MVVM (Model - View - ViewModel).
Don’t be scared by the acronym. It’s simply a rule to know where to write each line of code so you don’t mix apples and oranges.
What is each piece?
Let’s map the theoretical concepts to what we already know about Flutter:
- What is it? Pure data. Classes that represent “things” from the real world.
- What does it do? Represents data and, depending on the design, can include domain-specific rules. It doesn’t know the UI.
- In your code: These are your classes with
fromJsonandtoJson. - Example:
class Product { final String name; final double price; ... }
- What is it? The User Interface. What the user sees and touches.
- What does it do? Renders the screen based on the current state and captures user clicks.
- In your code: These are your
Widgets(Screens). - Example:
class CartScreen extends StatelessWidget { ... }
- What is it? The intermediary. The brain.
- What does it do?
- Stores the State (the list of products, whether it’s loading, etc.).
- Contains the Business Logic (calculating the total, validating a coupon).
- Exposes methods for the View to call.
- In your code: It can be a
ChangeNotifierclass that we expose via Provider.
Workflow
In this variant, the view receives the state it needs from the ViewModel and sends user actions to it.
User taps a button on the View (onTap).
View calls a function on the ViewModel (provider.add()).
ViewModel processes the logic, updates its internal Models, and notifies (notifyListeners).
View listens to the change and redraws itself automatically.
Practical example: a shopping cart
Let’s see what this looks like in actual code.
Dumb data. Just structure.
class Item {
final String name;
final double price;
Item({required this.name, required this.price});
}This is where the logic lives. If tomorrow you decide the cart has a maximum of 10 items, you change this file. The view doesn’t even know.
import 'package:flutter/material.dart';
import '../models/item.dart';
class CartProvider extends ChangeNotifier { // This is the ViewModel
// Private state
final List<Item> _items = [];
// Getters for the view (Public state)
List<Item> get items => List.unmodifiable(_items);
double get totalPrice {
return _items.fold(0, (total, item) => total + item.price);
}
// Business Logic (Actions)
void addItem(Item item) {
_items.add(item);
notifyListeners(); // Notifies the View!
}
void clearCart() {
_items.clear();
notifyListeners();
}
}The view is “dumb”. It doesn’t calculate sums. It only shows what the ViewModel tells it.
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Listen to the ViewModel (Watch)
final cart = context.watch<CartProvider>();
return Scaffold(
appBar: AppBar(title: Text("My Cart")),
body: Column(
children: [
// Item list
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (ctx, i) => ListTile(
title: Text(cart.items[i].name),
trailing: Text("\$${cart.items[i].price}"),
),
),
),
// Total and Button
Container(
padding: EdgeInsets.all(20),
color: Colors.green[100],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Total: \$${cart.totalPrice}", // The calculation was done by the ViewModel
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
ElevatedButton(
onPressed: () {
// User action -> Call to ViewModel
context.read<CartProvider>().clearCart();
},
child: Text("Pay"),
)
],
),
)
],
),
);
}
}What this separation brings
You might think: “Why create three files if I can do everything in the Widget?”
- Testability: You can test your
CartProvider(verify the sum works) without having to start an emulator or render pixels. - Maintainability: If the designer changes the screen from red to blue, you only touch the View. If the business changes the tax rule, you only touch the ViewModel.
- Reusability: You can use the same
CartProviderto show the number of items in a small icon on theHomeScreen.