Hands-On Google Web Toolkit

Ed and Adam use the Google Web Toolkit—a free, open source framework created to make Ajax easier—to build a mash-up photo viewer that lets you browse any Flickr photo album.


December 10, 2007
URL:http://www.drdobbs.com/global-developer/hands-on-google-web-toolkit/204800628

Ed is a principal systems developer at the SAS Advanced Computing Lab and author of Google Web Toolkit: Taking the Pain Out of Ajax. He can be contacted at [email protected]. Adam is a senior software developer at the SAS Advanced Computing Lab and specializes in all things Java. He can be contacted at [email protected].


Most developers create Ajax applications using several different languages across two or more tiers. On the client side, you have HTML markup of course, plus some logic written in JavaScript to perform tasks such as client-side validation and manipulation of the HTML document object model (DOM). On the web server, you may have parts written in PHP, Perl, Java, or other languages.

Unfortunately, slight differences in JavaScript among browsers, along with major differences in the DOM, have conspired to make writing these types of applications more difficult. Libraries such as Dojo and Prototype help smooth out the rough edges, but substantial dynamic web applications are still much harder to write than the traditional desktop applications they're supposed to replace.

The Google Web Toolkit (code.google.com/webtoolkit) is a free, open-source framework created by Google to make Ajax easier. In this article, we present GWTFlow, a mash-up photo viewer that lets you browse any Flickr photo album. The UI was inspired by Apple's Cover Flow album viewer (www.apple.com/itunes/jukebox/coverflow.html) and the Carousel mode in the .Mac Web Gallery. The complete source code is available online (see "Resource Center," page 5 and at www.adamhoughton.com/GWTFlow). By studying this application, you will learn useful techniques for creating your own dynamic web pages using GWT.

Why GWT?

GWT unifies the client and server parts of an application into a single program written in one language—Java. This has many advantages. For one thing, more developers know Java than JavaScript or ActionScript (the language used by Flash). Another reason is that Java is blessed with an abundance of developer tools, such as Eclipse, NetBeans, and IDEA.

By standardizing on one language, you can share code on the client and server. For example, you can run the same validation code—once on the client for immediate feedback, and once on the server for maximum security. You can even move code between tiers as you refactor applications to adapt to changing requirements.

Finally, GWT abstracts the browser's DOM, hiding differences between browsers behind easy to extend object-oriented UI patterns. This helps make code portable over every major web browser. While you may occasionally have to delve into CSS/DOM/JavaScript to address browser quirks in complex programs, GWT makes this the exception rather than the rule.

Getting Started

After downloading and installing GWT, make sure you are running a recent version of Java (Java 5 or higher is recommended; see the sidebar "Java 1.4 Versus Java 5"). Create a GWT project using your IDE or the projectCreator and applicationCreator programs supplied with the toolkit. Then select a web page where you want to insert the GWT component or application. This is known as the "host page" in GWT lingo. Listing One is the page we used in this article.

To inject the GWTFlow component into the page, we include our compiled JavaScript file (more on that later) and an HTML <div> placeholder element. Because GWTFlow needs to use special effects and support browser history, we also include a third-party JavaScript library and an <iframe> element. Listing Two is the resulting host page, with newly added elements in boldface.

<html>
 <head>
   <title>GWTFlow</title>
   <link rel="stylesheet" type="text/css" href="GWTFlow.css" />
 </head>
 <body>
 <div id="header">
   <div id="logo">
     <a class="blind" href="GWTFlow.html">GWTFlow</a>
   </div>
   <p>
     <a class="tblind" href="about.html">About</a>    
     <a class="tblind" href="contact.html">Contact</a>
   </p>
 </div>
 <div id="footer">
   <p>Copyright 2007 Adam Houghton</p>
 </div>
 </body>
</html>

Listing One

