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.
| Role | Bookshelf classes | Rules |
|---|---|---|
Model |
|
No widgets. Shared classes can run on the client and on the server. |
View |
the interfaces |
Interface first. The interface may extend |
Presenter |
|
Talks to views only through their interfaces. Talks to the rest of the application through the |
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
Displayinterface 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
Presentercallback interface (void onSave(Book edited)) and calls it from its handlers; the presenter callsview.setPresenter(this). This is the style of the second MVP article and of the Activities and Places guide, and it works well with UiBinder@UiHandlermethods 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().
| Class or annotation | Job | Who writes it |
|---|---|---|
|
A Java object that stands for a bookmarkable state of the UI ( |
You: one subclass per kind of screen, with |
|
Converts one kind of place to and from a short string (the token). |
You: one per place, usually a static nested class. |
|
Names the part of the token before the colon. |
You, on the tokenizer (or on a factory method). |
|
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. |
|
Moves the application to a place ( |
GWT: you create one and share it. |
|
Keeps the browser’s history token and the current place in step, in both directions. |
GWT: you create one and register it. |
|
What the user is doing in a place: |
You: one per screen. It is the presenter. |
|
Chooses the activity for a place. |
You: a few lines of |
|
Starts and stops activities inside one display region. |
GWT: you create one per region. |
|
Carries |
GWT: |
|
Gives activities the bus, the controller, the services and the views. |
You (or Dagger). |
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.
<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:
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).
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
CatalogViewdoes this with aDataGridand anAsyncDataProvider: the presenter callsshowRows(start, books, total)and never sees a column. -
A UiBinder view reports gestures to the presenter with
@UiHandlermethods that only delegate (presenter.onSave(driver.flush())), as theBookEditorform 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:
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.
@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 ofinstanceoftests, most derived type first) and returnsprefix + ":" + token, or only the token when the prefix is empty. It returnsnullwhen 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 returnsnullwhen 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:
| Call | Result | Why |
|---|---|---|
|
|
prefix |
|
|
|
|
|
|
|
|
the tokenizer maps the empty token to the first page |
|
|
the tokenizer returned |
|
|
split at the first colon, so the tokenizer receives |
|
|
no tokenizer has the prefix |
|
|
no tokenizer has the empty prefix |
|
|
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).
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:
-
If
getWhere().equals(newPlace), it returns at once. Nothing is fired. -
It fires a
PlaceChangeRequestEventon the bus. Any handler may callsetWarning(String)on it; only the first non-null warning counts.ActivityManageris such a handler: it sets the warning to the current activity’smayStop(). -
If there is no warning, or
Delegate.confirm(warning)returnstrue, the controller stores the new place and fires aPlaceChangeEvent. -
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.
Source: GWT Javadoc — PlaceController
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
PlaceChangeEventit writes the new token withHistory.newItem(token, false): the second argument isfalse, so the write does not fire a history event and does not loop back; -
on the history
ValueChangeEventit maps the token to a place and callsplaceController.goTo(place).
The rules it applies, all measured with an in-memory Historian in the test below:
| Situation | Result |
|---|---|
|
goes to the default place, and the handler writes |
a place change to the default place |
the stored token is the empty string, so the default place has a clean URL ( |
a place change to |
the stored token is |
the URL changes to a token the mapper does not know ( |
logs |
the URL changes to a token the tokenizer rejects ( |
logs |
a place the mapper cannot tokenise |
logs |
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.
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.
Source: GWT Javadoc — History
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.
| Method | When it is called |
|---|---|
|
The manager has decided that this activity is current. Fetch data, configure the view and call |
|
Before the place changes, and when the window is about to close. Return |
|
The user left before |
|
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:
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
runningflag 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.
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:
-
Ask the mapper for the next activity (if
setDisplaywas never called, or was called withnull, the manager does not even ask and the current activity is shut down). -
If the mapper returned the same activity object as the current one (
equals), do nothing. This is the hook forCachingActivityMapper. -
If the previous activity is still starting (it has not called
setWidget), call itsonCancel(). -
Otherwise empty the region (
setWidget(null)), remove every handler the activity registered on the scoped bus, and callonStop(). -
Make the new activity current and call
start(...)with a protected display: asetWidgetcall from an activity that is no longer current is silently ignored. -
If
onStop,onCancelorstartthrew, collect the exceptions and throw anUmbrellaExceptionafter 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:
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.
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.
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.
== 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.
Source: GWT Javadoc — ActivityManager
The lifecycle of one activity, in the cases the manager handles:
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,PlaceHistoryHandlerand everyActivityManagerare registered on it. -
The activity bus is the second argument of
start: aResettableEventBusthat forwards to the application bus and remembers what was registered through it. The manager removes everything registered through it when the activity stops, beforeonStop()is called (the Javadoc ofonStop()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:
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.
public interface ClientFactory {
EventBus getEventBus();
PlaceController getPlaceController();
CatalogServiceAsync getCatalog();
AppMessages getMessages();
CatalogView getCatalogView();
BookView getBookView();
EditView getEditView();
BreadcrumbView getBreadcrumbView();
}
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.
| Option | Status | Evidence |
|---|---|---|
Hand-written |
always works |
the Bookshelf scenario above |
Dagger 2 with |
maintained in the Dagger repository (its |
the sample below compiled with GWT 2.13.1 and its |
GIN ( |
archived: the last release is 2.0.0 (2012-09-27) and the |
a 2.13.1 compile of a minimal |
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.
<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" />
@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.
Source: GIN — google/gin (archived)
The Bookshelf flow, end to end
The Bookshelf application uses three screens and a small administration area. The places and their tokens:
| Place | Address bar | Activity | Notes |
|---|---|---|---|
|
(empty, the default place) |
|
the paged catalogue; |
|
|
|
read-only detail with Edit and Back |
|
|
|
the form; asks before leaving when it has unsaved changes |
|
|
|
loads its code fragment with |
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.
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>
Source: GWT Developer Guide — UiBinder
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:
| Step | Address bar afterwards | What the page showed |
|---|---|---|
Open |
|
breadcrumb |
Click Edit |
|
breadcrumb |
Type a new title and click the Catalogue link, answer Cancel |
|
the form is still on screen; one confirm call with |
Same, answer OK |
(empty) |
the catalogue, page |
Edit the title and click Save |
|
|
Set the hash to |
(empty) |
the catalogue: the default place is normalised to the empty token |
Set the hash to |
|
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.
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.
== 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.
== 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 returnsnullfor garbage; the activity validates what remains. A place must never be turned into HTML: show it withsetText(asBookViewImpldoes), notsetHTML, and useSafeHtmlwhen 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
Refererheaders, 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 aFilteredActivityMapperthat redirects to a login place is only a convenience. -
Protect state-changing calls. The
Appwaits 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:7opens 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.
| Symptom | Cause | Fix |
|---|---|---|
|
Two tokenizers in the mapper have the same |
Give each tokenizer a distinct prefix |
|
The colon is the separator between the prefix and the token |
Remove the colon from the prefix |
|
Two tokenizers were declared for the same place type |
One tokenizer per place type |
|
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 |
|
The compiler was run on the sources without the compiled |
Compile the Java sources first (Maven does) and put |
|
The mapper was declared as an abstract class |
Declare it as an interface that extends |
|
|
Add the rule, or write |
|
The module does not inherit |
Add the two |
|
A plain JUnit test called |
Use |
|
|
|
|
The token has no tokenizer, or the tokenizer returned |
Expected; return |
The default place is stored as |
The default |
Implement |
An activity starts twice for the same place; loaders run again and widgets flicker (test: started 2 times for two |
Without |
Implement |
The mapper is never asked and nothing appears (test: asked 0 times) |
|
Call |
|
An activity’s |
Read |
A late RPC answer fills the wrong screen (test: |
The view is reused and the old activity’s callback runs after |
A |
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 |
Register on the |
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 |
|
|
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 |
|
Test classes that share one module name; the later ones were not in the compiled module |
One module per |
|
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/SimpleEventBusand thePlaceController(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 ismayStop(), 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
AppControllerthat listens toHistoryand builds presenters in aswitch. 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.setWarningdescribes 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
Historylistener 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.
Alternatives and related projects
-
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 groupio.github.nalukit(naluandnalu-processor, withnalu-plugin-gwtfor UiBinder); the oldercom.github.nalukitgroup 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-placesis a port of the place classes to theorg.gwtproject.eventbus for GWT 3 / J2CL. Maven Central has only1.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.
Source: Nalu — NaluKit/nalu
What changed recently
-
2.1. Activities and Places were introduced (the package documentation of
com.google.gwt.place.sharedsays@since GWT 2.1), andIsWidgetappeared with them. -
Event bus types. The framework now runs on the bindery event bus (
com.google.web.bindery.event.shared); the legacycom.google.gwt.event.sharedwrappers are kept for old code. The two legacy-typed constructors ofPlaceControllerand the legacyregisteroverload ofPlaceHistoryHandlerare marked@Deprecatedin the 2.13.1 source. -
2.13.x. The release notes fetched on 2026-10-08 list no change to the
placeoractivitypackages. 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.
Source: GWT — Release Notes