javagwtwidgetuibindergwt-designer

GWT Presentation Layer: Who does what?


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:

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.


Solution

  • How I Construct Widgets

    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.

    Example

    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.