MVP, Activities & Places

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against GWT 2.13.1 (released 2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, the Jakarta Servlet 6 server of the Bookshelf scenario and the gwt-user 2.13.1 sources, checked on 2026-10-09. Everything here is client code: the places, tokenizers, activities, views and the generated PlaceHistoryMapper are Java compiled to JavaScript, and the presenters are written so that they also run on a plain JVM; the only build requirements are two <inherits> lines (and, optionally, Dagger on the annotation processor path). The server is involved only through the GWT-RPC CatalogService that the activities call. Every output block was produced by compiling and running a private copy of the Bookshelf scenario: plain JUnit tests on JDK 21 for the navigation flow, GWTTestCase tests in HtmlUnit for the generated mapper, probe modules compiled with the 2.13.1 compiler for the generator messages, and the production build served by Jetty on port 8922 and driven in the embedded Chrome 152 browser pane for the URL and dialog behaviour. Where a statement comes only from the Developer Guide or the 2.13.1 source, the text says so.

A single-page application needs three things that the browser does not give it for free: an address for every screen (so Back, Reload and bookmarks work), a way to switch the screen without reloading the page, and a way to keep the logic that decides what to show out of the widgets that show it. GWT answers the first two with the Activities and Places framework, introduced in GWT 2.1, and the third with the Model-View-Presenter (MVP) pattern. The two are independent, but they fit together so well that the Developer Guide teaches them as one.

The MVP pattern in GWT

MVP splits a screen into three roles. The model is the data and the business rules (in the Bookshelf scenario the shared Book class and the server behind CatalogService). The view is a thin layer of widgets that shows data and reports user gestures, and nothing else. The presenter holds the logic: it fetches the model, tells the view what to display, and reacts to what the view reports.

GWT has a stronger reason than most platforms to prefer this split, and the GWT team’s own MVP articles state it: a test of anything that touches a widget needs the DOM and a JavaScript engine, which means a GWTTestCase that starts a (headless) browser, which is slow. If the widgets are hidden behind an interface and the presenter never touches them, the presenter is plain Java and a plain JUnit test on the JVM is enough. The framework classes used on this page follow the same rule: the GWT sources ship JRE test suites for the activity and place packages (ActivityJreSuite, PlaceJreSuite) next to the browser suites, and the tests in this page run PlaceController and ActivityManager on the JVM.

Table 1. The three roles in the Bookshelf application
Role Bookshelf classes Rules

Model

Book, BookPage, CatalogServiceAsync (shared and server code)

No widgets. Shared classes can run on the client and on the server.

View

the interfaces CatalogView, BookView, EditView, BreadcrumbView and their widget implementations (BookViewImpl, BookEditor, …​)

Interface first. The interface may extend IsWidget but mentions no widget class. The implementation never calls the server and never decides where to go.

Presenter

CatalogActivity, BookActivity, EditActivity (and CatalogPresenter, the part of the catalogue logic that needs no navigation)

Talks to views only through their interfaces. Talks to the rest of the application through the ClientFactory. Contains no GWT.create() call of its own, so it can be created in a JUnit test.

GWT does not impose a base class for any of this; there is no View or Presenter type in the toolkit. The only helper is the IsWidget interface (one method, asWidget()), which most widgets and Composite implement. A view that extends IsWidget can be handed to a panel, and a fake view in a test returns null from asWidget().

There are two common ways to let the presenter see the view, and both appear in GWT’s own material.

  • The presenter owns a Display interface that exposes the widgets as capabilities (HasClickHandlers getSaveButton(), HasValue<String> getTitleBox()). The presenter registers its own handlers on those. This is the style of the first MVP article (2010). It keeps the view almost empty, but the presenter ends up knowing which gestures exist.

  • The view owns a Presenter callback interface (void onSave(Book edited)) and calls it from its handlers; the presenter calls view.setPresenter(this). This is the style of the second MVP article and of the Activities and Places guide, and it works well with UiBinder @UiHandler methods that simply delegate. It is the style used here.

The MVP pattern is not specific to GWT, and Android documentation compares it with MVC, MVVM and MVI in the same terms: see Android app architecture for the comparison table.

The parts of the framework

Activities and Places is a handful of small classes in two packages, com.google.gwt.place.shared and com.google.gwt.activity.shared. Each has one job, and nothing is hidden: you create them yourself in onModuleLoad().

Table 2. The classes, in the order a place change meets them
Class or annotation Job Who writes it

Place

A Java object that stands for a bookmarkable state of the UI (BookPlace(7)).

You: one subclass per kind of screen, with equals() and hashCode().

PlaceTokenizer<P>

Converts one kind of place to and from a short string (the token).

You: one per place, usually a static nested class.

@Prefix

Names the part of the token before the colon.

You, on the tokenizer (or on a factory method).

PlaceHistoryMapper, @WithTokenizers

Lists the tokenizers; GWT generates the implementation that maps a token to the right tokenizer and back.

You write an empty interface; the compiler writes the class.

PlaceController

Moves the application to a place (goTo), after asking the current activity whether it may stop.

GWT: you create one and share it.

PlaceHistoryHandler

Keeps the browser’s history token and the current place in step, in both directions.

GWT: you create one and register it.

Activity, AbstractActivity

What the user is doing in a place: start, mayStop, onCancel, onStop.

You: one per screen. It is the presenter.

ActivityMapper

Chooses the activity for a place.

You: a few lines of instanceof.

ActivityManager

Starts and stops activities inside one display region.

GWT: you create one per region.

EventBus

Carries PlaceChangeRequestEvent and PlaceChangeEvent, and your own application events.

GWT: SimpleEventBus.

ClientFactory (your name)

Gives activities the bus, the controller, the services and the views.

You (or Dagger).

flowchart LR user["User: link, Back button, bookmark, typed URL"] hist["History: location hash"] phh["PlaceHistoryHandler"] phm["PlaceHistoryMapper: tokens"] pc["PlaceController"] bus(("EventBus")) am["ActivityManager"] amap["ActivityMapper"] act["Activity: the presenter"] view["View: interface plus widgets"] region["AcceptsOneWidget: display region"] user --> hist hist -->|"ValueChangeEvent"| phh phh --> phm phh -->|"goTo(place)"| pc pc -->|"PlaceChangeRequestEvent, PlaceChangeEvent"| bus bus --> am bus -->|"PlaceChangeEvent: new token"| phh am --> amap amap --> act act --> view act -->|"setWidget(view)"| region

The figure shows the two directions. A click on a link, Back or a bookmark changes the history token; PlaceHistoryHandler turns it into a place with the mapper and hands it to the controller. Code that calls goTo() itself starts in the middle of the figure; the controller fires the events, ActivityManager swaps the activity, and PlaceHistoryHandler listens to the same event to write the new token. Notice that the event bus is the only thing the parts have in common.

Setting the module up

Neither package is inherited by com.google.gwt.user.User, so the application module names them. Both modules are in gwt-user, so there is no new Maven dependency.

Bookshelf.gwt.xml (module.gwt.xml of the client module)
<module>
  <inherits name="com.google.gwt.user.User" />
  <inherits name="com.google.gwt.place.Place" />
  <inherits name="com.google.gwt.activity.Activity" />

  <source path="client" />
  <source path="shared" />

  <entry-point class="com.example.bookshelf.client.App" />
</module>

Place.gwt.xml inherits Core, Event and DOM, adds the source paths shared and impl, and registers the generator (PlaceHistoryMapperGenerator) for every type that is assignable to PlaceHistoryMapper. Activity.gwt.xml inherits Place and UI and adds the source path shared. Inheriting only Activity is therefore enough to use both, but inheriting both makes the dependency visible. The shared package name only means that the classes avoid browser-only code in their API; the code still depends on Window and History, so it is client code.

Views as interfaces

A view is whatever the presenter shows. Because widget classes cannot be created on a JVM, the presenter must see the view through an interface. The Bookshelf book detail screen has this contract:

The view contract, with the presenter callback nested inside (BookView.java)
public interface BookView extends IsWidget {
  interface Presenter {
    void onEdit();

    void onBack();
  }

  void setPresenter(Presenter presenter);

  void show(Book book);

  void clear();
}

And this is its widget implementation. It uses setText for the data that came from the server, never setHTML (see the security notes at the end).

The widget implementation (BookViewImpl.java)
public class BookViewImpl extends Composite implements BookView {
  private final AppMessages msg;
  private final Label heading = new Label();
  private final Label details = new Label();
  private Presenter presenter;

  public BookViewImpl(AppMessages msg) {
    this.msg = msg;
    Button edit = new Button("Edit");
    edit.addClickHandler(e -> presenter.onEdit());
    Button back = new Button("Back");
    back.addClickHandler(e -> presenter.onBack());
    FlowPanel panel = new FlowPanel();
    panel.add(heading);
    panel.add(details);
    panel.add(edit);
    panel.add(back);
    initWidget(panel);
  }

  @Override
  public void setPresenter(Presenter presenter) { this.presenter = presenter; }

  @Override
  public void show(Book book) {
    heading.setText(msg.bookLine(book.getTitle(), book.getAuthor()));
    details.setText(book.getYear() + ", " + msg.copies(book.getCopies()));
  }

  @Override
  public void clear() {
    heading.setText("");
    details.setText("");
  }
}

Three design points come from the guide and from the second MVP article:

  • The implementation extends Composite, so the rest of the application does not depend on which widget is at the root.

  • Widgets are expensive to create (DOM calls), while activities are cheap, so views are created once and reused; activities are created for every place change and thrown away. Reuse is what makes clear() and the activity-side guards in the pitfalls below necessary.

  • The second MVP article’s "dumb view" advice goes one step further: the presenter hands the view the model objects, or a list of them, and the view decides how to render them (the article passes column definitions to a generic list view). The Bookshelf CatalogView does this with a DataGrid and an AsyncDataProvider: the presenter calls showRows(start, books, total) and never sees a column.

  • A UiBinder view reports gestures to the presenter with @UiHandler methods that only delegate (presenter.onSave(driver.flush())), as the BookEditor form of the Editors page does; see UiBinder for the template side.

One naming trap: with a lambda, new Button("Edit", e -> presenter.onEdit()) does not compile. Button still has a constructor that takes the deprecated ClickListener next to the one that takes a ClickHandler, and javac reports reference to Button is ambiguous, followed by both constructor Button(java.lang.String,com.google.gwt.user.client.ui.ClickListener) in com.google.gwt.user.client.ui.Button and constructor Button(java.lang.String,com.google.gwt.event.dom.client.ClickHandler) in com.google.gwt.user.client.ui.Button match. Create the button and call addClickHandler as above (see Events & Handlers for the migration from listeners).

Places and tokenizers

A place is a Java object that stands for a state of the UI that the user can come back to. The base class Place is abstract and empty; its Javadoc asks implementations to provide correct equals() and hashCode(), and three pieces of the framework depend on it, as the pitfalls table shows (the same-place check in PlaceController, the same-activity check in ActivityManager and the default-place check in PlaceHistoryHandler).

A tokenizer turns a place into the text that goes after the colon in the address bar and back. The Bookshelf book detail place carries a book id:

A place and its tokenizer (BookPlace.java)
public class BookPlace extends Place {
  private final long id;

  public BookPlace(long id) { this.id = id; }

  public long getId() { return id; }

  @Override
  public boolean equals(Object o) {
    return o instanceof BookPlace && id == ((BookPlace) o).id;
  }

  @Override
  public int hashCode() { return Long.hashCode(id); }

  @Override
  public String toString() { return "BookPlace[id=" + id + "]"; }

  @Prefix("book")
  public static class Tokenizer implements PlaceTokenizer<BookPlace> {
    @Override
    public BookPlace getPlace(String token) {
      try {
        return new BookPlace(Long.parseLong(token));
      } catch (NumberFormatException e) {
        return null;   // not a number: PlaceHistoryHandler falls back to the default place
      }
    }

    @Override
    public String getToken(BookPlace place) { return String.valueOf(place.getId()); }
  }
}

The tokenizer is the only code that sees text from the address bar, so it is also where the validation goes: it returns null for a token it cannot parse (the handler then logs a warning and uses the default place, see below) and never trusts the text. A place that carries no state (the admin screen) can return an empty token and a new instance.

The PlaceHistoryMapper

The mapper is the list of all tokenizers. You write an empty interface and annotate it; the compiler generates the class.

AppPlaceHistoryMapper.java
@WithTokenizers({
  CatalogPlace.Tokenizer.class,
  BookPlace.Tokenizer.class,
  EditPlace.Tokenizer.class,
  AdminPlace.Tokenizer.class
})
public interface AppPlaceHistoryMapper extends PlaceHistoryMapper {}

GWT.create(AppPlaceHistoryMapper.class) is resolved by deferred binding to a generated class named after the interface with dots replaced by underscores and Impl appended (AppPlaceHistoryMapperImpl in the same package). It extends AbstractPlaceHistoryMapper and, from the source of that class, works like this:

  • getToken(place) finds the tokenizer by the place’s class (the generated code is a chain of instanceof tests, most derived type first) and returns prefix + ":" + token, or only the token when the prefix is empty. It returns null when no tokenizer handles the place.

  • getPlace(token) splits at the first colon, looks the prefix up, and passes the rest to the tokenizer. A token without a colon has the empty prefix. It returns null when no tokenizer has the prefix.

The prefix comes from @Prefix on the tokenizer class, or on a factory method (below). Without the annotation, the prefix is the name of the place type, getName() of the type in the GWT type oracle. That is the simple class name for a top-level place; the Developer Guide describes it that way. For a nested place class it is Outer.Inner, because the name includes the enclosing class; the probe in the test below produced the token PlaceHistoryGwtTest.LooseDefault:x for a place declared inside a test class. Writing @Prefix explicitly keeps the URLs short, stable across refactorings and independent of the Java class names, which is also the better choice for bookmarks that must survive a rename.

The measured behaviour of the Bookshelf mapper, from a GWTTestCase in HtmlUnit:

Table 3. Token round trips (PlaceHistoryGwtTest, assertions on the generated mapper)
Call Result Why

getToken(new CatalogPlace(20))

catalog:20

prefix catalog, token from the tokenizer

getToken(new BookPlace(7))

book:7

getPlace("book:7")

BookPlace[id=7]

getPlace("catalog:")

CatalogPlace[start=0]

the tokenizer maps the empty token to the first page

getPlace("catalog:abc")

null

the tokenizer returned null for a token it cannot parse

getPlace("book:7:x")

null

split at the first colon, so the tokenizer receives 7:x, which is not a number

getPlace("nope:1")

null

no tokenizer has the prefix nope

getPlace("") and getPlace("7")

null

no tokenizer has the empty prefix

getToken(Place.NOWHERE)

null

no tokenizer handles the place

The compiler rejects an inconsistent mapper. Each of these was reproduced with a probe module (see the pitfalls table for the messages): two tokenizers with the same prefix, a prefix that contains a colon, and two tokenizers for the same place type.

A mapper that needs collaborators

Sometimes a tokenizer needs something from the application (a repository to look up an id, a flag). Instead of @WithTokenizers, the mapper extends PlaceHistoryMapperWithFactory<F> and the generated class asks the factory for the tokenizers. The factory type F is an interface whose public, no-argument methods return PlaceTokenizer subtypes, and @Prefix goes on the method (or on the tokenizer type).

A mapper with a tokenizer factory
public interface TokenizerFactory {
  @Prefix("book")
  BookPlace.Tokenizer books();
}

public interface FactoryMapper extends PlaceHistoryMapperWithFactory<TokenizerFactory> {}

FactoryMapper mapper = GWT.create(FactoryMapper.class);
mapper.setFactory(() -> new BookPlace.Tokenizer());   // any implementation of TokenizerFactory
String token = mapper.getToken(new BookPlace(9));     // "book:9", the factory method was called once

TokenizerFactory has one method, so a lambda is enough for it here. The test that exercises it asserted book:9 and exactly one factory call. Call setFactory before the first use of the mapper.

PlaceController: moving to a place

PlaceController is the one object that changes the current place. Everything that navigates calls goTo: a link handler, an activity after a save, the PlaceHistoryHandler when the user presses Back.

PlaceController places = new PlaceController(eventBus);
places.goTo(new BookPlace(7));
Place here = places.getWhere();   // initially Place.NOWHERE

From the source, goTo(newPlace) does the following:

  1. If getWhere().equals(newPlace), it returns at once. Nothing is fired.

  2. It fires a PlaceChangeRequestEvent on the bus. Any handler may call setWarning(String) on it; only the first non-null warning counts. ActivityManager is such a handler: it sets the warning to the current activity’s mayStop().

  3. If there is no warning, or Delegate.confirm(warning) returns true, the controller stores the new place and fires a PlaceChangeEvent.

  4. Otherwise nothing else happens. The place does not change.

The Delegate has two methods, confirm(String) and addWindowClosingHandler(...); the default implementation calls Window.confirm and Window.addWindowClosingHandler. The constructor registers a closing handler that issues the same request for Place.NOWHERE and, when an activity returns a warning, sets it as the message of the browser’s "leave page?" prompt. So mayStop() also guards closing the tab, not only in-app navigation. Tests pass their own Delegate to the two-argument constructor.

The one-argument constructor is the one the Developer Guide shows, but its EventBus parameter in the guide’s code is the legacy com.google.gwt.event.shared.EventBus. Both legacy-typed constructors of PlaceController are @Deprecated; pass a com.google.web.bindery.event.shared.EventBus, as the Bookshelf factory does.

The Javadoc of PlaceChangeRequestEvent.setWarning says that if the user clicks OK "navigation will be canceled". The code of goTo does the opposite: OK means leave, Cancel means stay. The measured dialog sequence below follows the code.

PlaceHistoryHandler: the address bar

PlaceHistoryHandler is the bridge between the place and the browser history (the History class and the URL fragment); the History & Navigation page covers History itself, so here is only what the bridge does. You give it the mapper, register it with the controller, the bus and a default place, and call handleCurrentHistory() once the application is ready.

AppPlaceHistoryMapper mapper = GWT.create(AppPlaceHistoryMapper.class);
PlaceHistoryHandler history = new PlaceHistoryHandler(mapper);
history.register(placeController, eventBus, new CatalogPlace(0));
// ... build the UI, start the ActivityManagers ...
history.handleCurrentHistory();   // go to the place in the URL, or to the default place

register adds two listeners and returns a registration to remove them again:

  • on PlaceChangeEvent it writes the new token with History.newItem(token, false): the second argument is false, so the write does not fire a history event and does not loop back;

  • on the history ValueChangeEvent it maps the token to a place and calls placeController.goTo(place).

The rules it applies, all measured with an in-memory Historian in the test below:

Table 4. PlaceHistoryHandler rules (from the source, confirmed by the tests)
Situation Result

handleCurrentHistory() with the empty token

goes to the default place, and the handler writes newItem("", false) back

a place change to the default place

the stored token is the empty string, so the default place has a clean URL (goTo(new CatalogPlace(0)) left the token at "")

a place change to BookPlace(7)

the stored token is book:7

the URL changes to a token the mapper does not know (nope:1)

logs Unrecognized history token: nope:1 at WARNING level and goes to the default place

the URL changes to a token the tokenizer rejects (book:abc, the tokenizer returned null)

logs Unrecognized history token: book:abc and goes to the default place

a place the mapper cannot tokenise

logs Place not mapped to a token: <place> and stores an empty token (from the source)

The token is passed through History.encodeHistoryToken (encodeURI, with # written as %23) when it is written and decoded when History.getToken() reads it, so a tokenizer sees and returns plain text.

The handler uses an internal Historian interface (addValueChangeHandler, getToken, newItem) that defaults to History; the two-argument constructor lets a test supply an in-memory one, and the tests on this page do. There is no option to replace the current history entry instead of adding one: every place change adds an entry, which is what makes Back step through the screens.

The same test class also ran the handler against the real History of the simulated browser:

new PlaceHistoryHandler(mapper).register(pc, bus, new CatalogPlace(0));
pc.goTo(new BookPlace(7));
assertEquals("book:7", History.getToken());
History.newItem("edit:3", true);                // like a link, Back or a typed URL
assertEquals(new EditPlace(3), pc.getWhere());

The default place must have equals()

The default place is compared with equals() to decide whether a place is "home" and should be stored as the empty token. A place class without equals() is never equal to a second instance, so a default place such as new LooseDefault() is never recognised. The test navigated to a second instance and the stored token was PlaceHistoryGwtTest.LooseDefault:x (with the default prefix, because the probe’s tokenizer has no @Prefix), where an equals() implementation gives "".

Back, then Cancel: the URL and the screen disagree

When the user presses the browser’s Back button, the address bar changes first and the application is told afterwards. If the current activity refuses to stop and the user clicks Cancel in the confirm dialog, PlaceController leaves the place as it was, and nothing writes the old token back. This was reproduced in the browser: on #edit:3 with an edited title, Back and Cancel left the address bar at #book:3 while the page still showed the edit form.

The fix is small, and the Bookshelf shell includes it: a Delegate that tells the application when the user refused, and a handler that writes the token of the current place again.

RestoringDelegate.java and the wiring in App.java
public class RestoringDelegate extends PlaceController.DefaultDelegate {
  private Runnable onRefused = () -> {};

  public void setOnRefused(Runnable onRefused) { this.onRefused = onRefused; }

  @Override
  public boolean confirm(String message) {
    boolean leave = super.confirm(message);
    if (!leave) {
      onRefused.run();
    }
    return leave;
  }
}

// in onModuleLoad(), after history.register(...):
factory.getDelegate().setOnRefused(() -> {
  Place where = factory.getPlaceController().getWhere();
  History.newItem(defaultPlace.equals(where) ? "" : historyMapper.getToken(where), false);
});

With this in place, the same run ended with #edit:3 in the address bar after Cancel, and a second Back with OK moved to #book:3. The price is one extra history entry, because History.newItem pushes; the framework offers no replace operation here.

Activities

An activity is what the user is doing at a place, and it is the presenter of that screen. An activity contains no widgets of its own; it restores state, sets up, and puts a view into a container. The interface has four methods.

Table 5. The Activity lifecycle
Method When it is called

void start(AcceptsOneWidget panel, EventBus eventBus)

The manager has decided that this activity is current. Fetch data, configure the view and call panel.setWidget(view). It may finish later: it may call setWidget from an RPC callback. The eventBus is a scoped bus: handlers registered on it are removed when the activity stops.

String mayStop()

Before the place changes, and when the window is about to close. Return null to let the user leave, or a message to ask for confirmation.

void onCancel()

The user left before start showed any widget (before setWidget was called). The activity never reached the screen.

void onStop()

After the activity’s widget was removed from the display. The scoped bus has already been emptied.

AbstractActivity implements mayStop() as null and onCancel() and onStop() as empty methods, so most activities override only start. Note that the second parameter of start is the legacy com.google.gwt.event.shared.EventBus type (a ResettableEventBus), so the import in an activity differs from the one in the factory.

The Bookshelf edit activity is the interesting one, because it is the one that can lose work:

EditActivity.java
public class EditActivity extends AbstractActivity implements EditView.Presenter {
  private final ClientFactory factory;
  private final long id;
  private EditView view;
  private boolean running;

  public EditActivity(ClientFactory factory, EditPlace place) {
    this.factory = factory;
    this.id = place.getId();
  }

  @Override
  public void start(AcceptsOneWidget panel, EventBus eventBus) {
    running = true;
    view = factory.getEditView();
    view.setPresenter(this);
    panel.setWidget(view);
    factory.getCatalog().getBook(id, new AsyncCallback<Book>() {
      @Override
      public void onSuccess(Book book) {
        if (running) {
          view.edit(book);
        }
      }

      @Override
      public void onFailure(Throwable caught) {
        if (running) {
          factory.getPlaceController().goTo(new CatalogPlace(0));
        }
      }
    });
  }

  @Override
  public void onSave(Book edited) {
    factory.getCatalog().saveBook(edited, new AsyncCallback<Book>() {
      @Override
      public void onSuccess(Book saved) {
        view.edit(saved);                                              // clears the dirty flag
        factory.getPlaceController().goTo(new BookPlace(saved.getId()));
      }

      @Override
      public void onFailure(Throwable caught) {
        // stay on the form; the user can retry
      }
    });
  }

  /** Asked before a place change and when the window is closed. */
  @Override
  public String mayStop() {
    return view != null && view.isDirty() ? factory.getMessages().discardChanges() : null;
  }

  @Override
  public void onCancel() { running = false; }

  @Override
  public void onStop() {
    running = false;
    view.setPresenter(null);
  }
}

Three habits in that class matter:

  • mayStop() is a courtesy, not a lock. It decides whether to ask; it cannot prevent a stop that the user confirms. Anything that must hold (permissions, validation) belongs on the server.

  • A running flag guards every asynchronous callback. The view is reused, so an RPC answer that arrives after the user has moved on would otherwise write into a view that now belongs to another activity. The measurements in the next sections show it happening.

  • onStop() detaches the activity from the shared view (setPresenter(null)) so that a gesture on a hidden view cannot reach an old presenter.

The "may I stop?" guard needs a flag from the view (isDirty()). The Bookshelf BookEditor keeps it with value-change handlers on its four boxes. A box reports ValueChangeEvent when the browser fires its change event, normally when the box loses focus, so a guard that must also catch the very last keystroke should listen to key events as well (see the events page).

ActivityMapper and ActivityManager

The mapper turns a place into an activity. It is a function from place to activity with the instanceof chain the Developer Guide mentions.

AppActivityMapper.java
public class AppActivityMapper implements ActivityMapper {
  private final ClientFactory factory;

  public AppActivityMapper(ClientFactory factory) { this.factory = factory; }

  @Override
  public Activity getActivity(Place place) {
    if (place instanceof CatalogPlace) {
      return new CatalogActivity(factory, (CatalogPlace) place);
    } else if (place instanceof BookPlace) {
      return new BookActivity(factory, (BookPlace) place);
    } else if (place instanceof EditPlace) {
      return new EditActivity(factory, (EditPlace) place);
    } else if (place instanceof AdminPlace) {
      return new AdminActivity();
    }
    return null;
  }
}

Returning null means "no activity for this place here": the region is emptied. The mapper creates a new activity for every call, as the guide recommends, so no state of a previous visit survives.

ActivityManager is the engine. One manager owns one display region, any object that implements AcceptsOneWidget (SimplePanel, SimpleLayoutPanel, or a class of your own; see Panels & Layout). From the source of ActivityManager, a PlaceChangeEvent does this:

  1. Ask the mapper for the next activity (if setDisplay was never called, or was called with null, the manager does not even ask and the current activity is shut down).

  2. If the mapper returned the same activity object as the current one (equals), do nothing. This is the hook for CachingActivityMapper.

  3. If the previous activity is still starting (it has not called setWidget), call its onCancel().

  4. Otherwise empty the region (setWidget(null)), remove every handler the activity registered on the scoped bus, and call onStop().

  5. Make the new activity current and call start(...) with a protected display: a setWidget call from an activity that is no longer current is silently ignored.

  6. If onStop, onCancel or start threw, collect the exceptions and throw an UmbrellaException after the swap is complete.

Separately, as a PlaceChangeRequestEvent handler, the manager sets the warning to currentActivity.mayStop().

CachingActivityMapper wraps a mapper and reuses the last activity while the place stays equal (it uses equals). FilteredActivityMapper applies a Filter that interprets one place as another before mapping, which is the place to put a rule such as "anonymous users who ask for the admin place get the login place". Both are in com.google.gwt.activity.shared.

Code splitting inside an activity

An activity is the natural place for a code-splitting point, because the manager already copes with an activity that has not shown its widget yet. The Bookshelf admin screen loads its code on the first visit:

AdminActivity.java
public class AdminActivity extends AbstractActivity {
  @Override
  public void start(AcceptsOneWidget panel, EventBus eventBus) {
    GWT.runAsync(new RunAsyncCallback() {
      @Override
      public void onSuccess() { panel.setWidget(new AdminPanel()); }

      @Override
      public void onFailure(Throwable reason) { GWT.log("Code split fragment failed to load", reason); }
    });
  }
}

The production build of the scenario wrote the split fragments under deferredjs/<permutation>/. While the fragment is loading, start has returned without calling setWidget, so a quick click on another link makes the manager call onCancel() (not onStop()), and the late setWidget is ignored, as the SlowActivity scenario below shows. The Code Splitting page covers runAsync and the fragment merging options.

Several display regions

A page often has more than one area whose content depends on the place. The recipe from the guide is one ActivityManager and one mapper per region, all on the same bus: a single place then maps to several activities. The Bookshelf shell has the content area and a breadcrumb line.

The second region (BreadcrumbActivityMapper.java) and its manager (in App.java)
public class BreadcrumbActivityMapper implements ActivityMapper {
  private final ClientFactory factory;

  public BreadcrumbActivityMapper(ClientFactory factory) { this.factory = factory; }

  @Override
  public Activity getActivity(Place place) {
    if (place instanceof CatalogPlace) {
      return new BreadcrumbActivity(factory, "Catalogue");
    } else if (place instanceof BookPlace) {
      return new BreadcrumbActivity(factory, "Catalogue / Book " + ((BookPlace) place).getId());
    } else if (place instanceof EditPlace) {
      return new BreadcrumbActivity(factory, "Catalogue / Book " + ((EditPlace) place).getId() + " / Edit");
    }
    return null;   // no activity: the region is emptied
  }
}

ActivityManager content = new ActivityManager(new AppActivityMapper(factory), eventBus);
content.setDisplay(shell.getContent());
ActivityManager trail = new ActivityManager(new BreadcrumbActivityMapper(factory), eventBus);
trail.setDisplay(shell.getBreadcrumb());

Every manager is asked mayStop() before a change (the measured trace below shows main and trail both answering), and only one non-null warning is shown because setWarning keeps the first.

From place change to view update

This is the whole journey for one navigation, from the click on the "Open" button in the catalogue to the book detail on screen.

sequenceDiagram actor U as User participant C as CatalogActivity participant PC as PlaceController participant Bus as EventBus participant AM as ActivityManager participant MAP as ActivityMapper participant B as BookActivity participant V as BookView participant H as PlaceHistoryHandler participant S as CatalogService U->>C: click Open on a row C->>PC: goTo(new BookPlace(7)) PC->>Bus: fireEvent(PlaceChangeRequestEvent) Bus->>AM: onPlaceChangeRequest AM->>C: mayStop() C-->>AM: null (no warning) PC->>Bus: fireEvent(PlaceChangeEvent) Bus->>AM: onPlaceChange(BookPlace 7) AM->>MAP: getActivity(BookPlace 7) MAP-->>AM: new BookActivity AM->>AM: region.setWidget(null) AM->>C: onStop() after removing its bus handlers AM->>B: start(protected region, scoped bus) B->>V: setPresenter(this) B->>AM: region.setWidget(view) B->>S: getBook(7) Bus->>H: onPlaceChange H->>H: History.newItem("book:7", false) S-->>B: Book B->>V: show(book)

Four things in the figure are easy to miss. First, the controller fires two events: the request (a question, answered by mayStop()) and the change (the fact). Second, the history token is written by a listener on the same PlaceChangeEvent that drives the swap, so the URL follows the application and never leads it, except when the browser changes it first (Back, a link, a typed URL), which goes through PlaceHistoryHandler to goTo the other way. Third, start can return before the screen is complete: the data arrives later and goes through the view interface. Fourth, the old activity is already stopped by then, so a late answer for it is the one that must be dropped.

The Bookshelf navigation test runs this journey on the JVM, with fake views and a recording display, and prints a trace. The tracing mapper wraps each activity so that its lifecycle calls show up; main is the content region and trail is the breadcrumb region.

Trace of the happy path (NavigationFlowTest.happyPath, trimmed to the second navigation)
== goTo(BookPlace(7))
main:CatalogActivity.mayStop() -> null
trail:BreadcrumbActivity.mayStop() -> null
main.setWidget(null)
main:CatalogActivity.onStop()
main:BookActivity.start()
main.setWidget(BookView)
BookView.show(Book 7)
trail.setWidget(null)
trail:BreadcrumbActivity.onStop()
trail:BreadcrumbActivity.start()
BreadcrumbView.setTrail("Catalogue / Book 7")
trail.setWidget(BreadcrumbView)

== goTo(EditPlace(7)) again

The last line is the point of equals(): the second goTo(EditPlace(7)) while already on that place printed nothing, because PlaceController saw an equal place and returned. Also visible: all mayStop() questions come first, then each manager swaps its own region in the order the managers were registered on the bus.

The lifecycle of one activity, in the cases the manager handles:

stateDiagram-v2 [*] --> Created: ActivityMapper.getActivity Created --> Starting: start called Starting --> Showing: setWidget called Starting --> Cancelled: place changes first, onCancel Showing --> Asked: PlaceChangeRequestEvent Asked --> Showing: mayStop returned a message and user said Cancel Asked --> Stopped: no warning or user said OK, onStop Cancelled --> [*] Stopped --> [*]

The event bus

ActivityManager and PlaceController take a com.google.web.bindery.event.shared.EventBus. The standard implementation is SimpleEventBus. The toolkit also contains a legacy flavour under com.google.gwt.event.shared (EventBus, SimpleEventBus, ResettableEventBus): wrappers over the bindery classes kept for old code, and the type that Activity.start still declares. Use the bindery SimpleEventBus for the application bus and treat the legacy types as an interop detail; the Events & Handlers page has the semantics of fireEvent, ordering and the ResettableEventBus.

Two buses are in play:

  • The application bus is created once (in the ClientFactory) and lives as long as the page. PlaceController, PlaceHistoryHandler and every ActivityManager are registered on it.

  • The activity bus is the second argument of start: a ResettableEventBus that forwards to the application bus and remembers what was registered through it. The manager removes everything registered through it when the activity stops, before onStop() is called (the Javadoc of onStop() promises this), and again when it is cancelled.

An activity that listens to application events (a "book saved" event raised by another screen, for instance) registers on the activity bus. The same activity registered on the application bus is a leak. The test registered one handler on each bus in every activity and navigated five times:

Handler leak (NavigationFlowTest.handlersOnTheActivityBusAreRemoved)
after 5 place changes: handlers registered on the application bus ran 10 times, on the activity bus 4 times

The handlers on the application bus accumulated (0 + 1 + 2 + 3 + 4 invocations), while the handlers on the activity bus ran only for the one activity that was current. Events that do not need to cross screens should not go on a bus at all: a view that reports a click calls its presenter directly, and the first MVP article’s rule of thumb is to put only application-wide events on the bus.

The ClientFactory

The activities need the bus, the controller, the services and the views. Passing them one by one through every constructor does not scale, and creating them with GWT.create or new inside the activities makes the activities untestable. The Developer Guide’s answer is a client factory: one interface that the application implements and every activity receives.

ClientFactory.java (the interface the activities see)
public interface ClientFactory {
  EventBus getEventBus();

  PlaceController getPlaceController();

  CatalogServiceAsync getCatalog();

  AppMessages getMessages();

  CatalogView getCatalogView();

  BookView getBookView();

  EditView getEditView();

  BreadcrumbView getBreadcrumbView();
}
ClientFactoryImpl.java (the real one, trimmed)
public class ClientFactoryImpl implements ClientFactory {
  private final EventBus eventBus = new SimpleEventBus();
  private final RestoringDelegate delegate = new RestoringDelegate();
  private final PlaceController placeController = new PlaceController(eventBus, delegate);
  private final CatalogServiceAsync catalog = GWT.create(CatalogService.class);
  private final AppMessages messages = GWT.create(AppMessages.class);
  private BookView bookView;
  // ... the other views ...

  @Override
  public BookView getBookView() {
    if (bookView == null) {
      bookView = new BookViewImpl(messages);   // created on first use, then reused
    }
    return bookView;
  }
  // ... the other getters ...
}

Because the interface hides every GWT.create, a test supplies a fake implementation with fake views, an in-memory service and a PlaceController built with a test Delegate. That is what the navigation test does.

A factory is also a deferred-binding point. GWT.create(ClientFactory.class) in onModuleLoad() plus a rule in the module XML lets a mobile or tablet build return different views, which is the use the guide describes (its mobile sample defines a formfactor property for the choice). The simplest rule, replacing the interface by one implementation, compiled in a probe:

<replace-with class="com.example.bookshelf.client.ClientFactoryImpl">
  <when-type-is class="com.example.bookshelf.client.ClientFactory" />
</replace-with>

Without a rule, GWT.create of an interface fails (see the pitfalls table). A <when-property-is> condition on a property of your own selects an implementation per form factor; the mechanism, including how to define such a property, is in Deferred Binding & Generators. If the module does not need that, new ClientFactoryImpl() as in the Bookshelf App is simpler and avoids a rebind.

Dependency injection: Dagger or GIN

A hand-written factory is fine for a small application. As it grows, a dependency injection framework removes the boilerplate. The Developer Guide mentions GIN; the current options, verified for this page, are below.

Table 6. Dependency injection options for a GWT 2.13 client (checked 2026-10-09)
Option Status Evidence

Hand-written ClientFactory

always works

the Bookshelf scenario above

Dagger 2 with dagger-gwt

maintained in the Dagger repository (its gwt directory); com.google.dagger:dagger-gwt 2.60.1 was published 2026-07-06 and the repository was last pushed 2026-10-08

the sample below compiled with GWT 2.13.1 and its GWTTestCase passed in HtmlUnit

GIN (com.google.gwt.inject:gin)

archived: the last release is 2.0.0 (2012-09-27) and the google/gin repository is archived (last push 2014-08-26)

a 2.13.1 compile of a minimal Ginjector failed (messages in the pitfalls table); do not start a new project on it

Dagger

dagger-gwt is a tiny artifact: it carries the GWT module descriptors (dagger.Dagger, plus javax.inject.Inject, jakarta.inject.Inject and org.jspecify.Jspecify) and depends on the sources jars of dagger, jakarta.inject-api, javax.inject and jspecify, which is what the GWT compiler needs to compile the Dagger runtime classes. The code generator (dagger-compiler) runs on the JVM as an annotation processor and writes plain Java (DaggerAppComponent) that the GWT compiler then compiles like any other source: no reflection, no GWT.create.

pom.xml of the client module (fragment)
<dependencies>
  <dependency>
    <groupId>com.google.dagger</groupId>
    <artifactId>dagger</artifactId>
    <version>2.60.1</version>
  </dependency>
  <dependency>
    <groupId>com.google.dagger</groupId>
    <artifactId>dagger-gwt</artifactId>
    <version>2.60.1</version>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <annotationProcessorPaths>
          <path>
            <groupId>com.google.dagger</groupId>
            <artifactId>dagger-compiler</artifactId>
            <version>2.60.1</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>

The module descriptor needs one more line:

<inherits name="dagger.Dagger" />
The module, the injected factory and the component (package client.di)
@Module
public abstract class ClientModule {
  @Provides @Singleton
  static EventBus eventBus() { return new SimpleEventBus(); }

  @Provides @Singleton
  static PlaceController placeController(EventBus bus) { return new PlaceController(bus); }

  @Provides @Singleton
  static CatalogServiceAsync catalog() { return GWT.create(CatalogService.class); }

  @Provides @Singleton
  static AppMessages messages() { return GWT.create(AppMessages.class); }

  @Provides @Singleton
  static BookView bookView(AppMessages messages) { return new BookViewImpl(messages); }

  // ... the other views ...

  @Binds
  abstract ClientFactory clientFactory(InjectedClientFactory impl);
}

@Singleton
public class InjectedClientFactory implements ClientFactory {
  private final EventBus eventBus;
  private final PlaceController placeController;
  private final Lazy<BookView> bookView;
  // ... the other fields ...

  @Inject
  InjectedClientFactory(EventBus eventBus, PlaceController placeController, CatalogServiceAsync catalog,
      AppMessages messages, Lazy<CatalogView> catalogView, Lazy<BookView> bookView,
      Lazy<EditView> editView, Lazy<BreadcrumbView> breadcrumbView) {
    // ... assign the fields ...
  }

  @Override public BookView getBookView() { return bookView.get(); }
  // ... the other getters ...
}

@Singleton
@Component(modules = ClientModule.class)
public interface AppComponent {
  ClientFactory clientFactory();
}

onModuleLoad() then starts with ClientFactory factory = DaggerAppComponent.create().clientFactory(); and the activities, mappers and views on this page are unchanged. (The RestoringDelegate of the earlier section is passed to the PlaceController in the module in the same way.) The test that proves the wiring creates two components and checks that the bus is a singleton per component and the view a singleton per factory:

ClientFactory f = DaggerAppComponent.create().clientFactory();
ClientFactory again = DaggerAppComponent.create().clientFactory();
assertSame(f.getEventBus(), f.getEventBus());
assertNotSame(f.getEventBus(), again.getEventBus());
assertSame(f.getBookView(), f.getBookView());

Lazy<T> is what keeps views from being built before the first screen asks for them, which a plain constructor parameter would do eagerly at start-up.

GIN

GIN (GWT INjection) was the standard answer when the Developer Guide page was written, and the guide’s text still mentions it. gwtproject/gwt-gin does not exist; the project is google/gin, and it is archived. On GWT 2.13.1 with JDK 21 a minimal Ginjector with one @GinModules module did not compile: Guice 3.0 (a dependency of GIN 2.0.0) needs --add-opens java.base/java.lang=ALL-UNNAMED on a modern JDK, and with that flag GIN’s generator still failed with a NoClassDefFoundError for a GWT-internal class that no longer exists. Existing GIN code can be kept alive only on an old toolchain; for new code, use Dagger or a hand-written factory.

The Bookshelf flow, end to end

The Bookshelf application uses three screens and a small administration area. The places and their tokens:

Table 7. Places and tokens of the Bookshelf client
Place Address bar Activity Notes

CatalogPlace(0)

(empty, the default place)

CatalogActivity

the paged catalogue; #catalog:20 opens it at row 20

BookPlace(7)

#book:7

BookActivity

read-only detail with Edit and Back

EditPlace(7)

#edit:7

EditActivity

the form; asks before leaving when it has unsaved changes

AdminPlace

#admin:

AdminActivity

loads its code fragment with GWT.runAsync (see Code Splitting)

stateDiagram-v2 [*] --> Catalog: default place Catalog --> Book: Open on a row Book --> Edit: Edit Book --> Catalog: Back Edit --> Book: Save succeeded Edit --> Edit: Cancel in the confirm dialog Edit --> Catalog: Catalogue link, user confirms Catalog --> Admin: Admin link Admin --> Catalog: Catalogue link

The entry point wires it all together. It builds the shell (a DockLayoutPanel with a breadcrumb region, a navigation area and a content region, written in UiBinder), starts one ActivityManager per region, registers the history handler and waits for the XSRF token before handling the first place: the first screen already needs a protected RPC.

App.java
public class App implements EntryPoint {
  @Override
  public void onModuleLoad() {
    ClientFactoryImpl factory = new ClientFactoryImpl();
    EventBus eventBus = factory.getEventBus();
    ShellView shell = new ShellView(factory.getPlaceController());

    ActivityManager content = new ActivityManager(new AppActivityMapper(factory), eventBus);
    content.setDisplay(shell.getContent());
    ActivityManager trail = new ActivityManager(new BreadcrumbActivityMapper(factory), eventBus);
    trail.setDisplay(shell.getBreadcrumb());

    AppPlaceHistoryMapper historyMapper = GWT.create(AppPlaceHistoryMapper.class);
    PlaceHistoryHandler history = new PlaceHistoryHandler(historyMapper);
    Place defaultPlace = new CatalogPlace(0);
    history.register(factory.getPlaceController(), eventBus, defaultPlace);
    // ... RestoringDelegate wiring shown above ...

    RootLayoutPanel.get().add(shell);
    // The RPC proxy refuses protected calls without an XSRF token, so load one before the first place.
    factory.initXsrf(history::handleCurrentHistory);
  }
}

The shell template adds the region next to the navigation:

<g:DockLayoutPanel unit="EM">
  <g:north size="3">
    <g:Label text="{msg.appTitle}" styleName="{res.css.header}" />
  </g:north>
  <g:north size="2">
    <g:SimplePanel ui:field="breadcrumb" />
  </g:north>
  <g:west size="12">
    <g:FlowPanel styleName="{res.css.nav}">
      <g:Anchor ui:field="catalogLink" text="{msg.catalogue}" />
      <g:Anchor ui:field="adminLink" text="{msg.admin}" />
    </g:FlowPanel>
  </g:west>
  <g:center>
    <g:SimpleLayoutPanel ui:field="content" />
  </g:center>
</g:DockLayoutPanel>

What happened in the browser

The production build was served by Jetty on port 8922 and driven in the Chrome 152 browser pane, with window.confirm replaced by a recorder so the dialog answers could be scripted. Here is the session, in order:

Table 8. Browser run of the Bookshelf flow
Step Address bar afterwards What the page showed

Open /#book:3 directly (a deep link, fresh load)

#book:3

breadcrumb Catalogue / Book 3, then Clean Code by Robert C. Martin, 2008, 2 copies, Edit and Back

Click Edit

#edit:3

breadcrumb Catalogue / Book 3 / Edit and the filled form

Type a new title and click the Catalogue link, answer Cancel

#edit:3

the form is still on screen; one confirm call with You have unsaved changes. Leave this page and discard them?

Same, answer OK

(empty)

the catalogue, page 1-10 of 25

Edit the title and click Save

#book:3

Clean Code, 2nd edition by Robert C. Martin

Set the hash to #catalog:0

(empty)

the catalogue: the default place is normalised to the empty token

Set the hash to #bogus:1

#bogus:1

the catalogue stays: the handler asked for the default place, and the controller saw that it was already there, so nothing rewrote the URL

The last row is a harmless wrinkle of the "same place is a no-op" rule: an unknown token on top of the default place keeps its text in the address bar. If that matters, handle the rejection in the tokenizer’s caller or write the token back as RestoringDelegate does.

Testing the flow

The presenter tests need no browser. PlaceController takes a test Delegate, ActivityManager takes any AcceptsOneWidget, and the Bookshelf NavigationFlowTest supplies fakes for the factory, the views, the service and the display. It lives outside the module’s source path (com.example.bookshelf.flow), because everything under the client package is compiled to JavaScript by the module.

Fakes and one scenario (NavigationFlowTest.java, fragment)
class FakeDelegate implements PlaceController.Delegate {
  boolean answer = true;
  Window.ClosingHandler closing;

  @Override
  public HandlerRegistration addWindowClosingHandler(Window.ClosingHandler handler) {
    closing = handler;
    return null;
  }

  @Override
  public boolean confirm(String message) {
    trace.add("confirm(\"" + message + "\") -> " + answer);
    return answer;
  }
}

@Test
public void unsavedChangesGuard() {
  wire();                                   // two ActivityManagers on the bus, tracing mappers
  places.goTo(new EditPlace(7));
  editView.dirty = true;                    // the user typed something
  delegate.answer = false;
  places.goTo(new CatalogPlace(0));         // user answers Cancel
  assertEquals(new EditPlace(7), places.getWhere());
  delegate.answer = true;
  places.goTo(new CatalogPlace(0));         // user answers OK
  assertEquals(new CatalogPlace(0), places.getWhere());
}

The trace that scenario printed:

== dirty form, user answers Cancel
main:EditActivity.mayStop() -> "You have unsaved changes. Leave this page and discard them?"
trail:BreadcrumbActivity.mayStop() -> null
confirm("You have unsaved changes. Leave this page and discard them?") -> false

== dirty form, user answers OK
main:EditActivity.mayStop() -> "You have unsaved changes. Leave this page and discard them?"
trail:BreadcrumbActivity.mayStop() -> null
confirm("You have unsaved changes. Leave this page and discard them?") -> true
main.setWidget(null)
main:EditActivity.onStop()
main:CatalogActivity.start()
main.setWidget(CatalogView)
CatalogView.connect(0)
...

== window closing with a dirty form
main:EditActivity.mayStop() -> "You have unsaved changes. Leave this page and discard them?"
trail:BreadcrumbActivity.mayStop() -> null
ClosingEvent.getMessage() = "You have unsaved changes. Leave this page and discard them?"

The same scenario also shows that closing the window routes through mayStop(): the closing handler that PlaceController registered produced the message for the browser prompt.

Two scenarios exercise the asynchronous cases. In the first, the fake service keeps the answer until the test releases it, which models a slow server; in the second, a test activity simply does not call setWidget yet.

A late answer, with the guard and without it (trimmed)
== goTo(BookPlace(1)) while the server is slow
== goTo(CatalogPlace(0)) before the answer
== the slow answer arrives now
(BookActivity with the running flag: the view was never written, bookView.shown stays null)

== naive activity: goTo(BookPlace(1)), then leave, then the answer arrives
main.setWidget(BookView)
main.setWidget(null)
main.setWidget(null)
BookView.show(Book 1)

Without the guard, BookView.show(Book 1) ran after the region had been emptied and the reused view belongs to the next screen. The two setWidget(null) lines are the stop (the old activity’s widget removed) and the new "no activity for this place" clear, because the naive mapper returned null for the catalogue place.

The activity that has not shown anything yet
== goTo(BookPlace(1)): the activity has not called setWidget yet
SlowActivity.start()

== goTo(CatalogPlace(0)) before it shows anything
SlowActivity.onCancel()
main.setWidget(CatalogScreen)

== the old activity calls setWidget now

onCancel(), not onStop(), was called for the activity that never reached the screen, and its late setWidget call produced no output: the manager’s protected display ignored it.

For the generated mapper and the handler you need the compiler, so these are GWTTestCase tests (HtmlUnit). Give each GWTTestCase class its own module: in this scratch application, three test classes that all named the module AppTest made the later ones fail with could not instantiate the requested class (the browser-side detail was Cannot call method "new" of undefined), and a module per class (AppPlacesTest, AppDaggerTest, each a three-line module that renames and inherits App) fixed it. The full mvn verify of the scenario ran 23 tests with no failures: 10 navigation tests on the JVM (nine scenarios and the PlaceController construction check), 6 place and history tests and 1 Dagger test in HtmlUnit, and the earlier Bookshelf tests.

Security notes

  • The token is untrusted input. Anyone can type or link to #book:../... The tokenizer parses defensively and returns null for garbage; the activity validates what remains. A place must never be turned into HTML: show it with setText (as BookViewImpl does), not setHTML, and use SafeHtml when markup is unavoidable.

  • The fragment is not secret. It is stored in the browser history and in bookmarks and is visible to anyone who sees the screen or the shared link. Put identifiers in it, never credentials, tokens or personal data. (Browsers do not send the fragment to the server, so it is not in server logs or Referer headers, but the application’s own code and any script on the page can read it.)

  • mayStop() is user experience, not access control. A place for a role the user does not have must be refused by the server on every call (the RPC and REST endpoints), and a FilteredActivityMapper that redirects to a login place is only a convenience.

  • Protect state-changing calls. The App waits for the XSRF token before the first place so that no activity can issue an unprotected save; the XSRF, RPC & Server Security page covers the mechanism.

  • Deep links reach any screen. #edit:7 opens the edit form without passing through the catalogue. The activity must load and authorise the data itself and must not assume that an earlier activity ran.

Pitfalls

Messages marked probe come from compiling a small module with the 2.13.1 compiler (-failOnError), the ones marked test from the Bookshelf tests, and the ones marked source from the GWT 2.13.1 source.

Table 9. Common problems with Activities and Places
Symptom Cause Fix

Found duplicate place prefix "x" on pm.client.Entry.TB, already seen on pm.client.Entry.TA followed by Failed to resolve 'pm.client.Entry.M' via deferred binding (probe)

Two tokenizers in the mapper have the same @Prefix, or two places share a simple name and neither has @Prefix

Give each tokenizer a distinct prefix

Found place prefix "a:b" containing separator char ":", on pm.client.Entry.TA (probe)

The colon is the separator between the prefix and the token

Remove the colon from the prefix

Found duplicate tokenizer's place type "pm.client.Entry.A" on pm.client.Entry.T2, already seen on pm.client.Entry.T1 (probe)

Two tokenizers were declared for the same place type

One tokenizer per place type

Error processing @WithTokenizers, cannot find type pm.other.OtherTokenizer, then a NullPointerException from the generator (probe)

The tokenizer class is outside the module’s source path (a server or shared package that the module does not list)

Put tokenizers in the client or shared packages that the module sources

Annotation error: cannot resolve pm.client.Entry$TA, Annotation error: expected class java.lang.Class, got null and Cannot invoke "java.lang.Class.getCanonicalName()" because "tokenizerClass" is null (probe)

The compiler was run on the sources without the compiled .class files of the tokenizers on its classpath; annotation values of type Class are resolved from class files

Compile the Java sources first (Maven does) and put target/classes on the GWT classpath

Rebind result 'pm.client.Entry.M' cannot be abstract (probe)

The mapper was declared as an abstract class

Declare it as an interface that extends PlaceHistoryMapper

Rebind result 'pm.client.Entry.Factory' must be a class (probe)

GWT.create(ClientFactory.class) on an interface with no <replace-with> rule

Add the rule, or write new ClientFactoryImpl()

No source code is available for type com.google.gwt.place.shared.Place; did you forget to inherit a required module? (probe)

The module does not inherit com.google.gwt.place.Place (or com.google.gwt.activity.Activity); User does not include them

Add the two <inherits> lines

java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in client code! It cannot be called, for example, from server code. If you are running a unit test, check that your test case extends GWTTestCase and that GWT.create() is not called from within an initializer or constructor. (test)

A plain JUnit test called new PlaceController(bus), whose one-argument constructor creates the default delegate with GWT.create

Use new PlaceController(bus, delegate) with a fake Delegate in JVM tests

reference to Button is ambiguous (javac)

new Button("Edit", e -> ...) matches both the deprecated ClickListener and the ClickHandler constructors

edit.addClickHandler(e -> ...)

Unrecognized history token: nope:1 in the log, and the catalogue appears (test)

The token has no tokenizer, or the tokenizer returned null

Expected; return null for bad input, or add a "not found" place

The default place is stored as PlaceHistoryGwtTest.LooseDefault:x instead of an empty token (test)

The default Place has no equals(), so it never equals the place being stored

Implement equals() and hashCode() in every place

An activity starts twice for the same place; loaders run again and widgets flicker (test: started 2 times for two new PlainPlace())

Without equals(), PlaceController does not see that it is already there

Implement equals(); or use CachingActivityMapper to keep the same activity for an equal place

The mapper is never asked and nothing appears (test: asked 0 times)

ActivityManager.setDisplay(...) was not called (or was called with null)

Call setDisplay on every manager before the first place change

com.google.web.bindery.event.shared.UmbrellaException: Exception caught: Exception caught: boom out of goTo (test)

An activity’s start() threw; the manager collects it into an UmbrellaException, and the bus wraps that in another. The place was already changed (getWhere() returned the new place)

Read getCause() for the real exception; keep start() free of work that can throw

A late RPC answer fills the wrong screen (test: BookView.show(Book 1) after the region was emptied)

The view is reused and the old activity’s callback runs after onStop()

A running flag set in start and cleared in onStop and onCancel, checked in every callback

Handlers keep running after the screen is gone (test: 10 invocations against 4)

The activity registered on the application bus instead of the bus passed to start

Register on the eventBus parameter of start

Back, then Cancel in the confirm dialog: the address bar shows the screen the user tried to reach (browser)

The address bar changes before the application can refuse

RestoringDelegate as above, which writes the current place’s token back with History.newItem(token, false)

#bogus:1 stays in the address bar though the catalogue is shown (browser)

The unknown token resolved to the default place, which was already current, so no place change fired

Accept it, or write the token of the current place when the handler falls back

could not instantiate the requested class with Cannot call method "new" of undefined when running several GWTTestCase classes (test)

Test classes that share one module name; the later ones were not in the compiled module

One module per GWTTestCase class that the batch runs

java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module, then java.lang.NoClassDefFoundError: com/google/gwt/dev/util/Preconditions (probe, GIN 2.0.0)

GIN is unmaintained: Guice 3.0 on a JDK 17+ and a removed GWT-internal class

Move to Dagger or a hand-written factory

Where older material is outdated

  • The Developer Guide page still shows its sample with the legacy com.google.gwt.event.shared.EventBus/SimpleEventBus and the PlaceController(EventBus) constructor, both of which are deprecated in the current source. Use the bindery types.

  • Method name in the guide. The "How it all works" text says the manager checks Activity.onMayStop(); the interface method is mayStop(), and the real flow is the one in the sequence diagram above.

  • Default prefix. The guide says the prefix is the simple class name. The generator uses the name of the type in the type oracle, which for a nested place class includes the outer class (measured above). Use @Prefix.

  • The guide recommends GIN for a client factory. The project is archived and does not run on the current toolchain (measured above).

  • The MVP articles (2010) hand-write an AppController that listens to History and builds presenters in a switch. Activities and Places replaced exactly that code, so read the articles for the roles (model, view, presenter) and the testing argument, and the framework for the navigation. They also use EasyMock for the views; hand-written fakes, as in this page, work as well and need no library.

  • Javadoc of PlaceChangeRequestEvent.setWarning describes the confirm dialog the other way round (see PlaceController above).

  • The 2007 book "GWT in Action" predates the whole framework: it handles history with a bare History listener and a hand-written screen switch, and it has no place, activity or presenter classes. Its history chapter is still a useful explanation of why a token exists, and the events page explains the listener-to-handler change that its examples need.

  • Nalu (NaluKit/nalu) is a routing framework for GWT 2, GWT 3 and J2CL. Its README describes route-based navigation with hash or hash-less URLs, controllers whose lifecycle (start, mayStop, stop) is "similar to GWT Activities", filters that intercept routing, and a controller-scoped handler manager that removes handlers when the controller stops. The wiki has a comparison with Activities and Places. The current release is 4.0.2 (2026-02-12) under the Maven group io.github.nalukit (nalu and nalu-processor, with nalu-plugin-gwt for UiBinder); the older com.github.nalukit group stops at 2.20.6. It is an annotation-processor-based alternative, not something this page ran.

  • GWTP (ArcBees/GWTP) is a larger MVP framework on top of GIN, built around presenters and a place manager. Its last Maven Central release is 1.6 (2017) and the repository was last pushed in 2024-07-02; since it depends on GIN, it inherits the problems above. Not run here.

  • org.gwtproject.place:gwt-places is a port of the place classes to the org.gwtproject.event bus for GWT 3 / J2CL. Maven Central has only 1.0.0-RC1 (2020-05-22) and the repository was last pushed on 2021-05-01, so it is a migration option to evaluate, not a drop-in for a 2.13 application.

What changed recently

  • 2.1. Activities and Places were introduced (the package documentation of com.google.gwt.place.shared says @since GWT 2.1), and IsWidget appeared with them.

  • Event bus types. The framework now runs on the bindery event bus (com.google.web.bindery.event.shared); the legacy com.google.gwt.event.shared wrappers are kept for old code. The two legacy-typed constructors of PlaceController and the legacy register overload of PlaceHistoryHandler are marked @Deprecated in the 2.13.1 source.

  • 2.13.x. The release notes fetched on 2026-10-08 list no change to the place or activity packages. The framework you learn from the 2010 documents is the framework of 2.13.1.

  • Beyond 2.13. The roadmap fetched on 2026-10-08 lists Activities and Places among the Developer Guide pages and sets the focus on Java language features and JRE emulation up to Java 21, with no replacement for this framework. For J2CL-ready code, the community ports (gwt-places, gwt-event) and frameworks such as Nalu are the paths to look at.