After Google App Engine was released, we folks over at Google Desktop started thinking about how great it would be to build collaborative, interactive applications using Desktop gadgets in tandem with App Engine. For those new to Google Desktop gadgets, they are graphically rich desktop gadgets (widgets) programmed using XML and JavaScript. Not only are they easy to develop, but your creations can reach the tens of millions of Google Desktop users out there, whether they are on Windows, Mac, or Linux.
Figure 1: Desktop gadgets
Desktop gadgets, as opposed to web-based gadgets and front ends, become a constant, sticky presence on the user's machine. What you do with that space is up to your imagination. You could build something fun, like a multiplayer trivia or word scramble game. Or let's say you have an avatar feature on your site. You could build a Desktop gadget that is used to design and animate the avatar, making use of the transparency, blending, and animation features in the platform.
If you have already built or are planning to build a web application with App Engine, you could consider opening up an API and writing a Desktop gadget as a client. A gadget can enhance the user experience by bringing recent activities or fresh content regularly to the user's desktop. For example, if you have an auction site, a gadget that tracks bids would be very valuable and promote use of the site.
This article shows you how easy it is to implement a desktop client with Desktop gadgets and a complementary server API with App Engine. We hope it inspires you to think of the next world-shattering application that amazes us all.
Do you like questions? How about answers? If you answered yes to either, you'll love our example application. We're going to develop a question and answer server that accomplishes the following:
Here is what the accompanying Desktop gadget will look like:
Figure 2: Client for the Q&A server
This article assumes basic familiarity with App Engine. If you're new, please check out the excellent Getting Started guide. In any case, you should be able to follow the server code with some basic knowledge of web application development.
The Desktop gadget client needs to communicate with the server through an API. If you have a clear idea of what the server does, the API design should be straightforward.
Our application offers these API methods:
Method | Parameters | Description |
---|---|---|
/question |
Returns a random question. See Response formats. | |
/vote |
qid vote |
Records an answer. qid is the ID of the question. vote should be set to 1 for a yes and 0 for a no answer. |
/top |
num |
Returns the list of most voted questions with at most num entries. See Response formats. |
/submit |
question |
Adds a new question to the pool. question contains the question text. |
For simplicity, all requests are HTTP GETs, and the responses are newline-separated plain text. Of course your much cooler API could be 100% RESTful and return XML, JSON, and so on.
Here's a sample response for the /question
method:
ahNkZXNrdG9wZ2FkZ2V0c2FtcGxlcg4LEghRdWVzdGlvbhgCDA Is this a question? 2008-06-24 08:42:09.269583
The first line is the question ID, used to identify the question in other requests. The second line is the question text. The last line is the timestamp in ISO 8601 format.
And here's an example for the /top
response:
Is today a good day? 35 26 Is this a question? 10 11 If a tree falls in the forest, does it make a sound? 0 0
Each question in the list takes up three lines of data. The first line is the question text. The next two lines are the yes and no answer count.
The server's main responsibility is to manage the question and answer data.
App Engine uses data models in its Datastore API.
Here is the definition for the Question
data model:
class Question(db.Model): text = db.StringProperty(required=True) yesCount = db.IntegerProperty(default=0) totalCount = db.IntegerProperty(default=0) randomValue = db.FloatProperty()
Creating, updating, and deleting entities in the datastore is as simple as
calling one of model's built-in methods.
Here's an example of how to create and store a new Question
:
question = Question(text=questionText) question.put()
Queries are just as easy. Here is the code that selects a random question with the help of the randomValue
property.
randomSelection = random.random(); # Select the question with the largest 'randomValue' but within # 'randomSelection'. try: question = Question.all().filter('randomValue <=', randomSelection).order('-randomValue').get() # If randomSelection is too small, we just pick the first question. # Warning: This strategy isn't fair on all questions. if not question: question = Question.all().get() except db.Error: self.response.out.write('DATABASE_ERROR')
The server weighs in at under 200 lines of code. You can get the complete code here or browse the Python script.
We've already discussed the data model. In addition, the script must also process incoming requests and output the responses.
Let's look at the handler for /submit
:
class SubmitQuestionHandler(webapp.RequestHandler): def post(self): self.response.headers['Content-Type'] = 'text/plain' questionText = self.request.get('question') if not questionText: self.response.out.write('INVALID_REQUEST') question = Question(text=questionText) question.randomValue = random.random() question.put() # GAE doesn't support transactions across entity groups. Hence, one of the 2 writes might not happen. # Thus we write the question first so that in the worst-case, our count a bit lagging from the 'actual count' # If the count is slightly less, the last few questions will not be displayed instantaneously. # But that is okay.(better than 404s/retries that can be caused by incrementing the count first) Counter.increment()
The handler creates a new Question
data object with the provided
question text and saves it to the datastore. Now let's look at the
implementation for /top
.
# Handler for 'GET' requests for the top questions. class TopQuestionsHandler(webapp.RequestHandler): def get(self): self.response.headers['Content-Type'] = 'text/plain' numQuestions = self.request.get('num') if not numQuestions: numQuestions = 10 elif (numQuestions > 25): numQuestions = 25 questions = Question.all().order("-totalCount").fetch(limit=numQuestions) for question in questions: noCount = question.totalCount - question.yesCount self.response.out.write("%s\n%ld\n%ld\n\n" % (question.text, question.yesCount, noCount))
The method contains logic to determine the number of questions to return. It then retrieves a list of questions, reverse ordered by the total votes. The other handlers are just as simple.
Again, the data model makes it fairly easy to implement a basic CRUD API for your web application.
Now that we have an API, it's time to bring the application to life. Please download the Desktop gadget, try it out, and contribute a question or two (keep it clean!). For the remainder of this article, we look at a simplified version of the gadget.
The gadget contains three source files: main.xml
, main.js
, and gadget.gmanifest
.
main.xml
describes the UI:
<view height="150" width="400" onopen="view_onOpen();"> <div enabled="true" height="100%" hitTest="htclient" width="100%" background="#000000"> <div enabled="true" height="148" hitTest="htclient" width="398" x="1" y="1" background="#DDDDFF"> <a height="25" name="yes" width="45" x="78" y="105" size="10" bold="true" onclick="answerQuestion(1);"> YES;</a> <a height="25" name="no" width="45" x="178" y="105" size="10" bold="true" onclick="answerQuestion(0);"> NO;</a> <a height="25" name="skip" width="45" x="278" y="105" size="10" bold="true" onclick="getNextQuestion();"> SKIP;</a> <label height="75" name="question" width="250" x="50" y="20" size="10" bold="true" multiline="true" wordwrap="true">LOADING_QUESTION;</label> </div> </div> <script src="main.js" /> </view>
Figure 3: Sample gadget UI
The UI description is quite similar to HTML.
We use a label
to output the status and current question and a
elements (links) to trigger actions.
The application code is defined in main.js
. Desktop gadgets use XMLHttpRequest
, which should be familiar to most of us.
We use it to interact with the server API.
Here is an example of getting a question:
// Fetches a new question from the server and displays it. function getNextQuestion() { is_loading = true; question.innerText = 'Loading question...'; var req = new XMLHttpRequest(); // Append a random number to the url so that we do not get the question from // the cache. req.open('GET', 'http://desktopgadgetsample.appspot.com/question?' + Math.random(), true); req.onreadystatechange = function() { if (req.readyState == 4) { if (req.status == 200) { onQuestionData(req.responseText); } else { getNextQuestion(); } req = null; } }; req.send(); }
Here's the code that parses the data:
// Called when the question is received from the server. function onQuestionData(response_text) { var data = response_text.split('\n'); question_info = {}; question_info.id = data[0]; question_info.text = data[1]; question_info.timestamp = data[2]; question.innerText = question_info.text; is_loading = false; }
The last file, gadget.gmanifest
, contains metadata such as the gadget name's and description.
As you've seen, the Desktop gadget API uses XML to describe the UI with HTML-like tags. It also uses standard JavaScript to implement application logic, manipulate the UI, and request remote data.
To learn more about Desktop gadgets, please visit the Google Desktop Gadget API page. You'll find links to tutorials, docs, and other resources.
As a programmer, you've probably noticed some flaws in the example Desktop gadget and server API. Well then, let me present "exercises for the reader":
Desktop gadgets are a convenient way to build and distribute a desktop client for your server application. App Engine makes it easy to expose a server API, especially since CRUD operations are nicely encapsulated in the data model. We encourage everybody to bring your sites to the desktop. Please visit us on our developer group if you wish to discuss ideas or need help.
Paneendra once killed a cat with his bare hands.
Vishwajith is an Indus River dolphin.
James has lost three cell phones and broken four within the past few years. Needless to say, he won't be buying an iPhone.