MVVM: Avoiding the Massive ViewModel & When It Fits
The "Massive ViewModel" Anti-Pattern
A ViewModel accumulating too much unrelated logic and state becomes hard to understand, test, and maintain -- essentially reproducing the same bloat problem MVVM was meant to solve, just relocated to a different layer.
Keeping ViewModels Focused
Split a large screen's logic into smaller, composed ViewModels rather than one monolith.
Extract genuinely reusable logic into separate service/use-case classes the ViewModel delegates to.
Inject dependencies (repositories, services) rather than having the ViewModel construct them internally -- keeps it swappable for testing.
Exposing Async State
@Published var isLoading = false
@Published var errorMessage: String?
@Published var results: [Item] = []
func search(query: String) async {
isLoading = true
do {
results = try await repository.search(query)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
// The View reflects each state transition automatically
// via data binding -- no manual polling neededRedesigning a View Without Touching the ViewModel
Since the ViewModel holds no direct references to specific UI controls, a significant visual redesign can often be done by only changing the View layer -- leaving the tested ViewModel logic completely untouched.
When MVVM Might Be Overkill
For a genuinely trivial screen with minimal state, the ceremony of defining a separate ViewModel class, bindings, and commands can add more boilerplate than value -- MVVM's benefits scale with UI/state complexity, not a fixed rule to apply everywhere regardless of need.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free