I'm learning GWT and trying to wrap my head around all the UI options. I'm having trying to make sense of when/where/how to use Widgets, UIBinder, GWT Designer and custom Widgets. Specifically:
com.google.gwt.user.client.ui.*
vs. com.google.gwt.user.client.ui.Composite
?To me, I feel like all of these things do the same stuff, and perhaps this is just GWT's way of providing you with multiple ways to accomplish presentation? If not, please correct me on these items.
I only use Widgets over UiBinder when the widget is extremely simple. For example, I'm just adding two widgets to a Panel
. If there is any CSS involved, I always go with UiBinder because it is much easier to work with styles.
Widgets do not get translated into UiBinder XML. All GWT code becomes JavaScript that adds and manipulates DOM elements, so everything gets translated to an executable language, not to a templating system.
I write lots of UiBinder snippets. I try to follow good rules about abstraction and composition that you can find all over the web.
The MVP pattern is a must if you have any non-trivial logic because testing a GWT-free presenter is very quick and easy with JUnit, whereas GWT tests have much more overhead and are much slower.
I like to keep as much styling in CSS files as possible because it's a standard good practice to separate concerns, you can compress and bundle your CSS files, and lots of other reasons that are the same as why in a normal HTML page you put CSS in separate files instead of on the page directly.
I never use the GWT designer. I always prefer to have clean code rather than the crazy junk usually spit out by any UI code generator.
99% of the time, my widgets extend Composite
because either I'm using UiBinder or I'm adding things to a Panel
. Even if I only have a single widget, I find it easier to extend Composite
and add my one widget to a SimplePanel
. I rarely extend Widget
because then you have to make a call to Document.get().createFooElement()
to create a DOM Element
, but I typically find Widget
s, which can be added to Panel
s, easier and safter to work with than Element
s
Each widget implements an interface that extends IsWidget
. Everyone who wants to use the Widget
should depend on the interface, not on the underlying class. This presents a single, JSNI-free abstraction.
If the widget is very simple, I will have a single class that extends Composite
and implements the interface. Either the widget will be very simple and add a few items to a Panel
or it will use UiBinder.
If the widget has non-trivial logic that I would like to test, I use the MVP pattern. There will be a presenter class that implements the 'public' interface of the widget, a view interface that extends IsWidget
that the presenter depends on, and a view widget that implements the view interface.
A benefit of having a single 'public' interface for the widget is that you can change from implementing the interface with a single Composite
class to using MVP if the logic becomes complex, and no one using the widget needs to change at all.
I use Gin to wire all the interfaces and implementations together.
This is best explained with some code. Let's say I have a chart that I want to use on several pages, so I decide to make a reusable widget for it. There is some non-trivial logic around processing the RPC response before displaying it, so I want to thoroughly unit test it. I'd go with something like this:
public interface FinancialChart extends IsWidget {
void setTickerSymbol(String tickerSymbol);
}
class FinancialChartPresenter extends Composite implements FinancialChart {
private final FinancialChartView view;
private final DataServiceAsync service;
@Inject(FinancialChartView view, DataServiceAsync service) {
this.view = view;
this.service = service;
}
@Override public Widget asWidget() {
return view.asWidget();
}
@Override public void setTickerSymbol(String tickerSymbol) {
service.getData(tickerSymbol, new AsyncCallback<FinancialData>() {
@Override public void onFailure(Throwable t) {
// handle error
}
@Override public void onSuccess(FinancialData data) {
SimpleData simpleData = // do some parsing with presentation-specific
// logic, e.g. make dramatic increases or decreases in price have a
// a different color so they stand out. End up with something simple
// that's essentially some (x, y) points that the dumb view can plot
// along with a label and color for each point.
view.drawGraph(simpleData);
}
}
}
interface FinancialChartView extends IsWidget {
void drawGraph(SimpleData simpleData);
}
class FinancialChartWidget extends Composite implements FinancialChartView {
@Override public void drawGraph(SimpleData simpleData) {
// plot the points on a chart. set labels. etc.
}
}
class SomethingWithFinancialChartWidget extends Composite
implements SomethingWithFinancialChart {
interface Binder extends UiBinder<Widget, SomethingWithFinancialChartWidget> {}
@UiField(provided = true) final FinancialChart chart;
@Inject SomethingWithFinancialChartWidget(Binder binder, FinancialChart chart) {
this.chart = chart;
initWidget(binder.createAndBindUi(this));
}
}
// In SomethingWithFinancialChartWidget.ui.xml
<ui:HTMLPanel>
<!-- lots of stuff -->
<mynamespace:FinancialChart ui:field="chart" />
<!-- lots more stuff -->
</ui:HTMLPanel>
class MyPackagesGinModule extends AbstractGinModule {
@Override protected void configure() {
bind(FinancialChart.class).to(FinancialChartPresenter.class);
bind(FinancialChartView.class).to(FinancialChartWidget.class);
}
}
This allows me to write very simple, thorough, and fast JUnit tests for the FinancialViewPresenter
because it has no GWT dependencies that require JSNI, which has to run in a browser as part of a much slower GWT test case. You can create a mock FinancialChartView
.
One thing to note here is that since SomethingWithFinancialChartWidget
is depending on the interface FinancialChart
, it cannot instantiate that object because it is just an interface. That is why chart
is set up as @UiField(provided = true)
in the Java code of SomethingWithFinancialChartWidget
. Gin set up the binding from the FinancialChart
interface to a concrete class so it can provide an implementation to the @Inject
constructor of SomethingWithFinancialChartWidget
, and then setting this.chart
gives the UiBinder the object it needs.
There are many files that get created for all the interfaces and implementations in MVP, but the abstraction is absolutely worth it because they enable easy unit testing of presenters and allow you to change how the top-level interface, FinancialChart
in this example, is implemented, e.g. change from a single Composite
class to MVP, with no client needing to change.
I'm sure there are some implementation details that may not be super clear or things I glossed over, e.g. GWT tests, so please post comments and I can edit my answer to update and clarify.
Let's take some distance with your questions and answer the overall concern at once. But first, my favorite catchphrase:
There's no magic!
UiBinder is a tool that takes an XML definition and generates Java code out of it. That is to say (and that's true for everything constructed through a call to GWT.create()
: there's no magic!) that you could have written the same by hand, in Java code. In other words, UiBinder is only a tool to get you more productive, by requiring less code to achieve the same goal. But as it generates code that you could have written by hand, it won't give better performance at runtime (not worse either); it however makes it easier to use patterns that result in better performance, namely HTMLPanel
: maintaining HTMLPanel
-based code in pure-Java is a nightmare, and the complete opposite with UiBinder.
UiBinder will:
ClientBundle
, with an implicit CssResource
for each <ui:style>
, implicit an ImageResource
for each <ui:image>
and an implicit DataResource
for each <ui:data>
Messages
interface for I18N out of <ui:msg>
, <ui:ph>
et al.createAndBindUi
method) based on ui:field
attributes in the XML and @UiField
annotations in the Java code; or possibly retrieve them from the partner object instead, if the annotation has provided=true
@UiConstructor
-annotated constructors, or @UiFactory
-annotated methods of the partner object, or as a last resort using a GWT.create()
call@UiHandler
-annotated methods of the partner object as event handlers for widgets in the XML template (based on the ui:field
attributes)The most common use of UiBinder is for those objects I talked about above to be widgets. Assembling them then means adding widgets into other container widgets.
This is why you generally see UiBinder being used inside a Composite
to provide the value for the initWidget()
method: UiBinder will create and put together all the widgets needed and return the top-most one, to be used as the root for the composite widget.
You'll have understood already: you won't use UiBinder over widgets, and widgets aren't translated to UiBinder XML (quite the contrary actually).
Now to your more specific questions:
Do you write lots of UIBinder XML "snippets" or just pack them all into one big monolithic XML file?
UiBinder is kind of an implementation detail. Most of the time, you'll build (reusable) widgets (composites) with UiBinder; it won't transpire to the outside world, which will only see a widget, not "something built with UiBinder".
That means you'll have lots of small/medium UiBinder templates (generally one per screen in your app, at the very least, or for a more general rule, one per complex UI component).
For making my own custom components, what's the difference between extending com.google.gwt.user.client.ui.* vs. com.google.gwt.user.client.ui.Composite?
Ümit answered that one, and the documentation too: https://developers.google.com/web-toolkit/doc/latest/DevGuideUiCustomWidgets
Division of labor: Widgets/UIBinder vs. Layouts vs CSS files: who does what?
There's unfortunately no simple answer to that one. It depends.
It depends the kind of application you're building, and the kind of UI you want. For the "global layout" of your app, you'll use either several RootPanel
or one big HTMLPanel
and do the layout with HTML and CSS (either in the HTML host page, or in a UiBinder template to serve as a shell for your app), or you'll use so-called layout panels.
For more localized layout (inside composite widgets), I'd recommend using HTMLPanel
or FlowPanel
; or possibly layout panels. If you need to handle scroll events, or if you need scrolling right inside a container widget, then use a ScrollPanel
, otherwise just use overflow: scroll
in CSS.
There are tradeoffs. You'll have to do experiments. And in the end, use what you think is good for you. There's no one-size-fits-all.
Finally, while it's its most common use case, note that UiBinder can be used without widgets, for example to create non-composite widgets (or UiObject
s, but there's very few use cases for them; you'll see them used for MenuItem
s and TreeItem
s for instance), using setElement()
rather than initWidget()
, or create custom Cells.
And actually, there isn't much use cases for Composite
either: you can simply implement IsWidget
(returning the result of createAndBindUi
, called once and cached in a private field) and it should work everywhere a widget is expected/accepted.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With