9.6 KiB
Working with GWT UI Binder
[[TOC]]
Introduction
The GWT UI Binder technology uses HTML-like documents to describe the structure and static content of the UI. See the tutorial here: http://www.gwtproject.org/doc/latest/DevGuideUiBinder.html. With UI Binder, widgets are developed by declaring the widget's static HTML content in a .ui.xml file that more or less looks like HTML. By naming convention, a corresponding class related to the .ui.xml file. The class can bind its fields to elements in the .ui.xml file using Java annotations such as @UiField, matching with ui:field attributes in the .ui.xml file.
In the .ui.xml flie, elements can be used to represent widgets. In particular, all predefined GWT widgets are usually mapped to the g: namespace and can be used as in <g:FlowPanel ui:field="someFieldName" />. This will bind an instance of type FlowPanel to the widget class's field named someFieldName which has to be of type com.google.gwt.user.client.ui.FlowPanel.
The Design and Development Process with Ubilabs
We chose to work with a partner for UI Design: Ubilabs in Hamburg, Germany. They use a framework called Jade for their Web development. Their git repository lives at https://<yourgithubuser>@github.com/ubilabs/sap-sailing-analytics.git. The output of the Ubilabs build process is a bunch of HTML files (matching the "partials" component structure used in Jade) and the corresponding CSS files. Ubilabs plans to commit this generated output regularly to their git repository so we can understand the changes that we need to map into our environment.
As Ubilabs does not use GWT, we need to map their build outputs to our GWT architecture. GWT UI Binder fits with this goal. For each Jade partial we develop a corresponding UI Binder widget. The CSS file generated by Ubilabs can be used as is for the UI Binder widget. The HTML file needs to be renamed to .ui.xml and its contents need to be adjusted to the UI Binder styles and elements.
We asked Ubilabs whether they can instrument their Jade-based generator in such a way that for each Jade Partial X that will become a GWT UI Binder Widget we will automatically receive an XResources.java file that contains the CssResource interface declaration for the widget's CSS with a @Source annotation and a css() method declaration and an INSTANCE field declaration as more or less static content which fits in with how GWT ideally handles CSS resources. In the generated X.ui.xml file we then expect the CSS resource to be declared as in
<ui:with field="local_res" type="com.sap.sailing.gwt.home...XResources" />
which can be used as in
<header class="{local_res.css.siteheader}">
Ubilabs will deliver two versions of the site: one for use on desktops and tablets, and another for smartphone use. We need to map both versions to GWT UI Binder and decide based on the user agent and viewport size which one to choose.
Internationalization (i18n)
We're not yet decided on how to implement i18n. GWT offers two mechanisms that seem viable, see http://www.gwtproject.org/doc/latest/DevGuideUiBinderI18n.html. We could use GWT's feature that generates the property files. The upside would be that in case of many static texts we would get a quick start. The generation of UUIDs from the original static texts additionally ensures that changes cause compile errors and hence notify us that re-translation is required, which is good.
The downside is that redundancies across different entry points are not handled well and that the property files may not fit in well with programmatic use of those message strings.
Alternatively, it is possible to declare the string messages class and the method name that shall be used for the string message property. This way, we could consolidate all texts into a single message properties file which may help avoid redundancies.
We'll probably try out both approaches in a small scale and then decide.
UI Architecture
We looked at GWTP but discarded it again for its complexity in conjunction with the GIN injection framework that drives complexity a lot when combined with UI Binder. If you want to learn more about GWTP and GIN, read this: GWPT (https://github.com/ArcBees/GWTP and https://github.com/arcbees/gwtp/wiki) is a model-view-presenter framework that uses Gin (GWT INjection, see https://code.google.com/p/google-gin/ and the tutorial at https://code.google.com/p/google-gin/wiki/GinTutorial) as an underlying framework for GWT-based dependency injection.
Instead, we decided to use a very simple, light-weight MVP (Model, View, Presenter) approach that uses the standard GWT mechanisms of Activity, Place, ActivityMapper and PlaceHistoryMapper together with GWT's capabilities for code splitting. We assume a stage area that is used to display the activities' views, and the activities are bound to the places by an activity mapper. The place history manager keeps track of the places and their URL parameters and offers browser back/forth navigation through the places. Code splitting intends to produce one fragment for each component consisting of a Place, an Activity, a view which is typically a UI Binder widget in our case, and a proxy for code splitting.
Creating UI Components
A micro framework in package com.sap.sse.gwt.client.mvp supports developers in the implementation of UI components. It offers an AbstractEntryPoint class that manages most of the configuration tasks and establishes the links between the activity manager, place history manager, activity mapper and the event bus. The AbstractActivityProxy helps in applying code splitting to activities. The example subpackage contains two sample components that demonstrate how to use the micro framework. There is also some Javadoc on the most important classes.
Dealing with CSS Media Queries
GWT doesn't handle media queries in CssResource interfaces. To still cope, we agreed to use the following approach. Each CSS file that contains media queries of the form @media ... are copied so that for each media query in the original CSS file one additional copy is created. We then remove (or comment out) the contents of all CSS rule sets enclosed in any media queries from the original CSS file so that the declarations remain with an empty body. In each of the media query-specific copies we do the same, only for all rule sets that were not contained in the media query section. The @media enclosings are removed from or commented out in the original CSS file and all copies.
The Java resource file that describes the CSS file then has to declare a CSS resource for each of the copies and the original CSS file. The interface for the main resource that has to extend CssResource must be declared using the @Shared annotation. This ensures that the specializing media query-specific interfaces receive the same obfuscated class names as the main CSS resource. For each media query-specific copy, another interface that extends the main CSS's interface needs to be declared.
Example: Assume we have an original CSS document of the following form:
.a {
width: 75px;
}
@media (min-width: 25em) {
.b {
height: 10px;
}
}
@media (min-width: 50em) {
.b {
height: 20px;
}
}
Then the cleaned version of the original CSS looks like this:
.a {
width: 75px;
}
/* @media (min-width: 25em) { */
.b {
}
/* } */
/* @media (min-width: 50em) { */
.b {
}
/* } */
The copy for the first media query:
.a {
}
/* @media (min-width: 25em) { */
.b {
height: 10px;
}
/* } */
/* @media (min-width: 50em) { */
.b {
}
/* } */
and for the second media query:
.a {
}
/* @media (min-width: 25em) { */
.b {
}
/* } */
/* @media (min-width: 50em) { */
.b {
height: 20px;
}
/* } */
The Java resource interface then would look something like this:
public interface MyResources extends ClientBundle {
public static final MyResources INSTANCE = GWT.create(MyResources.class);
@Source("path/to/main.css")
MainCss mainCss();
@Shared
public interface MainCss extends CssResource {
String a();
String b();
}
public interface Media1Css extends MainCss {
}
@Source("path/to/media1.css")
Media1Css media1Css();
public interface Media2Css extends MainCss {
}
@Source("path/to/media2.css")
Media2Css media2Css();
}
The injection then has to happen in the component to which the CSS belongs which could be an entry point's onModuleLoad() method or a widget's / UiBinder's constructor. Example:
MyResources.INSTANCE.mainCss().ensureInjected();
StyleInjector.injectAtEnd("@media (min-width: 25em) { "+MyResources.INSTANCE.media1().getText()+"}");
StyleInjector.injectAtEnd("@media (min-width: 50em) { "+MyResources.INSTANCE.media2().getText()+"}");
GWT Code Splitting
For the various areas of the site we don't always want to have to load a new page. Instead, we'd like to use the "places" pattern with a local history management. This goes together well with GWT Code Splitting (see http://www.gwtproject.org/doc/latest/DevGuideCodeSplitting.html#patterns) which allows an application to load its parts when they are needed. This speeds up the initial loading process and keeps bandwidth consumption low.
Note that one code split fragment is produced per occurrence of the GWT.runAsync idiom in the code. To split each activity into its own fragment, each activity needs to have its own proxy invoking GWT.runAsync. To make the implementation of these proxies as easy as possible, we provide the class com.sap.sse.gwt.client.mvp.AbstractActivityProxy. Check out its Javadoc.