<html>
 <head>
   <title>GWTFlow</title>
   <link rel="stylesheet" type="text/css" href="GWTFlow.css" />
   <!— Inject GWT code by including compiled JavaScript file —>
   <script language='javascript' src='org.gwtflow.GWTFlow.nocache.js'></script>
   <!— JavaScript we'll later need for effects —>
   <script type="text/javascript" src="script/prototype.js"></script>
   <script type="text/javascript" src="script/effects.js"></script>
 </head>
 <body>
 <!— IFrame for GWT history support —>
 <iframe src="javascript:''" id="__gwt_historyFrame"
     style="width:0;height:0;border:0"></iframe>
 <div id="header">
   <div id="logo">
     <a class="blind" href="GWTFlow.html">GWTFlow</a>
   </div>
   <p>
     <a class="tblind" href="about.html">About</a>    
     <a class="tblind" href="contact.html">Contact</a>
   </p>
 </div>
 <!— GWTFlow will put its content in this div —>
 <div id="main"></div>
 <div id="footer">
   <p>Copyright 2007 Adam Houghton</p>
 </div>
 </body>
</html>
Listing Two

Hosted versus Web Mode

While developing and debugging your application, you'll be using GWT in hosted mode. When in production however, your application will be running in web mode. Think of hosted mode as training wheels for your GWT program. It's a hybrid environment unique to GWT that lets you run and debug the real Java code, while still inside a browser.

The simplest way to launch hosted mode is to run the script file created by applicationCreator. In the example, we called the project "GWTFlow," so the script to run hosted mode is called "GWTFlow-shell." This brings up the GWT development shell, which then starts your program in hosted mode inside an embedded web browser. Or, using your IDE to start the shell, you can set breakpoints, examine variables, and single step through your application just like any other Java program. A good plug-in (such as GWT Designer for Eclipse) can make this task easier, along with providing extra benefits such as WYSIWYG user interface creation.

After you've debugged and unit tested your code, the next step is to compile it into a form that can be run inside a regular browser using the GWTFlow-compile script or equivalent. The GWT compiler translates your client-side code into JavaScript. It then combines your code with a JavaScript version of the GWT API in one big JavaScript file, which you load in the host HTML page. Actually, it creates several different browser- and language-specific versions. By default, the compiled JavaScript code is obfuscated and compressed to make it load and run as fast as possible for users. However, in the event that you encounter a bug that cannot be tracked down in hosted mode, you can tell GWT to create human-readable JavaScript that you can debug in web mode using a tool such as FireBug (www.ddj.com/development-tools/196802787).

Designing the User Interface

The sample application's UI consists of two widgets (UI elements) and a main controller. The first widget, AlbumWidget, lets users select an image service, input their username, and select a photo album; see Figures 1 and 2. The second widget, ImageFlowWidget, displays the photos and provides navigation through the album. The main program (GWTFlow.java) controls the two widgets and maintains application state. All of the widgets are dynamically added and removed from the placeholder <div> element we put in the host HTML page.

AlbumWidget is constructed using a number of simpler GWT-provided widgets, such as Button, Image, Label, RadioButton, and TextBox. These are all arranged on VerticalPanels and HorizontalPanels. (For documentation on these and more, see the references at the end of this article.)

The advantage of using the widgets and panels provided by the toolkit is that they work the same way on all browsers. A prime example is the scrollable selector for choosing the photo in Figure 2. This would be difficult to implement in traditional HTML; but in GWT, it's a simple Grid table contained within a ScrollPanel. That's not to say all cross-browser problems go away—we experienced a few quirks with Internet Explorer and Safari—but they are significantly fewer when compared to traditional Ajax development.

[Click image to view at full size]

Figure 1: Users can select a photo service and username.

[Click image to view at full size]

Figure 2: List of albums is retrieved from the server and displayed in a scrollable grid.

ImageFlowWidget extends AbsolutePanel, which allows for absolute positioning and overlapping of widgets. Image widgets are placed in this panel and scaled to simulate perspective. The main image is always the largest and is positioned in the center of the window. Other pictures are stacked underneath and sized smaller and smaller the further they are from the center.

