GWT 2.1 Activities

Updated 2010-10-16: updated to GWT 2.1.

GWT 2.1 activities sit on top of the GWT 2.1 places to help in managing the place change events and navigation confirmation, and are part of what the GWT Team calls GWT's MVP Framework.

The core concepts

There are two main concepts in GWT 2.1 activities: display regions and activities. A display region is a part of the screen/page whose content changes in response to navigation. An activity is what the user does in a display region for a given place.

An important property of activities is that their lifetime is different from the one of the application (actually, from the display region, but those will generally have the same lifetime as the application, so…)

Any in-the-wild example of those concepts?

As an example, even though it's not made with GWT, let's look at GMail. Leaving alone ads, gadgets, and tasks for simplicity, the screen is divided into several parts:

All of these are display regions, except the top-level navigation menu, as it has the same lifetime as the application (note that it listens to some events, probably place change events, to update the selected item, but it does not mean that it is or should be a display region and/or activity).

The search bar and navigation menu are very simple, as they just swaps activity when you switch between “Mail” and “Contacts”, whereas the main region constantly changes to contain the “main activity”: e.g. some list of mail conversations, a particular mail conversation, some list of contacts, or a particular contact's details.

How those concepts map to classes in GWT?

An activity in GWT can be any class implementing the Activity interface, whereas a display region is a class implementing the AcceptsOneWidget interface. Swapping activities in a display region in response to a place change event is the job of an ActivityManager, which will ask an ActivityMapper the appropriate activity for the current place.

Activity life-cycle, from the ActivityManager point of view

When you navigate from one place to another, the current activity is first asked whether it mayStop(). This will eventually ask confirmation from the user before actually navigating (you'll probably recognize the PlaceChangeRequestEvent hidden behind this behavior).

If the navigation actually happens (i.e. the PlaceChangeEvent is fired), the current activity is first stopped (by a call to its onStop() method) before the next activity can be started.

The next activity is given by the ActivityMapper, depending on the current place. It is then start()ed within the ActivityManager's managed display region (AcceptsOneWidget). When the activity is fully initialized, it asks back to be displayed, and we're done… until the next place change request.

Initialization of an activity can be asynchronous (usually tied to a GWT-RPC or RequestFactory request), which means that the user can change its mind before the activity is fully initialized and navigate to yet another place. In that case, the current activity is cancelled (by a call to its onCancel() method) instead of stopped. Note that in this case, it isn't previously asked whether it mayStop(), as it isn't fully started yet.

Activity life-cycle, from the Activity point of view

In response to navigation, the activity is asked to start(). It's passed the display region and the event bus. Initialization can be asynchronous, and when fully initialized, the activity calls the display region back with an IsWidget object (i.e. an object capable of providing a Widget that displays it; note that the Widget base class implements IsWidget by returning itself, so you generally don't have to implement it yourself). The event bus is actually a ResettableEventBus, a wrapper around the application's global event bus, with the added property that any registered handler will automatically be unregistered when the activity is stopped, without it having to deal with HandlerRegistrations itself.

Once the activity has been started, and before it is fully initialized, it can be cancelled (by a call to its onCancel method) at any time if the user changes its mind and navigates to yet another place.

Otherwise (it's not been cancelled), when the user wants to navigate away, the activity will first be asked if it mayStop(), and will eventually be stopped (by a call to its onStop() method).

Note that because there can be several activities running concurrently (in different display regions), the activity might be stopped (or not) whichever the value it returned from the call to mayStop(). This is because a non-null return-value from mayStop() will only ask the user for confirmation, and null return-value won't overwrite a non-null value returned by another concurrently running activity (i.e. even if the activity returns null, another one might have returned a non-null value, asking the user for confirmation, and that one could finall have decided to stay at the current place). In other worlds, whichever the value you return from mayStop(), you shouldn't assume onStop() will or won't be called right away. However, onStop() won't ever be called without mayStop() being called first.

Activity life-cycle, from the display region and user point of view

The user interacts with the activities shown in display regions and isn't aware of this internal workflow. Stay concentrated as, because of the asynchronous nature of activities' startup, the notion of the current activity changes from the user's point of view!

In response to a user action (“show this mail conversation”, “go back to inbox”, etc.), the current activity has to be stopped and another started. As the next activity is starting (remember, this is already the current activity from the ActivityManager's point of view), the current activity (previous one actually) is still displayed, but it's already logically stopped (its onStop() method has already been called). Please note that this behavior will change in the next release: the display region's widget will be set to null as soon as the activity is stopped and until the next one is fully initialized.

It's your job as a developer to choose which behavior you want: the view is disabled, or it might trigger navigation.

Technically, it could still handle all user events but, because it's already stopped, it can be replaced at any given time and you won't have any notification (well, there's the widget's onUnload(), but anyway it's probably already too late to do anything useful). I'd therefore advise you to not do it, except to trigger navigation as it's about leaving the activity (which, you'll agree, doesn't conflicts with the already-stopped status of the activity).

The rule of thumb is thus to disable any action when an activity is stopped (i.e. from its onStop() method), except possibly navigation. It's up to you to decide whether this is done by disabling (graying out) widgets or just turning a deaf ear to their events.

Hey, where's the MVP framework you talked about?

Well, there's nothing forcing you to do MVP with GWT 2.1 activities, but it sure settles the basements for MVP: the Activity will be your presenter and the IsWidget you'll give back to the AcceptsOneWidget will be your view. And that's it. Really!

A note about performance

What takes most of the time in AJAX web apps on the client-side (i.e. with network and servers out of the equation) is manipulating the DOM. GWT 2.0 already gave us a wonderful tool to help us use the HTMLPanel, in the form of UiBinder, but it brings you nothing if you're constantly calling it. This means that your views should, when possible, be singletons. It doesn't imply though that your presenters should be!

If your presenters live longer than your activity (I mean from start to onStop here), they'll have to make sure they correctly clean up their internal state each time they're recycled. Because presenters are POJOs, they are actually quite lightweight, and the idea is that the cost for you to ensure the state is correctly cleaned up between uses of a singleton object outweighs the negligible performance lost induced by using many short-lived objects.

This works best if you listen to user events by registering a presenter delegate to your view: you'll call view.setDelegate(this) from start and view.setDelegate(null) from onStop. For your app to properly work, you only have to ensure that you don't try to use two presenters simultaneously with the same view, but it'll actually come naturally in most cases, and is really no different from ensuring a singleton presenter isn't used twice at a given time.

Conclusion

You're now ready to start coding wonderful applications, using a simple yet powerful approach to navigation handling, which integrates nicely with MVP.