📜 ⬆️ ⬇️

Introduction to MVP GWT 2.1

When I wrote a topic about using the Command pattern to organize RPC calls in GWT, I mentioned the MVP pattern for building the architecture of GWT projects. Immediately after the speech of Comrade Ray Rayan with a report on the design of complex applications on GWT, workers around the world began to implement the voiced ideas in the form of libraries and frameworks. The result of these works are the means to apply both some aspects of the MVP approach ( GWT-Presenter ) and its entirety ( Mvp4G ). This is all great, but I personally (I am sure that the rest of the GWT developers) would like to have a standardized (if you can say so) framework / approach for organizing GWT applications using the MVP scheme. And now the team that is responsible for Google for GWT, finally, in version 2.1, along with the other tasty buns, suggested the built-in MVP framework.
In this article I will try to highlight the main points of the embedded GWT MVP framework. As an example, a small application built using this approach will be used.
This note is a free treatise ( GWT MVP Development ) and in no way claims a unique approach. If someone is interested in such a rather big introduction

I will say right away that I will not focus on the design pattern MVP itself. You can get acquainted with its scheme, for example, on ( MVP on the Wiki ). In order to interest% username% I will give a screenshot of the application, which I eventually got.
Demo project
Yes, I understand that the application does not do anything useful, but on its basis I will try to show how to divide the architecture into separate independent parts (mail, contacts and tasks, by the way, taken just from the ceiling), organize switching and communication between them with using the built-in mechanisms in GWT 2.1. In addition, the M-component of the MVP pattern, i.e. model, because the application for illustrative purposes there is no data binding.

Main components of embedded MVP


The GWT team proposed the following key components for building an application using MVP:

Let us now consider in more detail all these components separately using the example of a real code. By the way, the application code is available for free access . So you can pry into it as you read.
')

Visual component or View


First of all, it should be said that in MVP, it is customary to “exchange” between the presenter and the view only interfaces. Therefore, for each type in the application there is a corresponding interface
package com.gshocklab.mvp.client.mvp.view; import com.google.gwt.user.client.ui.IsWidget; public interface IMailView extends IsWidget { public void setPresenter(IMailPresenter presenter); public interface IMailPresenter { } } 

The view interface describes the corresponding presenter interface that will handle the entire payload (sending requests for receiving data, processing events from the event bus, etc.).
The implementation of this interface will be simple and should not cause any special questions.
 public class MailView extends Composite implements IMailView { interface MailViewUiBinder extends UiBinder<Widget, MailView> { } private static MailViewUiBinder uiBinder = GWT.create(MailViewUiBinder.class); private IMailPresenter presenter; public MailView() { initWidget(uiBinder.createAndBindUi(this)); } @Override public void setPresenter(IMailPresenter presenter) { this.presenter = presenter; } } 

The associated ui.xml file contains just one widget, a Label with simple text. It makes no sense to bring his code, it can be viewed on the project website.
That's all for the view part. We now turn to the more interesting, to the Activity.

Page logic (views) or Activity


In the demo application on the left there is a navigation bar with links. Clicking on these links switches between views and sets the CSS style for the current link. I rendered this action into the abstract parent class AbstractMainActivity, which is a successor of the built-in class AbstractActivity.
 package com.gshocklab.mvp.client.mvp.activity; public abstract class AbstractMainActivity extends AbstractActivity { private static Map<String, Element> navLinks = new LinkedHashMap<String, Element>(); static { navLinks.put(AppConstants.MAIL_LINK_ID, DOM.getElementById(AppConstants.MAIL_LINK_ID)); navLinks.put(AppConstants.CONTACTS_LINK_ID, DOM.getElementById(AppConstants.CONTACTS_LINK_ID)); navLinks.put(AppConstants.TASKS_LINK_ID, DOM.getElementById(AppConstants.TASKS_LINK_ID)); } public void applyCurrentLinkStyle(String viewId) { for (String linkId : navLinks.keySet()) { final Element link = navLinks.get(linkId); if (link == null) continue; if (linkId.equals(viewId)) { link.addClassName("b-current"); } else { link.removeClassName("b-current"); } } } } 

And the concrete implementation of a specific Activity.
 package com.gshocklab.mvp.client.mvp.activity; public class MailActivity extends AbstractMainActivity implements IMailView.IMailPresenter { private ClientFactory clientFactory; public MailActivity(ClientFactory clientFactory) { this.clientFactory = clientFactory; } @Override public void start(AcceptsOneWidget container, EventBus eventBus) { applyCurrentLinkStyle(AppConstants.MAIL_LINK_ID); final IMailView view = clientFactory.getMailView(); view.setPresenter(this); container.setWidget(view.asWidget()); } } 

