Widget Best Practices / Widget Building

Posted by Fred Sauer, Developer Advocate - Friday, May 01, 2009 at 5:48:00 PM

Developers often asked what's the best way to go about building custom widgets. There are of course a number of best practices to consider when answering this question. In this post I'll highlight a few I like to start off with.
  • Prefer composition to inheritance. Unnecessarily exposing implementation details is generally a bad idea, and it's no different when you are building custom widgets. In GWT terms, this means that your custom widgets should typically extend Composite.
// Here the HorizontalPanel implementation is exposed 
// Others may come to depend on the presence of one or more inherited methods public class OverExtendedWidget extends HorizontalPanel {
  public OverExtendedWidget() {
    //...
  }
}


// The use of HorizontalPanel is a hidden implementation detail
// We are free to change the implementation without affecting others public class ConservativeWidget extends Composite {


  private final HorizontalPanel panel;


  public ConservativeWidget() {
    panel = new HorizontalPanel();
    initWidget(panel);
    // ...
  }
}
  • Conservative API. Extending Composite also ensures that you do not inadvertently expose methods inherited from the parent classes into your new widget's API. Doing so can quickly lead to other classes depending on not only your class' implementation, but also the implementation of any parent classes. Remember, you can always add to your API. It's generally impossible to take anything away without introducing a breaking change.
public class LabeledTextBox extends Composite {


  // Be wary of multiple constructors as they can quickly get out of hand
  public LabeledTextBox(String labelText) {
    // ...
  }


  public LabeledTextBox(String labelText, boolean hideUserEnteredText) {
    // ...
  }


  public LabeledTextBox(String labelText, String textBoxText, boolean hideUserEnteredText) {
    // ...
  }


  // Only expose getters and setters if you have a use case for them
  public void setLabelText(String labelText) {
    // ...
  }
  public String getLabelText() {
    // ...
  }
}
  • Recycle and reuse. When your widgets repeat themselves try to avoid mirroring that repetition in your implementation. Often it is better to compose your new widget out of smaller, reusable widgets. This can both reduce widget complexity and allows improved testability. In a future post we'll explore widget design trade offs and lighter weight alternatives to widget composition to ensure the best possible user experience, which is GWT's ultimate mission (see GWT's mission statement for details).
public class LoginPanel extends Composite {


  private final VerticalPanel container;


  // We've decomposed the Label/TextBox pair into a separate LabeledTextBox widget class
  private final LabeledTextBox usernameTextBox;
  private final LabeledTextBox passwordTextBox;


  private final Button loginButton;


  public LoginPanel() {
    container = new VerticalPanel();
    initWidget(container);


    usernameTextBox = new LabeledTextBox("Username");
    container.add(usernameTextBox);


    passwordTextBox = new LabeledTextBox("Password", true);
    container.add(passwordTextBox);



    loginButton = new Button("Login");
    container.add(loginButton);
  }


  @Override
  protected void onLoad() {
    super.onLoad();
    usernameTextBox.setFocus(true);
  }
}
  • Design for real use cases. Instead of just imagining how your API might be used, you should write actual use cases that you want to support. You can start with a few lines of code which construct and use your new widget. Once you get the hang of it, you may want to consider test-driven development, a software development method whereby you write failing tests before you implement the features and functionality to make those same tests pass. While it may feel odd to write code that uses non-existent classes and/or interfaces and doesn't even compile, it can really help to flush out design problems early on. This should also help you with your 'Conservative API' efforts since the compiler will only complain about methods you actually try to use. As a bonus you can use your IDE to help you stub out your code. Eclipse, for example, provides many different kinds of Quick Fixes.

Eclipse Quick Fix offers to create a new Java class for you.



Eclipse Quick Fix can add missing methods or make other code changes for you.


  • Use Style. Your widgets should have a default look and feel. Your widgets should also allow for easy styling so that they will fit in with the rest of the page they live on. Providing CSS class names to key DOM elements in your widget is a good start. Setting a (primary) style name in the constructor is generally a good practice as well. This allows a designer to easily use different class names for different instances of your widgets on the same page. Browse the source for some of the out of the box GWT widgets to get an idea on how to do this.
public class RadioButton extends CheckBox {


  public RadioButton(String name) {
    // ...
    setStyleName("gwt-RadioButton"); 
  }


  // ...
}
  • Clean up after yourself. If you do extend Widget directly, use onLoad()/onUnload() rather than onAttach()/onDetach() to perform any DOM attachment setup or detachment cleanup. Any JavaScript references, such as event listeners, should be cleaned up in onUnload() to avoid memory leaks. Those same references should be setup in onLoad() rather than in the widget constructor. See for example the Checkbox source code. Recall that the widget life-cycle consists of three stages (1) construction, (2) attach to DOM, (3) detach from DOM. The most common life cycle for a widget is simply 12. Since GWT is all about building AJAX applications, many of your widgets will at least see a 123 life cycle. Widgets that move around the DOM as they are manipulated on the page will likely see a 123, 23, 23, ... life cycle. A common way to be tripped up by life cycles is when you, say, move your initialization code from widget construction to the onLoad() method without realizing that onLoad() is called each time your widget is (re)attached to the DOM.
  // This method is called when a widget is attached to the browser's document
  @Override
  protected void onLoad() {
    setEventListener(inputElem, this);
    // ... 
  } 


  // This method is called when a widget is detached from the browser's document 
  @Override   protected void onUnload() {
    // Clear out the inputElem's event listener (breaking the circular
    // reference between it and the widget); avoid memory leaks
    setEventListener(inputElem, null);
    // ...
  }

I hope you enjoyed reading a few high level best practices for widget design. In a follow-up post we'll continue to look at widget design through the eyes of the GWT compiler.

Finally, I hope you join us at Google I/O where we will have a number of sessions dedicated to GWT and plenty fellow developers to talk to, both on the conference room floor and in the developer sandbox.

No comments: