Abstract: Google Web Toolkit (GWT) allows ordinary Java Programmers to produce highly responsive web user interfaces, without needing to become experts in JavaScript. Here we demonstrate a little maths game for practicing your arithmetic. Included is an Easter egg.
Welcome to the 143rd edition of The Java(tm) Specialists' Newsletter, sent to you from the island of Crete. The weather is warming up and so we try to get to the beach on most days. Before you get jealous, we are a bit cramped in the house we are renting, so we need to get out a bit. The beach is simply the most sensible place to go to. Feeling less jealous now? I guess not ... ;-) One way that you can spoil my beach fun is to invite me to present in-house courses at your company :-)
Since today is Easter in Greece, I would like to greet you with the traditional Greek Christos Anesti! In the spirit of Easter, we have an Easter egg hidden in the demo of today's code. Further hints will follow in the newsletter.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
Whenever you go to a Java conference nowadays, there will be at least one session dedicated to the Google Web Toolkit (GWT). You will be blown away by incredible AJAX feats, all coded in Java. Then you will hear that you do not need to worry about cross-browser JavaScript code, because GWT takes care of this for you. On Google's webpage we see this paragraph: "GWT shields you from worrying too much about cross-browser incompatibilities. If you stick to built-in widgets and composites, your applications will work similarly on the most recent versions of Internet Explorer, Firefox, and Safari. (Opera, too, most of the time.) DHTML user interfaces are remarkably quirky, though, so make sure to test your applications thoroughly on every browser."
I decided to spend a few days investigating GWT to gauge its applicability for my website. It became clear quite early that mixing GWT with an existing multi-page website would be a challenge. Also, I found that a lot of the ideas I tried worked perfectly in Firefox but failed dismally in Internet Explorer. You do have to test everywhere. However, the effects are stunning.
A few years ago, I wrote a simple Maths (that's what we call Math in South Africa) tutor for my son Maxi. We called it "Maxi Maths". It was simply a command line Java program, that would feed him questions and then measure the time it took him to answer. Functionally it was perfect in that it did exactly what we needed, and no more. By practicing his arithmetic, he regularly finished in the top four of his class.
After some time, we decided to "upgrade" to a Swing application, which worked quite well. From there we went over to an Applet and downgraded it to AWT. We had the usual Applet issues, and eventually did a JSP page. However, the JSP page had the typical page reload issues. We wanted to have a richer client experience. So last week, we decided to try this in GWT, mainly to satisfy our curiosity, but also to learn more about GWT.
Before I show you the code, you need to prepare your system a bit. First off, download the GWT and install it. Now run the applicationCreator batch file shipped with the GWT, as follows:
applicationCreator net.kabutz.maxi.gwt.maths.client.Maths
This will prepare a nice directory structure for you, with two batch files: Maths-compile and Maths-shell. Call them both and you will see a nice scaffolding application that gets you going on using GWT.
Next we create the maths game. A very simple maths tutor that asks you a bunch of questions and then gives you a score. Each question extends the MathsQuestion abstract class. We have catered for plus, minus, multiply and divide, but it could be extended for other operators like remainder.
Note that GWT is limited in which classes can be compiled to
JavaScript. For example, I could call the
Math.random()
method, but could not instantiate
java.util.Random
. Also, you cannot use any
reflection code in the client part of GWT. If you add a
server component, you can do anything that Java would
normally allow under Servlets.
package net.kabutz.maxi.gwt.maths.client.game; public abstract class MathsQuestion { private int answer; private String question; protected int random(int max) { return (int) (Math.random() * max) + 1; } protected void setQuestion(int first, int second, char operator, int answer) { this.question = first + " " + operator + " " + second + " = "; this.answer = answer; } public String getQuestion() { return question; } public int getAnswer() { return answer; } }
We then have four concrete maths questions. I must admit that initially I used a switch statement for the questions. However, a better Design Pattern is to use the Strategy pattern, on which this design is based. It would allow us to add new types of questions without modifying the rest of our code.
package net.kabutz.maxi.gwt.maths.client.game; public class PlusQuestion extends MathsQuestion { public PlusQuestion(int upto) { int first = random(upto - (upto / 10)); int second = random(upto - first - 1); setQuestion(first, second, '+', first + second); } } package net.kabutz.maxi.gwt.maths.client.game; public class MinusQuestion extends MathsQuestion { public MinusQuestion(int upto) { int first = random(upto); int second = random(first - 1); setQuestion(first, second, '-', first - second); } } package net.kabutz.maxi.gwt.maths.client.game; public class TimesQuestion extends MathsQuestion { public TimesQuestion(int upto) { int first = random((int) Math.sqrt(upto)); int num = random(upto); int second = num / first; setQuestion(first, second, '×', first * second); } } package net.kabutz.maxi.gwt.maths.client.game; public class DivideQuestion extends MathsQuestion { public DivideQuestion(int upto) { int top = random(upto); int second = random((int) Math.sqrt(top)); int first = top / second * second; setQuestion(first, second, '÷', first / second); } }
We put these together in a MathsBean and MathsGameBean. Each MathsBean has a pointer to a MathsQuestion, which we could see as a strategy instance. We could also generate these questions on the server and return them as XML to the browser. I am not showing it here, but it would be really easy to do. You need to just let the classes implement com.google.gwt.user.client.rpc.IsSerializable. The GWT assistance in IntelliJ IDEA 6 is excellent and will help you get all the pieces fitting together.
package net.kabutz.maxi.gwt.maths.client.game; public class MathsBean { private final MathsQuestion strategy; private boolean correct = false; public MathsBean(int upto) { strategy = makeRandomQuestion(upto); } private MathsQuestion makeRandomQuestion(int upto) { switch ((int) (Math.random() * 4)) { default: case 0: return new PlusQuestion(upto); case 1: return new MinusQuestion(upto); case 2: return new TimesQuestion(upto); case 3: return new DivideQuestion(upto); } } public String getQuestion() { return strategy.getQuestion(); } public boolean checkAnswer(int answer) { return correct = strategy.getAnswer() == answer; } public int getAnswer() { return strategy.getAnswer(); } public boolean isCorrect() { return correct; } }
The MathsGameBean holds a number of MathsBeans and remembers how long we have been answering the questions for. Each MathsBean knows whether it was answered correctly or not, so we do not keep the information inside the MathsGameBean, but look it up on request.
package net.kabutz.maxi.gwt.maths.client.game; public class MathsGameBean { private int currentQuestion = 0; private long start = System.currentTimeMillis(); private final MathsBean[] questions; public MathsGameBean(int questions, int upto) { this.questions = new MathsBean[questions]; for (int i = 0; i < questions; i++) { this.questions[i] = new MathsBean(upto); } } public boolean hasNext() { return currentQuestion < questions.length; } public MathsBean next() { return questions[currentQuestion++]; } public int getScore() { int correct = 0; for (int i = 0; i < questions.length; i++) { correct += questions[i].isCorrect() ? 1 : 0; } return correct; } public int getTotalQuestions() { return questions.length; } public int getSeconds() { return (int) ((System.currentTimeMillis() - start + 500) / 1000); } public int getRate() { return getTotalQuestions() * 60 / getSeconds(); } }
We use these classes to write a simple Maths Tutor.
The main entry class that we will use is Maths. It has two panels, MathsGameSetupPanel and MathsGamePlayPanel, which contain components used to either setup the game, or play it.
Here is the magic of GWT. All of the Java code you have seen up to now, and the Java code that we will show next, is turned into JavaScript code. This is supposed to be portable between browsers, but is not always. You still need to test it on all browser / OS combinations!
Since we are swapping between the "setup" and "play" pages,
we implemented a very basic state pattern for managing the
state transitions. There are many ways of implementing the
state pattern. In this approach, we pass a pointer to Maths
into the state, to allow it to switch to the next page.
We do that with the methods setupGame()
and
playGame()
.
package net.kabutz.maxi.gwt.maths.client; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.user.client.ui.*; public class Maths implements EntryPoint { private Panel currentPanel; public void onModuleLoad() { setupGame(); } public void setupGame() { setPanel(new MathsGameSetupPanel(this)); } public void playGame(int questions, int upto) { setPanel(new MathsGamePlayPanel(this, questions, upto)); } private void setPanel(Panel panel) { if (currentPanel != null) { RootPanel.get().remove(currentPanel); } currentPanel = panel; RootPanel.get().add(currentPanel); } }
We create the setup panel with two text boxes and a submit button. The CSS style used is "game-style", described further down in the newsletter. The default values for the number of questions is "10" and the default maximum number is "50". It looks like a typical Swing application, but generates JavaScript when you compile it with GWT.
package net.kabutz.maxi.gwt.maths.client; import com.google.gwt.user.client.ui.*; public class MathsGameSetupPanel extends VerticalPanel { private TextBox questions = new TextBox(); private TextBox upToNumber = new TextBox(); public MathsGameSetupPanel(final Maths owner) { setStyleName("game-style"); questions.setText("10"); upToNumber.setText("50"); add(makeInput("Number of questions:", questions)); add(makeInput("Up To Number:", upToNumber)); add(new Button("Play Game!", new ClickListener() { public void onClick(Widget widget) { int numQues = Integer.parseInt(questions.getText()); int upto = Integer.parseInt(upToNumber.getText()); owner.playGame(numQues, upto); } })); } private Panel makeInput(String label, TextBox inputText) { HorizontalPanel inputPanel = new HorizontalPanel(); inputPanel.add(new Label(label)); inputPanel.add(inputText); return inputPanel; } }
The actual game play panel shows the question and a textbox
where we can enter the answer. We register a keyboard
listener on the answer textbox. When we press
enter
, we check the answer, showing the correct
answer if necessary.
(Clue: We can add listeners to
all sorts of characters with the KeyboardListener.)
We apply a different style to the
remark label, depending on whether it was correct or not.
Part of the checkAnswer()
method involves
checking whether we have further questions. If we do, we
display them, otherwise we show the final score and display
a button to "Play again!".
package net.kabutz.maxi.gwt.maths.client; import com.google.gwt.user.client.ui.*; import net.kabutz.maxi.gwt.maths.client.game.*; public class MathsGamePlayPanel extends VerticalPanel { private MathsGameBean mathsGame; private MathsBean maths; private TextBox answerBox = new TextBox(); private Label questionLabel = new Label(); private Label remark = new Label(); private Maths owner; public MathsGamePlayPanel(Maths owner, int questions, int upto) { setStyleName("game-style"); this.owner = owner; mathsGame = new MathsGameBean(questions, upto); maths = mathsGame.next(); questionLabel.setText(maths.getQuestion()); answerBox.setMaxLength(10); answerBox.setWidth("60px"); answerBox.addKeyboardListener(new KeyboardListenerAdapter() { public void onKeyPress(Widget widget, char c, int i) { if (c == 13) { checkAnswer(); } } }); Panel questionPanel = new HorizontalPanel(); questionPanel.add(questionLabel); questionPanel.add(answerBox); add(questionPanel); add(remark); } private void checkAnswer() { int answer = Integer.parseInt(answerBox.getText()); if (maths.checkAnswer(answer)) { remark.setText("Well Done!"); remark.setStyleName("correct-answer"); } else { remark.setText("Nope! " + maths.getQuestion() + maths.getAnswer() + " not " + answer); remark.setStyleName("wrong-answer"); } answerBox.setText(""); if (mathsGame.hasNext()) { maths = mathsGame.next(); questionLabel.setText(maths.getQuestion()); } else { add(new Label(mathsGame.getScore() + " / " + mathsGame.getTotalQuestions())); add(new Label(mathsGame.getRate() + " answers per minute")); add(new Button("Play again!", new ClickListener() { public void onClick(Widget widget) { owner.setupGame(); } })); answerBox.setEnabled(false); } } }
The HTML page would be rather basic. We could name the
element that we add our panels to, but in our case all we
want to do is to add and remove panels to the bottom of the
page. Here is the Maths.html
file, positioned in
the "public" directory constructed by the GWT
applicationCreator.
<html> <head> <title>Welcome to Maxi Maths</title> <link rel=stylesheet href="Maths.css"> <meta name='gwt:module' content='net.kabutz.maxi.gwt.maths.Maths'> </head> <body> <script language="javascript" src="gwt.js"></script> <h1>Welcome to Maxi Maths</h1> </body> </html>
There is quite a lot we can do with CSS. The styles are applied from within the GWT code with setStyleName("name"). Here we see the Maths.css file, which should be in the same directory as the Maths.html file:
body { font-family: sans-serif; font-size: small; } .game-style { background-color: #00FF00; border: 3px solid orange; } .game-style * { padding: 6px; font-family: cursive; font-size: large; } .correct-answer { font-style: italic; color: blue; } .wrong-answer { font-style: oblique; color: red; }
If you put all this together, you get a nice little maths tutor to amuse your kids with.
GWT seems a more comfortable approach for a Java programmer than scratching around in JavaScript. A lot of the coding approaches with Swing can be applied to GWT. However, you do need to control the browsers that your clients will use to connect to your webpage. If someone tries to connect with IE 5, they might just see a blank page.
Kind regards
Heinz
P.S. Let me know if you found the Easter egg in the demo of today's code. The Easter egg is only in the online demo, not in the actual code presented in this newsletter. You will have to figure out how to implement that :-) Also, remember the definition of Easter egg in software is an undocumented feature, not an actual egg. If you discover it, please blog about it, but don't reveal the place it is hidden. Rather leave additional clues.
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.