How it works: ActivityManager, when it receives a URL change event from PlaceHistoryManager, creates the necessary Activity instance using the ActivityMapper and starts it with the start () method. In this method, one of the parameters is the container in which the view widget will be substituted. The view we get from ClientFactory, which will be a little lower. We inject the presenter into the resulting instance of the view and display the view as a widget. Yes, another CSS rule is being established for the link that leads to the current view. But this is purely visual design.
ClientFactory is a simple factory that creates the necessary objects. Its interface is described as follows.
 public interface ClientFactory { public EventBus getEventBus(); public PlaceController getPlaceController(); public IMailView getMailView(); public IContactsView getContactsView(); public ITasksView getTasksView(); } 

Its implementation is not distinguished by “intelligence and wit”.
 public class ClientFactoryImpl implements ClientFactory { private final EventBus eventBus = new SimpleEventBus(); private final PlaceController placeController = new PlaceController(eventBus); private final IMailView mailView = new MailView(); private final IContactsView contactsView = new ContactsView(); private final ITasksView tasksView = new TasksView(); @Override public EventBus getEventBus() { return eventBus; } @Override public PlaceController getPlaceController() { return placeController; } @Override public IMailView getMailView() { return mailView; } @Override public IContactsView getContactsView() { return contactsView; } @Override public ITasksView getTasksView() { return tasksView;} } 

The instantiation of the ClientFactory object will be performed using Deffered binding according to the rule, which is described in the GWT module description file. But more about that again later in the section where the configuration of the entire MVP enterprise in a single working system will be considered. It is worth noting here that in real projects for tasks that ClientFactory solves it is better to use Google GIN. Advantages of the DI-tool, it makes no sense to describe, they are already clear.
The last of the key elements of the built-in MVP is an object that is responsible for the state of the UI and performs manipulations with history tokens.

Place or hash URLs and their processing


As mentioned above, the Place object is responsible for the current state of the UI. The state is transferred via URL through history tokens. In fact, in this object, you can store parameters that are passed to the hash URL. The status of the URL is encoded / decoded using a Tokenizer object. When working with request parameters that are passed to a hash URL, it is very important to follow the following rule: all parameters that come to us from the URL after processing should be encoded back into the URL in the same way. It is this logic that is implemented in the methods of the Tokenizer class.
By convention, which is accepted by the GWT team and is described in the official manual, the tokenizer class is usually described as an internal static class of the Place object. This simplifies the code for storing query parameters in the Place object's variables. Although it is possible to apply an approach with separate classes for tokenizers.
Not to be unfounded, consider the code class MailPlace
 package com.gshocklab.mvp.client.mvp.place; import com.google.gwt.place.shared.Place; import com.google.gwt.place.shared.PlaceTokenizer; import com.google.gwt.place.shared.Prefix; public class MailPlace extends Place { private static final String VIEW_HISTORY_TOKEN = "mail"; public MailPlace() { } @Prefix(value = VIEW_HISTORY_TOKEN) public static class Tokenizer implements PlaceTokenizer<MailPlace> { @Override public MailPlace getPlace(String token) { return new MailPlace(); } @Override public String getToken(MailPlace place) { return ""; } } } 

This class is inherited from the built-in Place class. In it, the constant declared is the part of the hash URL that will uniquely identify the state. In this case, it is “mail”. The class Tokenizer is responsible for recreating the state and saving it via history tokens. The binding of a specific hash URL to the tokenizer is performed using the Prefix annotation.
Processing history is generally an interesting topic and deserves a separate article. Here we confine ourselves to having our own hash URL with each Place object. This URL must end with “:”. After this colon, you can specify additional parameters, for example, you can create URLs like #mail: inbox, #contacts: new, etc. and these tokens will be processed in the getPlace () method. In fact, the first part of the hash URLs is the subsystem identifier (mail, tasks, etc.), everything that follows after the colon can be interpreted as subsystems' actions.
In the demo project, additional tokens (or actions) are not used, so the getToken () method in all tokenizers returns an empty string, and the getPlace () method returns the created Place object.

Determining the desired Activity and registering handlers


When a new URL is received and the Place object is successfully instantiated, the ActivityManager manager uses the ActivityMapper to decide which object to launch. The definition is implemented is simple and trite.
 public class DemoActivityMapper implements ActivityMapper { private ClientFactory clientFactory; public DemoActivityMapper(ClientFactory clientFactory) { super(); this.clientFactory = clientFactory; } @Override public Activity getActivity(Place place) { if (place instanceof MailPlace) { return new MailActivity(clientFactory); } else if (place instanceof ContactsPlace) { return new ContactsActivity(clientFactory); } else if (place instanceof TasksPlace) { return new TasksActivity(clientFactory); } return null; } } 