When browsers display absolute positioned elements, they overlap elements that were added later on top of earlier ones. So we need to add images from the outside in (using AbsolutePanel.add()), with the main image last. Image positions, however, are calculated from the inside out to prevent smaller images from getting completely hidden. Listing Three presents the algorithm for handling this. Starting from the center image, it first calculates the size and position, recurses to the neighboring image, calls AbsolutePanel.add(), and finally returns to the caller.


private void calcImagePosition(int imageIdx, int[][] whlt, int offsetWidth,
     int offsetHeight, boolean addToPanel, boolean animateAdd)
 {
   if (imageIdx < 0 || imageIdx >= images.length) {
     return; // Stopping recursion once we reach the first or last image
   }
   // Calculating image's width/height based on its distance from center.
   int distFromCenter = Math.abs(selIdx - imageIdx);
   int maxWidth = (int)((offsetWidth*.6)/(distFromCenter+1));
   int maxHeight = (int)((offsetHeight*.6)/(distFromCenter+1));
   int[] wh = scaleImage(dtos[imageIdx].width, dtos [imageIdx].height,
       maxWidth, maxHeight);
   int width = wh[0];
   int height = wh[1];

   // Calculating image's top position so it's always centered on viewport
   int top = (offsetHeight / 2) - (height / 2);

   // Calculating image's left position so 45% of its width pokes out
   int left;
   if (imageIdx < selIdx) {
     int xDiff = (int)(width*.45);
     left = whlt[imageIdx+1][LEFT] - xDiff;
   } else if (imageIdx > selIdx) {
     int xDiff = (int)(width*.55);
     left = whlt[imageIdx-1][LEFT] + whlt[imageIdx-1][WIDTH] - xDiff;
   } else { // centering middle image
     left = (getOffsetWidth() / 2) - (width / 2);
   }

   whlt[imageIdx] = new int[] { width, height, left, top };

   // Recursive calls to calc positions for images on the left and/or right
   if (imageIdx <= selIdx) {
     calcImagePosition(imageIdx-1, whlt, offsetWidth, offsetHeight,
         addToPanel, animateAdd);
   }
   if (imageIdx >= selIdx) {
     calcImagePosition(imageIdx+1, whlt, offsetWidth, offsetHeight,
         addToPanel, animateAdd);
   }

   // Adding images to the panel.  This happens on the initial load and on
   // window resize events.  If the user is just clicking between images,
   // they are already on the panel so we don't need to add them.
   if (addToPanel) {
     images[imageIdx].setWidth(width + "px");
     images[imageIdx].setHeight(height + "px");
     // The first time we load, we'll use the Scriptaculous grow effect
     // for any images that are in the viewport.
     if (animateAdd && inViewport(left, images[imageIdx].getWidth())) {
       images[imageIdx].setVisible(false);
       images[imageIdx].addLoadListener(new LoadListener() {
         public void onLoad(Widget sender) {
           Effect.grow(sender, new EffectOption[] {
               new EffectOption("duration", LOAD_DURATION)});
         }
         public void onError(Widget sender) {
           showErrorMessage();
         }
       });
     }
     // Last added are on top, so we add from the outside in.
     this.add(images[imageIdx], left, top);
   }
 }
Listing Three

Animation

GWT does not currently provide a built-in animation or effects library (although developers are working on one for an upcoming release). Instead, you need to call out to a JavaScript library such as Script.aculo.us (script.aculo.us), YUI (developer.yahoo.com/yui), or Rico (openrico.org). For GWTFlow, we chose the Effect class in the open-source GWT Widget Library. Using GWT's JavaScript Native Interface (JSNI), it wraps the Script.aculo.us effects library, providing animation for moving, scaling, and fade effects.

When users navigate to a new image, GWTFlow recalculates the new images and sizes, calls Effect.move() and Effect.scale() (provided by the GWT Widget Library), and lets Script.aculo.us work its magic. Listing Four presents the method called to animate the image movement.

private void animateMove(Widget widget, int left, int top, double dur) 
{
  Effect.move(widget, new EffectOption[] { new EffectOption("mode", "absolute"),
    new EffectOption("x", left), new EffectOption("y", top), 
    new EffectOption("duration", dur)} );
}

