Learn

Architecture & pattern guides

Read the idea, build it on a Grid, prove it in a challenge. Short, practical guides — illustrated with RodGrid's own nodes — on system design, design patterns, flow modeling, and how to get the most out of the app.

All articles

MVP: Model, View, Presenter

MVP moves a screen's decisions out of the View into a Presenter that drives the View through a small interface, so a test can check them against a fake View. The price is pushing every change by hand, and keeping the Presenter from calling a View that is already gone.

Where is the test for the overweight warning?

The question came up in a code review on a parcel-shipping app. Its booking screen takes a parcel's weight and destination, shows a price, warns when the parcel is too heavy for standard delivery, and enables Book pickup only when the form is complete. There was no test. The warning is decided inside the weight field's change handler, which reads the field, parses it, asks the rate table for a price, writes "€14.90" into a label, shows or hides a banner and toggles the button. To test it, you start the screen, type into a real field and read a real label.

The parcel rules themselves are fine: the rate table and the 30 kg limit for standard delivery are plain code with their own tests. What has none is the layer between those rules and the pixels, the presentation logic: writing 32 as "32 kg", choosing which warning to show, deciding when the button may be pressed. It is small, it changes with every redesign, and it is often the code that breaks. In classic MVC it often ends up in the View (MVC: Model, View, Controller); UI architecture: keeping screens apart from domain rules compares where each pattern of the family puts it.

The screen behind an interface

MVP, Model-View-Presenter, moves that logic out of the View and into a Presenter. The Model is the parcel domain: the rate table and the weight limits. The View is the screen. The Presenter sits between them and never touches a widget. It talks to the View through a small interface the View implements, with methods such as showPrice(text), showOverweightWarning(visible) and setBookEnabled(enabled), and the View calls back with what the user did: weightChanged(text), bookClicked().

In the Passive View variant, the one followed here, the View decides nothing. It reports what the user did, as raw text, and shows exactly what it is told. When the weight of a parcel to Lyon changes to 32, the View hands the Presenter the string, weightChanged("32"), and has no opinion about it. The Presenter parses it and asks the Model for a quote, and the Model answers that 32 kg is over the standard limit, so the parcel goes as freight at €42. The Presenter turns that answer into three instructions, showPrice("€42"), showOverweightWarning(true) and setBookEnabled(true), and the View carries them out: a label, a banner, a button.

One screen, one Presenter

The View forwards what the user did; the Presenter decides and answers through the View's interface. The Model never learns that a screen exists.

A fake View in the test

Because the Presenter knows the View only as that interface, a test can hand it a fake View that records the calls it receives: build the Presenter with the fake and a Model holding a parcel to Lyon, call weightChanged("32"), and assert that the fake recorded showOverweightWarning(true) and showPrice("€42"). The check that needed a running screen now runs in milliseconds, with no rendering, and, because the quote comes from plain code rather than a server, no timing to go wrong.

The awkward cases become one call and one assertion each: an empty field, a weight typed with its unit, a parcel of exactly 30 kg, which still goes standard. What the test does not reach is the View itself, whether showOverweightWarning(true) really makes the banner appear. MVP accepts that gap on purpose: the View is kept so thin that there is little in it to get wrong, and a few end-to-end checks cover it.

How much the View may decide

Martin Fowler names a second variant, Supervising Controller, in which the View uses the UI framework's data binding to fill plain fields such as the weight straight from the Model, and the Presenter keeps input handling and the harder logic, such as which warning to show. It writes less code, but whatever the binding does happens outside the Presenter, where a fake View cannot see it. Passive View puts every presentation decision where a test can reach it, at the price of writing every update by hand; binding the whole View to a presentation object instead, with no View interface, is the step MVVM: Model, View, ViewModel takes.

Every change, pushed by hand

That price grows with the screen. The parcel form has three outputs; a real booking screen soon has twenty, from a delivery date to a customs notice. Each one is a View method, and a call the Presenter must remember to make every time anything it depends on changes. Thirty fields means thirty handlers, each with its own list of outputs to refresh. Add a promotion-code field that lowers the price, forget to call showPrice from its handler, and the screen shows the old price next to an accepted code. No compiler flags a missing line, and the fake View test catches it only if someone wrote a test for that path.

A common defence is a single private method that recomputes every output from the Model and pushes all of them after any input changes. It makes some calls that change nothing on screen, and in exchange a new field's handler only has to call it, instead of remembering which outputs it affects.

A Presenter outlives its View

MVP usually pairs one Presenter with one View, and the two live on different clocks. The View belongs to the UI framework, which creates and destroys it: the user navigates away, or, on Android, rotating the phone by default destroys the screen's Activity and creates a new one. The Presenter is ordinary code, and it may still be working when that happens:

  1. The user taps Book pickup, and the View calls presenter.bookClicked().
  2. The Presenter sends the booking to the server and waits for the answer.
  3. The user leaves the screen, and the framework tears the View down.
  4. The answer arrives, and the Presenter calls view.showBookingConfirmed() on a View that no longer exists.

Depending on the platform, that call crashes or does nothing visible, and as long as the Presenter keeps a strong reference to it, the dead screen and everything it holds stay in memory. The usual remedy is bookkeeping: the View attaches itself to the Presenter when it appears and detaches when it goes, the Presenter checks for a View before calling one, and work only that screen needed is cancelled on detach. If the booking must survive a rotation, something has to keep the Presenter alive across it and attach it to the new View, then bring that View up to date, including any answer that arrived in between.

Sketching the booking screen

The parcel screen makes a good free exercise on a Grid of your own; it is practice, not a Challenge. Put down a View, a Presenter and the parcel rules, and write the interface's methods on the edge from the Presenter to the View. Then add the booking request and its late answer, and mark the one place where the Presenter has to ask whether its View is still there.

The Model-View-ViewModel Challenge is worth doing next to it, as a contrast rather than an MVP exercise. It wants the same thing, screen logic you can test without a screen, and gets it with the opposite arrow: there the ViewModel may not hold the View at all. Lay the two drawings side by side and look first at the edge between the View and the middle object: in MVP the Presenter calls the View, in MVVM the View binds to a ViewModel that never learns it exists.

Practice this