Registering hash URL handlers, i.e. Tokenizers are running on the PlaceHistoryMapper interface.
 package com.gshocklab.mvp.client.mvp; import com.google.gwt.place.shared.PlaceHistoryMapper; import com.google.gwt.place.shared.WithTokenizers; import com.gshocklab.mvp.client.mvp.place.ContactsPlace; import com.gshocklab.mvp.client.mvp.place.MailPlace; import com.gshocklab.mvp.client.mvp.place.TasksPlace; @WithTokenizers({MailPlace.Tokenizer.class, ContactsPlace.Tokenizer.class, TasksPlace.Tokenizer.class}) public interface DemoPlaceHistoryMapper extends PlaceHistoryMapper { } 

All that is needed at this stage is simply to list the tokenizer classes of the application in the @WithTokenizers annotation.

Putting it all together


All the code for initializing and starting the MVP framework is assembled in the onModuleLoad () method of EntryPoint
 package com.gshocklab.mvp.client; import com.google.gwt.activity.shared.ActivityManager; import com.google.gwt.activity.shared.ActivityMapper; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.GWT; import com.google.gwt.event.shared.EventBus; import com.google.gwt.place.shared.PlaceController; import com.google.gwt.place.shared.PlaceHistoryHandler; import com.google.gwt.user.client.History; import com.google.gwt.user.client.ui.RootLayoutPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.gshocklab.mvp.client.layout.AppLayout; import com.gshocklab.mvp.client.mvp.DemoActivityMapper; import com.gshocklab.mvp.client.mvp.DemoPlaceHistoryMapper; import com.gshocklab.mvp.client.mvp.place.MailPlace; public class MvpInActionEntryPoint implements EntryPoint { private SimplePanel containerWidget; private MailPlace defaultPlace = new MailPlace(); @Override public void onModuleLoad() { final AppLayout mainLayout = new AppLayout(); containerWidget = mainLayout.getAppContentHolder(); final ClientFactory clientFactory = GWT.create(ClientFactory.class); EventBus eventBus = clientFactory.getEventBus(); PlaceController placeController = clientFactory.getPlaceController(); // activate activity manager and init display ActivityMapper activityMapper = new DemoActivityMapper(clientFactory); ActivityManager activityManager = new ActivityManager(activityMapper, eventBus); activityManager.setDisplay(containerWidget); // display default view with activated history processing DemoPlaceHistoryMapper historyMapper = GWT.create(DemoPlaceHistoryMapper.class); PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper); historyHandler.register(placeController, eventBus, defaultPlace); RootLayoutPanel.get().add(mainLayout); History.newItem("mail:"); } } 

I think the explanation code will be superfluous, everything is simple and clear. It should be noted that the call to History.newItem (“mail:”) may be superfluous. MailActivity and so run, because MailPlace is specified as the default Place. Another thing is that at the start we will not see the hash URL #mail: in the address bar of the browser. If the display of the starting hash URL for the project is not critical, then the call to History.newItem () can be removed.
In order for the embedded MVP framework to work, you need to include the corresponding GWT modules in the GWT module description file (gwt.xml file)
 <?xml version="1.0" encoding="UTF-8"?> <module rename-to='mvpinaction'> <inherits name='com.google.gwt.user.User' /> <inherits name="com.google.gwt.activity.Activity"/> <inherits name="com.google.gwt.place.Place"/> <entry-point class='com.gshocklab.mvp.client.MvpInActionEntryPoint' /> <replace-with class="com.gshocklab.mvp.client.ClientFactoryImpl"> <when-type-is class="com.gshocklab.mvp.client.ClientFactory" /> </replace-with> <source path='client' /> </module> 

It also specifies the deferred binding rule for creating the ClientFactory instance.

Instead of conclusion


That's all. I do not provide the file with the root layout of the AppLayout application, it can be viewed in the source code. In this file there are links, in the href attributes of which are hash URLs to go to the application subsystems. You can also open this or that subsystem simply by typing the correct URL in the address bar of the browser. It will automatically start the process of converting the state from the URL to the corresponding place with the launch of the corresponding activity, which will display the desired form.
I note in addition that the note and demo project did not consider such important moments in life as the use of the event bus (eventBus), the processing of hash URL parameters and much more.

Working demo-project , source code Google Code . Watch out Mercurial!
Waiting for feedback and comments.

PS Sorry for the multi-letter. I hope that this introductory note turned out to be useful to someone (although it doesn’t fully cover all the power of the embedded MVP), I wrote it for good reason and it will serve as a starting point for implementing really cool GWT applications

Source: https://habr.com/ru/post/113121/


All Articles