Listing Four

Getting Data From Flickr

When users type in a username and click the Next button, we need to retrieve the list of images and albums from the server. Flickr provides access to its data via an extensive web service API (www.flickr.com/services/api). We created a proxy service to provide this data using GWT's Remote Procedure Call (RPC) functionality (Figure 3).

[Click image to view at full size]

Figure 3: ImageService acts as a proxy between the client and Flickr.

RPC services in GWT extend from the Java Servlet API, with built-in callbacks defined to make everything asynchronous. The objects returned are serializable POJOs (Plain Old Java Objects), which we use to store image and album metadata.

The actual calls to Flickr are done in ImageService.java using Flickrj, an open-source Java library that wraps Flickr's REST-based API (flickrj.sourceforge.net). Listing Five shows the code to look up users, get a list of their photo sets, and transform the list into an array of lightweight Album data transfer objects.

public AlbumDTO[] getFlickrAlbums(String username) throws IOException,
    SAXException, FlickrException 
{
  Flickr flickr = FlickrUtil.getInstance(); // Access Flickr using API key
  PeopleInterface people = flickr.getPeopleInterface();
  User user = people.findByUsername(username);
  PhotosetsInterface photoSets = flickr.getPhotosetsInterface();
  Collection sets = photoSets.getList(user.getId()).getPhotosets();
  AlbumDTO[] albums = new AlbumDTO[sets.size()];
  Iterator iter = sets.iterator();
  for (int i = 0; i < albums.length; i++) {
    Photoset set = (Photoset) iter.next();
    albums[i] = new AlbumDTO(); // Custom POJO w/Album metadata
    albums[i].id = set.getId();
    albums[i].name = set.getTitle();
    albums[i].description = set.getDescription();
    albums[i].imageCount = set.getPhotoCount();
    albums[i].smallSquareUrl = set.getPrimaryPhoto().getSmallSquareUrl();
  }
  return albums;
}
Listing Five

As in any Ajax application, client-side responsiveness is a primary concern. An RPC service lets us minimize the data sent across the wire and use the server's processing power to retrieve image information. It also allows for a cleanly defined API that can be extended to support other image providers, such as Google's Picasa Web Albums.

However, RPC services typically require a servlet engine such as Tomcat or JBoss, adding complexity to deployment and limiting the number of hosting providers. One alternative we considered was JSON (JavaScript Object Notation), a data interchange format with support for native JavaScript web service requests. JSON data feeds are supported by a variety of providers, including Flickr and some Google services (although not Picasa). In the end, we decided against JSON, favoring RPC for performance, ease of debugging, and code maintainability.

Fit and Polish

Support for browser history has always been tricky in web applications, especially in the Ajax world ("please use my special previous arrow, not the big one you click everywhere else!"). Thankfully, GWT provides easy integration with browser history. As users navigate through their pictures, GWTFlow pushes entries onto the browser history stack. By implementing HistoryListener in the GWTFlow.java controller, the application receives notification of history events. This lets users navigate the application like a traditional website: Pushing the back button shows the previous image or dialog screen, not the previous website.

Because the URL is changing in the browser bar, a nice side effect is that bookmark support comes for free. Users returning to the site will see the exact same state of the application where they bookmarked it. Figure 4 is the completed application.

[Click image to view at full size]

Figure 4: You can see a few images before and after the current one. Click on any image to move to it or press the browser's Back button to go back to the last picture selected.

Conclusion

JavaScript is the new Assembly language. Google Web Toolkit lets you create Ajax programs in Java and compile them into browser-specific JavaScript, hiding most of the browser quirks that unnecessarily complicate Ajax development. Using GWT, you can create highly interactive applications that run on all major browsers with no plug-ins. Most importantly, you can do it with a portable, object-oriented language and mature tools that you already know.

References

A live demo of GWTFlow and source code is available at www.adamhoughton.com/GWTFlow. Here are a few resources to help you learn more about GWT:

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.