100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
.NET

Common MVVM Pitfalls

The recurring mistakes teams make when adopting MVVM, from bloated ViewModels to memory leaks and broken separation of concerns.

Practical MVVMIntermediate8 min readJul 10, 2026
Analogies

The Massive ViewModel Anti-Pattern

The most common MVVM failure is the 'Massive ViewModel', where business logic, input validation, data formatting, and even navigation decisions all pile up inside a single ViewModel class — effectively recreating the 'Massive View Controller' problem MVVM was adopted to solve, just one layer removed. The fix is extracting logic into dedicated use cases, validators, or formatter classes, leaving the ViewModel as a thin coordinator that wires those pieces to observable state.

🏏

Cricket analogy: It's like an all-rounder trying to open the batting, bowl the powerplay overs, and captain the side all at once — eventually one role suffers, so a good team distributes those responsibilities.

Leaking View References

Storing a direct reference to an Activity, Fragment, Context, or UIViewController inside a ViewModel field is a frequent source of memory leaks, because the ViewModel typically outlives a single View instance across configuration changes, and the retained reference prevents the View from being garbage collected. The fix is communicating through events, such as a SharedFlow or Channel of one-off UI events, or an Application-scoped context when platform access is unavoidable, never storing a View callback directly on the ViewModel.

🏏

Cricket analogy: It's like a bowler refusing to hand the ball back to the umpire between overs, holding onto it long after their spell has ended and disrupting the next bowler's setup.

kotlin
// BAD: leaks the Activity and breaks platform independence
class ProfileViewModel(private val activity: MainActivity) : ViewModel() {
    fun onSaveClicked() {
        activity.showToast("Saved!")
    }
}

// BETTER: emit a one-off event, let the View decide how to render it
class ProfileViewModel(private val repository: ProfileRepository) : ViewModel() {
    private val _events = MutableSharedFlow<ProfileEvent>()
    val events: SharedFlow<ProfileEvent> = _events

    fun onSaveClicked() = viewModelScope.launch {
        repository.save()
        _events.emit(ProfileEvent.Saved)
    }
}

Overusing Two-Way Binding

Binding every field bidirectionally without any validation or gating logic can produce cascading, hard-to-trace updates — for example, a WPF property setter that raises INotifyPropertyChanged, which triggers a converter, which updates another bound property, which raises another change notification. The fix is being selective about which fields actually need two-way binding and preferring explicit command-driven mutation for anything that affects multiple pieces of state at once.

🏏

Cricket analogy: It's like a scoreboard operator manually recalculating the run rate, required run rate, and win probability separately every single ball instead of deriving them all from one authoritative score update.

Keep navigation decisions out of View code by exposing a navigation event (a sealed class or an event stream) from the ViewModel that the View observes and acts on. This keeps the ViewModel platform-agnostic while still letting it decide when navigation should happen.

Ignoring Lifecycle and State Restoration

Assuming a ViewModel simply survives forever ignores two real lifecycle events: configuration changes (screen rotation) and process death, where the operating system reclaims memory and later recreates the ViewModel from scratch. Without saving critical in-progress state to something like Android's SavedStateHandle, or an equivalent persistence mechanism on other platforms, users lose form input, scroll position, or multi-step wizard progress the moment the OS reclaims memory in the background.

🏏

Cricket analogy: It's like a rain delay wiping a batter's mental count of balls faced if nobody bothered to record it on the official scorecard — the moment play resumes, that context is gone.

These pitfalls rarely show up in day-to-day manual testing because a freshly launched app on a fast device hides memory leaks and state-loss bugs. They tend to surface only in production, on low-memory devices or after extended background time, so proactive testing and profiling matter more than they might seem to during development.

  • The Massive ViewModel anti-pattern recreates the Massive View Controller problem MVVM was meant to fix — extract logic into use cases and formatters.
  • Never store a direct reference to an Activity/Fragment/Context/UIViewController inside a ViewModel field; it causes memory leaks.
  • Communicate one-off UI actions through events (SharedFlow, Channel) rather than direct method calls into the View.
  • Unrestricted two-way binding on every field can trigger cascading, hard-to-trace change notifications.
  • Keep navigation decisions in the ViewModel as observable events, not as direct View manipulation.
  • Persist critical in-progress state (e.g., via SavedStateHandle) to survive configuration changes and process death.
  • Many MVVM pitfalls are invisible in quick manual testing and only surface under production conditions like low memory.

Practice what you learned

Was this page helpful?

Topics covered

#NET#MVVMDesignPatternStudyNotes#MicrosoftTechnologies#CommonMVVMPitfalls#Common#MVVM#Pitfalls#Massive#StudyNotes#SkillVeris